Decision Gate (xema/decision-gate)
The decision gate is a built-in workflow action that pauses a run while
one or more deciders (humans, agent sessions, or external endpoints)
review pinned artifact versions and produce a binding verdict. Replies
aggregate per a declared policy; downstream consumers (e.g. xema/publish-kb)
read the gate's approvedArtifacts output directly so the bytes that get
published are exactly the bytes the deciders approved.
When to use it
- A multi-step workflow needs human sign-off before continuing.
- A reviewer LLM should produce an automated verdict on a deliverable.
- An external compliance endpoint must clear a change before it ships.
- A combination of the above with a quorum policy (e.g. "2 of these 3 must approve").
If you only need a fixed delay (no decision), use xema/wait
instead.
Quick example
Single human reviewer, 24-hour deadline, reject on timeout:
1jobs:
2 approve-requirements:
3 needs: [requirements]
4 matrixGather: [requirements]
5 uses: xema/decision-gate
6 with:
7 title: Approve requirements
8 timeoutSeconds: 86400
9 onTimeout: reject
10 subjectArtifacts: ${{ needs.requirements.outputs }}
11 recipients:
12 - kind: human
13 target: { userId: ${{ trigger.actorSubject }} }
14 policy:
15 kind: single
16 outputs:
17 outcome: ${{ job.outputs.outcome }}
18
19 publish:
20 needs: [approve-requirements, requirements]
21 if: ${{ needs.approve-requirements.outputs.outcome == 'approved' }}
22 uses: xema/publish-kb
23 with:
24 # The publish step reads the pinned (artifactId, versionId, version, hash)
25 # tuple from the gate output. It NEVER picks "latest" — the bytes the
26 # deciders approved are the bytes that get published.
27 artifactRef: ${{ needs.approve-requirements.outputs.approvedArtifacts[0] }}Inputs
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Human-readable label shown on the Tasks page card. |
timeoutSeconds | integer | yes | Deadline in seconds (1 to 604800 = 7 days). |
onTimeout | approve | reject | yes | What happens if the policy isn't satisfied before the deadline. |
correlationId | string | yes | Run correlation id for audit. Typically ${{ trigger.correlationId }}. |
inquiryId | string | optional | Stable id used as the inquiry key. Defaults to a deterministic value derived from the parent workflow + job key. |
description | string | optional | Free-form context shown to humans + included in agent decider prompts. |
subjectArtifacts | array | optional | Upstream job outputs carrying pinned (artifactId, versionId, version, hash) references — for an Agent job, its promoted structuredOutput. The gate flattens them into the inquiry. Empty/omitted means the gate is wrapping a non-deliverable concern (e.g. a release sign-off without a specific artifact). |
recipients | array (1+) | yes | Who decides. Each entry is a discriminated { kind, target } — see below. |
policy | object | yes | How replies aggregate. See Reply policies below. |
Recipient kinds
human — a user identified by their identity-system subject
1recipients:
2 - kind: human
3 target:
4 userId: [email protected]
5 - kind: human
6 target:
7 # Special token: resolves at runtime to the user who triggered the run.
8 # Not allowed when triggerKind=schedule (no triggering user exists).
9 userId: ${{ trigger.actorSubject }}The recipient sees the inquiry on the Tasks page (sidebar entry) and clicks through to the focused decision page where they approve or reject.
agent — an LLM-driven decider
1recipients:
2 - kind: agent
3 target:
4 agentRef: senior-reviewer
5 invocationConfig:
6 # Optional per-recipient overrides — adapter-specific.
7 modelTier: preciseThe agent runs as a peer activity, reviews the pinned subjects, and submits a structured verdict. Schema-violation in the agent's reply is treated as ABSTAIN.
endpoint — an external HTTP check
1recipients:
2 - kind: endpoint
3 target:
4 url: https://compliance.example.com/api/check
5 authRef: service-token-compliance
6 timeoutMs: 30000
7 verdictMapper:
8 # JSONPath into the response body, plus a value→verdict map.
9 jsonPath: $.decision
10 verdictMap:
11 allow: approve
12 deny: reject
13 unknown: abstainThe endpoint receives the pinned subjects + description as a JSON POST body and returns a verdict via the configured response shape.
Reply policies
| Kind | Win condition | Lose condition |
|---|---|---|
single | First reply wins (any verdict). | n/a |
any_of | First non-abstain reply wins. | n/a |
all_of | All recipients reply approve. | Any recipient rejects. |
m_of_n | At least m recipients reply approve. | Remaining recipients can no longer reach m approvals. |
m_of_n example — 2 of 3 reviewers must approve:
1policy:
2 kind: m_of_n
3 m: 2
4 # Optional: how agent/endpoint ABSTAIN replies count under the
5 # threshold. Default 'skip' treats abstain as a non-vote; the gate
6 # waits for the remaining recipients. 'reject_implicit' counts an
7 # abstain as a rejection (closes the slot in the no column).
8 abstainTreatment: skipThe gate short-circuits as soon as the outcome is provable:
m_of_nresolves APPROVE the momentapprovedCount >= m.m_of_nresolves REJECT the momentapprovedCount + pendingCount < m(quorum is unreachable).all_ofresolves REJECT on the first reject.any_of/singleresolve on the first reply.
When the gate resolves, any in-flight non-human decider activities are
cancelled and the corresponding recipient rows transition to skipped
with a reason code (quorum_reached, quorum_unreachable, all_of_reject,
first_reply_resolved).
Outputs
| Field | Type | Description |
|---|---|---|
outcome | approved | rejected | expired_approved | expired_rejected | cancelled | Terminal outcome. |
approvedArtifacts | array of pinned refs | Non-empty iff outcome is approved or expired_approved. Each entry is {artifactId, versionId, version, hash}. |
decisionTrail | array | Per-recipient decision records: {recipientId, verdict, reason, submittedBy, submittedAtIso}. |
inquiryId | string | The stable inquiry id (useful for audit cross-references). |
Downstream consumers MUST read the pinned versionId from
approvedArtifacts rather than picking a "latest" version — this is the
guarantee that the bytes the deciders approved are the bytes that get
published.
Cancellation and timeouts
If the parent run is cancelled (operator clicks "Cancel" on the run-detail
page), the gate workflow is cancelled too: outstanding decider activities
are cancelled, the inquiry is marked cancelled (visible on the Tasks
page so reviewers know not to keep deliberating), and the gate's output
is outcome: cancelled with an empty approvedArtifacts.
If the deadline passes before the policy is satisfied, the gate applies
onTimeout (either approve or reject) and produces
outcome: expired_approved or expired_rejected accordingly. There is no
recursive escalation in v1 — to chain reviewers, model it as a multi-step
workflow with separate gate jobs.
Common patterns
Single human approver (the simplest case)
1- kind: human
2 target: { userId: ${{ trigger.actorSubject }} }
3policy:
4 kind: singleTwo humans, either can decide
1recipients:
2 - kind: human
3 target: { userId: [email protected] }
4 - kind: human
5 target: { userId: [email protected] }
6policy:
7 kind: any_ofQuorum (2-of-3)
1recipients:
2 - kind: human
3 target: { userId: [email protected] }
4 - kind: human
5 target: { userId: [email protected] }
6 - kind: human
7 target: { userId: [email protected] }
8policy:
9 kind: m_of_n
10 m: 2Human + agent reviewer (any one approves)
1recipients:
2 - kind: human
3 target: { userId: ${{ trigger.actorSubject }} }
4 - kind: agent
5 target: { agentRef: senior-reviewer }
6policy:
7 kind: any_ofCompliance endpoint must clear, plus a human
1recipients:
2 - kind: endpoint
3 target:
4 url: https://compliance.example.com/check
5 authRef: service-token-compliance
6 verdictMapper:
7 jsonPath: $.decision
8 verdictMap:
9 allow: approve
10 deny: reject
11 - kind: human
12 target: { userId: ${{ trigger.actorSubject }} }
13policy:
14 kind: all_ofPrevious: ← Troubleshooting Next: Review →