Skip to content

Commit 0983b14

Browse files
committed
fix(approvals): regen approval reference doc + behavior test/docs for quorum/per_group (#3266)
CI follow-ups on the #3266 spec change: - Regenerate content/docs/references/automation/approval.mdx (generated from the spec schema; the new behavior enum / minApprovals / approver group made it stale). - approval.test.ts: the "rejects an unknown behavior" case hard-coded 'quorum' as its unknown value — now a valid behavior. Switch it to 'weighted' and add positive coverage that quorum / per_group / minApprovals / grouped approvers parse. - approvals.mdx: document the behavior matrix (first_response / unanimous / quorum / per_group), the veto rule, minApprovals clamping, and decision attachments. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
1 parent cc46574 commit 0983b14

3 files changed

Lines changed: 49 additions & 3 deletions

File tree

content/docs/automation/approvals.mdx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,35 @@ export const opportunityApproval: Flow = {
117117
For multi-step approvals, chain multiple `approval` nodes. For parallel
118118
approvals, see the aggregating-node pattern in [Flow Metadata](/docs/automation/flows).
119119

120+
### Combining multiple approvers (#3266)
121+
122+
When a node has several approvers, `behavior` decides what "approved" means —
123+
one node, no parallel branches needed:
124+
125+
| `behavior` | Advances when… | Use for |
126+
|:---|:---|:---|
127+
| `first_response` (default) | any one approves | single reviewer / "any manager" |
128+
| `unanimous` | every resolved approver approves | small fixed panels |
129+
| `quorum` | `minApprovals` of N approve (M-of-N) | "2 of 3 directors" |
130+
| `per_group` | each `group` reaches `minApprovals` (default 1) | "legal **and** finance" sign-off (会签) |
131+
132+
```ts
133+
// One legal AND one finance approver must sign off; either rejection vetoes.
134+
{ type: 'approval', config: {
135+
approvers: [
136+
{ type: 'position', value: 'legal_counsel', group: 'legal' },
137+
{ type: 'position', value: 'controller', group: 'finance' },
138+
],
139+
behavior: 'per_group', // or 'quorum' with minApprovals: 2
140+
} }
141+
```
142+
143+
A single **rejection** always finalizes the node as `rejected` (one veto), in
144+
every mode. `minApprovals` is clamped to the resolvable approver count / group
145+
size, so it can never deadlock. A decision may also carry `attachments` (file
146+
references) recorded on its audit row — e.g. a signed contract. Weighted voting
147+
and approval-matrix governance are enterprise, not here.
148+
120149
## The full lifecycle — submit to field change
121150

122151
What actually happens between "the flow hits the approval node" and "the record

content/docs/references/automation/approval.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const result = ApprovalDecision.parse(data);
5656
| :--- | :--- | :--- | :--- |
5757
| **type** | `Enum<'user' \| 'org_membership_level' \| 'role' \| 'position' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>` || |
5858
| **value** | `string` | optional | User id / membership tier / position / team / department / field / queue — per `type` |
59+
| **group** | `string` | optional | Group label for per_group sign-off (e.g. "legal", "finance") |
5960

6061

6162
---
@@ -66,8 +67,9 @@ const result = ApprovalDecision.parse(data);
6667

6768
| Property | Type | Required | Description |
6869
| :--- | :--- | :--- | :--- |
69-
| **approvers** | `{ type: Enum<'user' \| 'org_membership_level' \| 'role' \| 'position' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>; value?: string }[]` || Allowed approvers for this node |
70-
| **behavior** | `Enum<'first_response' \| 'unanimous'>` || How to combine multiple approvers |
70+
| **approvers** | `{ type: Enum<'user' \| 'org_membership_level' \| 'role' \| 'position' \| 'team' \| 'department' \| 'manager' \| 'field' \| 'queue'>; value?: string; group?: string }[]` || Allowed approvers for this node |
71+
| **behavior** | `Enum<'first_response' \| 'unanimous' \| 'quorum' \| 'per_group'>` || How to combine multiple approvers |
72+
| **minApprovals** | `integer` | optional | Approvals required — total (quorum) or per group (per_group). Default 1 |
7173
| **lockRecord** | `boolean` || Lock the record from editing while pending |
7274
| **approvalStatusField** | `string` | optional | Business-object field to mirror request status onto |
7375
| **escalation** | `{ enabled: boolean; timeoutHours: number; action: Enum<'reassign' \| 'auto_approve' \| 'auto_reject' \| 'notify'>; escalateTo?: string; … }` | optional | Per-node SLA escalation |

packages/spec/src/automation/approval.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,22 @@ describe('ApprovalNodeConfigSchema', () => {
133133
});
134134

135135
it('rejects an unknown behavior', () => {
136-
expect(() => ApprovalNodeConfigSchema.parse({ ...minimal, behavior: 'quorum' })).toThrow();
136+
expect(() => ApprovalNodeConfigSchema.parse({ ...minimal, behavior: 'weighted' })).toThrow();
137+
});
138+
139+
it('accepts quorum / per_group behaviors with minApprovals and grouped approvers (#3266)', () => {
140+
const quorum = ApprovalNodeConfigSchema.parse({ ...minimal, behavior: 'quorum', minApprovals: 2 });
141+
expect(quorum.behavior).toBe('quorum');
142+
expect(quorum.minApprovals).toBe(2);
143+
const perGroup = ApprovalNodeConfigSchema.parse({
144+
approvers: [
145+
{ type: 'position', value: 'legal_counsel', group: 'legal' },
146+
{ type: 'position', value: 'controller', group: 'finance' },
147+
],
148+
behavior: 'per_group',
149+
});
150+
expect(perGroup.behavior).toBe('per_group');
151+
expect(perGroup.approvers[0].group).toBe('legal');
137152
});
138153
});
139154

0 commit comments

Comments
 (0)