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
One organization id decided three different things at once in
`openNodeRequest`: where the request row lives, where its inbox index rows
live, and where its approvers are looked up. The first two are the request's
own organization by definition. The third is not — a group CFO holds her `cfo`
position in the GROUP organization while the purchase order she signs off
lives in the PLANT organization. `expandPositionUsers('cfo', <plant>)` matched
nobody, the slot fell back to the dead `position:cfo` literal, and a group
escalation could not be expressed at all.
An approver may now declare which organization's directory resolves it:
- { type: position, value: plant_manager, group: plant }
- { type: position, value: cfo, organization: $root, group: finance }
Four design points the ADR left open, settled from what the code and the
authoring model require (ADR text amended in this commit):
- PER APPROVER, not per node. The node-level form cannot express the commonest
group shape — one node requiring a plant manager AND a group CFO in parallel.
Splitting into serial nodes changes the semantics rather than the syntax. A
node-level default remains addable later as sugar; the reverse is not true.
- SYMBOLS FIRST (`$root` / `$parent`). Flow metadata is portable across
environments; an organization id is data minted per deployment, so a literal
id in a flow is unportable by construction and an AI author cannot know one.
The symbols walk D6's `parent_organization_id` tree with zero deployment
knowledge. A slug covers what they cannot — notably a SIBLING organization
(a shared-services centre approving payables for every plant).
- LEGALITY IS "SHARES A ROOT", not "is an ancestor" — the sibling case above is
first-class. The rule reads only the organization tree, never the submitter,
so one flow routes identically for everyone and a routing bug is reproducible.
- NON-`group` POSTURES REFUSE at runtime. Posture is environment configuration,
so the same portable metadata deploys into any posture and no static check
can see which; ignoring the declaration would let a group → isolated
migration reroute approvals with no signal.
Two failure modes are made loud rather than silent: an approver type with no
org-scoped directory (`user`/`field`/`manager`/`team`) refuses the declaration
instead of ignoring it, with a new `approval-approver-cross-org-unsupported`
lint catching it at author time; and a targeted approver holding no membership
in the request's organization is dropped with a warning naming them — D2's
union wall would otherwise hide the request from someone already routed to,
leaving a task she cannot open. Dropping hands it to the node's existing
`onEmptyApprovers` policy.
An approver without `organization` is unchanged: same resolution, same
queries, no extra reads.
Tests: 17 resolver units (symbols, sibling/foreign slugs, posture, cycles,
unreadable-membership), 7 service integration (including the pre-D9 gap, the
mixed plant+group node, and that the request still belongs to the request org),
4 lint. plugin-approvals 305, lint 500, spec 6736, cli 682 all green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
Copy file name to clipboardExpand all lines: content/docs/references/automation/approval.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -58,6 +58,7 @@ const result = ApprovalDecision.parse(data);
58
58
|**value**|`string`| optional | User id / membership tier / position / team / department / field — per `type`; for `expression`, a CEL expression over `current.*` / `trigger.*` / `vars.*`|
59
59
|**resolveAs**|`Enum<'user' \| 'department' \| 'position' \| 'team'>`| optional | How an `expression` result is expanded into approvers (default 'user') |
60
60
|**group**|`string`| optional | Group label for per_group sign-off (e.g. "legal", "finance") |
61
+
|**organization**|`string`| optional | ADR-0105 D9 — organization whose directory resolves this approver: `$root` (group org), `$parent` (one level up), or an organization slug. Omitted = the request's own organization. |
61
62
62
63
63
64
---
@@ -68,7 +69,7 @@ const result = ApprovalDecision.parse(data);
0 commit comments