You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 16c3601
Browse filesBrowse the repository at this point in the historyBrowse files
Flight 6 of the published-skills factual sweep. Every corrected claim was
settled against the implementing code (spec schemas, plugin-approvals,
service-automation, trigger-api / trigger-schedule, @objectstack/lint), never
against another document, with executed parse / CEL / schedule-normalization
probes where the claim is behaviour-bearing.
Net: -24 tokens, -4 lines across the package; the two ratcheted files that
moved stay under their existing ceilings (no ceiling touched).
Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT
Co-authored-by: Claude <noreply@anthropic.com>
|`position`| Holders of a position — `value` = the position machine name, resolved via `sys_user_position` (ADR-0090 D3) |
480
-
|`org_membership_level`| The better-auth **org-membership tier** — `value` is one of `owner`/`admin`/`member`, and nothing else. **NOT** a position: `{ type: 'org_membership_level', value: 'sales_manager' }` matches nobody; use `position`. Spelled `role` before ADR-0090 D3 — that spelling is deprecated, still resolves, and is removed in the next major |
479
+
|`org_membership_level`| The **org-membership tier** — `value` is one of `owner`/`admin`/`delegated_admin`/`member`. **NOT** a position: `{ type: 'org_membership_level', value: 'sales_manager' }` matches nobody; use `position`. Spelled `role` before ADR-0090 D3 — that spelling is deprecated, still resolves, and is removed in the next major |
481
480
|`team`| Members of a flat `sys_team`|
482
481
|`department`| A department + all descendant departments |
483
482
|`manager`| The submitter's manager (`sys_user.manager_id`) |
484
483
|`field`| User id read from a record field (`value` = field name). Resolved against the record's **live** state at node entry, so a field written mid-flow routes correctly; a multi-select user field fans out into one approver per user |
485
-
|`queue`|A data-ownership queue|
484
+
|`queue`|⛔ Declared but never resolved — the slot routes to nobody. Do not author|
486
485
|`expression`| A **CEL expression** resolved at node entry (`value` = the expression) — see **Dynamic approvers** below. Only `current.*` / `trigger.*` / `vars.*` roots are available; the optional `resolveAs: 'user'(default) \| 'department' \| 'position' \| 'team'` re-expands each resolved id through the graph |
487
486
488
487
### Dynamic approvers (`type: 'expression'`)
@@ -602,11 +601,11 @@ pre-write row, both made total over the object's declared fields. See
602
601
|:------|:--------|
603
602
|`approvers`| Who may act (≥ 1 — see Approver Types above). Each approver may carry an optional **`group`** label (e.g. `{ type: 'position', value: 'auditor', group: 'finance' }`) — with `behavior: 'per_group'`, approvers sharing a label form one group; unlabelled approvers each form their own |
604
603
|`behavior`|`first_response` (first approver decides), `unanimous` (all must approve), `quorum` (`minApprovals` of N — M-of-N collective sign-off), or `per_group` (EACH approver `group` must reach `minApprovals` — one-from-each-group sign-off, 会签). In every mode a single rejection finalizes the node as `rejected`. Default `first_response`|
605
-
|`minApprovals`| Approvals required — total for `quorum`, per group for `per_group`. Default `1`; clamped at runtime to the resolvable approver count so a misconfiguration can never deadlock |
604
+
|`minApprovals`| Approvals required — total for `quorum`, per group for `per_group`. Omitted ⇒ ALL resolvable approvers under `quorum`, `1` per group; clamped at runtime so a misconfiguration can never deadlock |
606
605
|`lockRecord`| Lock the triggering record from edits while pending. Default `true`|
607
606
|`approvalStatusField`| Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) |
608
607
|`onEmptyApprovers`| What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through). Declare it explicitly on any node with an `expression` approver (linted) |
609
-
|`decisionOutputs`| Decision outputs a decision may carry (author declares, approvers fill values). Entries are bare keys (free-text input) **or typed declarations**`{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple? }` — a typed entry renders the matching record picker in the decision dialog (`multiple` collects an id array). Accepted outputs resume the run as `<nodeId>.<key>` variables; undeclared keys reject the decision; `decision`/`requestId` reserved |
608
+
|`decisionOutputs`| Decision outputs a decision may carry (author declares, approvers fill values). Entries are bare keys (free-text input) **or typed declarations**`{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple?, required? }` — a typed entry renders the matching record picker in the decision dialog (`multiple` collects an id array). Accepted outputs resume the run as `<nodeId>.<key>` variables; undeclared keys reject the decision; `decision`/`requestId` reserved |
610
609
|`escalation`| Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) |
611
610
|`maxRevisions`| ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge |
612
611
@@ -620,13 +619,9 @@ These are wired on the **graph**, not in node config:
620
619
`http`, a `notify` node, …) to the `approve` / `reject` out-edge.
621
620
-**Roll back on reject** — route the `reject` edge as a **back-edge** to an
622
621
earlier node so the submitter can revise (the old `back_to_previous`).
623
-
-**Send back for revision (ADR-0044)** — distinct from a plain reject: an
624
-
Approval node can emit a third decision **`revise`** on a `revise`-labeled
625
-
out-edge that routes to an **`approval_revise`** rework window (not a plain
626
-
`wait`). The submitter edits and resubmits, re-entering the node via an
627
-
edge `type: 'back'` (a declared back-edge — traversed at run time but excluded
628
-
from DAG cycle validation). `maxRevisions` (node config, default `3`) caps the
629
-
loop before auto-reject.
622
+
-**Send back for revision (ADR-0044)** — distinct from a plain reject: a
623
+
`revise` out-edge into an **`approval_revise`** window, closed by a
624
+
`type: 'back'` resubmit edge. See *Send-back for revision* above.
630
625
-**Hard reject** — route the `reject` edge to an `end` node (the old
631
626
`reject_process`).
632
627
@@ -666,7 +661,7 @@ defineStack({
666
661
// + 'job' for scheduled (cron) flows
667
662
// + 'queue' for inbound-webhook ('api') flows — the trigger-api plugin
668
663
// depends on the queue service; without it every inbound POST
669
-
// returns 503 queue_unavailable.
664
+
// returns 503 SERVICE_UNAVAILABLE.
670
665
});
671
666
```
672
667
@@ -697,6 +692,7 @@ read at runtime, not Zod-validated):
697
692
|`record-after-update`| after update |`afterUpdate`|
698
693
|`record-before-delete`| before delete |`beforeDelete`|
699
694
|`record-after-delete`| after delete |`afterDelete`|
695
+
|`record-before-write` / `record-after-write`| create OR update — one flow, both events | both insert + update hooks |
700
696
701
697
### Trigger Configuration — on the `start` node
702
698
@@ -725,7 +721,7 @@ read at runtime, not Zod-validated):
725
721
> **`previous`** and **`record`** are the CEL variables available in update
726
722
> triggers — `previous.x` is the value before the change, `record.x` is the
727
723
> value after. (Salesforce-flavor `OLD` / `NEW` were removed in M9.5 and now
728
-
> evaluate to `null`.) See [objectstack-formula](../objectstack-formula/SKILL.md).
724
+
> fault the predicate.) See [objectstack-formula](../objectstack-formula/SKILL.md).
729
725
730
726
### Time-relative triggers — scheduled per-record date sweep
731
727
@@ -850,7 +846,7 @@ them right the first time:
850
846
❌ `'{ROUND(x, 2)}'` / `'{Math.round(x)}'` / `'{(x).toFixed(2)}'` — any other
851
847
name in call position **fails the node** with a named error naming the
852
848
supported set. The build does **not** catch these (conditions are checked,
853
-
value expressions are not) and a `fault` edge cannot route it.
849
+
call-position names are not) and a `fault` edge cannot route it.
854
850
855
851
7.**`create_record`'s `outputVariable` holds the created RECORD, not its id.**
856
852
Reference a field explicitly.
@@ -968,7 +964,7 @@ syntax error, an unknown function (`PRIOR()`) or a `{…}`-wrapped reference
968
964
**throws**, located and corrective — never silent.
969
965
970
966
**Node values** take the single-brace `flow-template` dialect (`'{round(x)}'`);
971
-
no validator implements it, so an unknown function there is NOT build-checked —
967
+
no validator checks its call names, so an unknown function is NOT build-checked —
972
968
it throws `FlowExpressionFunctionError` at **run time**.
973
969
974
970
The quiet case is a typo'd *field* name: bare refs (`status == 'open'`) DO
|[approvals/test-revise-loop.md](./approvals/test-revise-loop.md)| ADR-0044 send-back-for-revision: the `revise` branch, the signal `wait` node, and the resubmit edge declared`type: 'back'`|
11
+
|[approvals/test-revise-loop.md](./approvals/test-revise-loop.md)| ADR-0044 send-back-for-revision: the `revise` branch, the `approval_revise` window, and the resubmit edge `type: 'back'`|
0 commit comments