Skip to content

Commit 3545221

Browse files
committed
docs(approvals): document cross-organization approver targeting
The drift check flagged `automation/approvals.mdx` — correctly. D9 adds an authoring capability, and the hand-written guide is where an author (human or AI) actually looks; a feature reachable only from the generated schema reference is a feature most authors never find. Adds a section covering what the reference table cannot: which value to reach for ($root over a slug, because flow metadata is portable and organization ids are not), and the three things the field deliberately does NOT do — it does not move the request, it is not a route to an arbitrary tenant, and it does not apply to approver kinds that consult no organization directory. Plus the read-visibility precondition, which is the one way a correctly-authored cross-org approval still ends up empty. Also points the approver-kinds paragraph at the new section, so the fact that only some kinds accept `organization` is discoverable from where the kinds are listed rather than only from the lint that rejects it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FebXPaaGrLhGKw1LHPbpL
1 parent e10d4e2 commit 3545221

1 file changed

Lines changed: 66 additions & 1 deletion

File tree

content/docs/automation/approvals.mdx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,68 @@ submit:
173173
with `behavior: 'per_group'`, each intermediate value (e.g. each returned
174174
department) forms its own sign-off group.
175175

176+
## Approving across organizations (ADR-0105 D9) [#cross-org-approvers]
177+
178+
<Callout type="info">
179+
Requires the `group` tenancy posture. Under `single` or `isolated` the runtime
180+
**refuses** an approver that declares `organization`, rather than ignoring it.
181+
</Callout>
182+
183+
A group-shaped deployment routinely needs a plant's document signed off by
184+
someone at the group: the purchase order lives in the **plant** organization,
185+
but the CFO holds her `cfo` position in the **group** organization. By default
186+
an approver is resolved against the request's own organization, so `cfo` would
187+
match nobody there.
188+
189+
Declare which organization's directory resolves that approver:
190+
191+
```typescript
192+
config: {
193+
approvers: [
194+
{ type: 'position', value: 'plant_manager', group: 'plant' },
195+
{ type: 'position', value: 'cfo', organization: '$root', group: 'finance' },
196+
],
197+
behavior: 'per_group', // one plant sign-off AND one group sign-off
198+
}
199+
```
200+
201+
`organization` takes a **symbol** or an organization **slug**:
202+
203+
| Value | Resolves to |
204+
| :--- | :--- |
205+
| `$root` | the group organization — climbs `parent_organization_id` to the top |
206+
| `$parent` | exactly one level up (division sign-off in a three-tier group) |
207+
| a slug, e.g. `acme-ssc` | that organization — for a **sibling**, such as a shared-services centre approving payables for every plant |
208+
209+
Prefer the symbols. Flow metadata is portable across environments while
210+
organization ids are minted per deployment, so `$root` says "the group" without
211+
naming anything deployment-specific. An organization **id** is rejected: pass
212+
the slug.
213+
214+
Three things this deliberately does **not** do:
215+
216+
- **It does not move the request.** Only the approver lookup changes — the
217+
request, its audit trail, and its inbox rows stay in the plant's
218+
organization.
219+
- **It is not a route to any tenant.** The target must share a
220+
`parent_organization_id` root with the request's organization, so approval
221+
routes *within one group*. The rule reads only the organization tree, never
222+
the submitter, so a flow routes identically for everyone.
223+
- **It does not apply to every approver kind.** `user`, `field`, `manager` and
224+
`team` name people without consulting an organization directory;
225+
`organization` on those is refused at runtime and flagged by `os lint`
226+
(`approval-approver-cross-org-unsupported`).
227+
228+
<Callout type="warn">
229+
A cross-organization approver must also be able to **read** the request. The
230+
request stays in the plant's organization, so a group-side approver reaches it
231+
only if she also holds a membership there — the usual group shape is that group
232+
staff are members of every plant while holding their positions at the group.
233+
Approvers without that membership are dropped from the slate with a warning
234+
naming them, and the node's `onEmptyApprovers` policy takes over — better an
235+
empty slate you can see than a task the tenancy wall hides.
236+
</Callout>
237+
176238
A result may legitimately be **empty** (a present-but-empty field or variable);
177239
the node-level `onEmptyApprovers` policy decides what that means —
178240
`admin_rescue` (default: the request opens, a privileged admin takes over via
@@ -230,7 +292,10 @@ hierarchy. `queue` still parses so stored flows keep loading, but it is **not
230292
implemented** by the runtime and is no longer offered for authoring (#3508) —
231293
a queue entry resolves to nobody. Route to a `team`, `department`, or
232294
`position` instead. `field`, `manager`, and `expression` resolve against the record's
233-
**live** state at node entry (#3447). An entry that resolves
295+
**live** state at node entry (#3447). `position`, `department`,
296+
`org_membership_level` and `expression` may additionally name which
297+
organization's directory resolves them — see
298+
[Approving across organizations](#cross-org-approvers). An entry that resolves
234299
to nobody is not an error: the request opens with an empty `pending_approvers`
235300
and nothing can move it, so the run parks forever.
236301

0 commit comments

Comments
 (0)