Review (xema/review)
The review action is a built-in workflow action that runs a reviewer pool over one or more artifacts and (optionally) loops a producer step until the artifacts are approved. It composes the decision gate under the hood: each iteration dispatches one fresh decision-gate inquiry, the verdicts feed back into the loop, and on reject the producer can be re-dispatched with the prior reviewer feedback merged into its agent context.
Three authoring shapes are supported, each with the same primitive:
- Redraft loop —
subject+redraft.step. Reviewers vote on the producer's output; reject re-dispatches the producer; loop until approve or all reviewers exhaust. - Terminal review —
subjectonly. Reviewers vote once; reject exits withoutcome: rejectedand downstream branching takes over (use this when the artifact comes from outside the workflow — SCM PR, uploaded doc — and there is nothing to redraft). - Approval-to-proceed gate — neither
subjectnorredraft. Reviewers vote yes/no on a question with no artifact attached (release sign-off, deploy approval, …).
When to use it
- A draft → review → fix → re-review loop with a producer that can be re-dispatched on reject.
- An artifact that comes from outside the workflow needs sign-off (PR review, externally-uploaded doc).
- A multi-stakeholder approval where reviewers must justify their verdict.
- A go/no-go decision gate (no artifact, just a yes/no).
If you need to pin specific artifact versions through approval (the
caller wants the pinned (artifactId, versionId, version, hash) tuple
in the gate's output for downstream xema/publish-kb), use
xema/decision-gate directly instead. The
review action surfaces a free-form subject and is not tied to the
artifact-pinning model.
Quick examples
Redraft loop
A producer agent drafts requirements; one human + one agent reviewer
vote all_of. On reject, the producer re-runs with reviewer feedback
in agentContext.review. The agent reviewer is auto-demoted to
advisory after 3 rejects so a stuck unanimous-reject pool can never
block the human approver.
1jobs:
2 draft:
3 uses: xema/agent
4 with:
5 agentRef: requirements-writer
6 deliverableSpecRef: requirements-standard
7 agentContext:
8 prompt: "Write requirements for the requested feature."
9
10 review:
11 needs: [draft]
12 uses: xema/review
13 with:
14 subject: ${{ needs.draft.outputs.structuredOutput }}
15 redraft: { step: draft }
16 reviewers:
17 - kind: agent
18 target: { agentRef: requirements-reviewer }
19 mandatory: true
20 agentMaxIterations: 3
21 - kind: human
22 target: { userId: ${{ trigger.actorSubject }} }
23 mandatory: true
24 policy: { kind: all_of }
25 iterationTimeoutSeconds: 86400
26 onIterationTimeout: reject
27 outputs:
28 outcome: ${{ job.outputs.outcome }}
29 finalDraft: ${{ job.outputs.finalDraft }}Terminal review (PR-style — no in-loop redraft)
The artifact arrives from outside the workflow. Reject ends the run; downstream branching reacts.
1jobs:
2 review-pr:
3 uses: xema/review
4 with:
5 subject: ${{ inputs.prUrl }}
6 reviewers:
7 - kind: human
8 target: { userId: ${{ inputs.author }} }
9 mandatory: true
10 policy:
11 kind: single
12 requireReason: on_reject
13 iterationTimeoutSeconds: 86400
14 onIterationTimeout: reject
15 outputs:
16 outcome: ${{ job.outputs.outcome }}
17 summary: ${{ job.outputs.summary }}
18
19 notify-rejected:
20 needs: [review-pr]
21 if: ${{ needs.review-pr.outputs.outcome == 'rejected' }}
22 uses: xema/scm-post-review
23 with:
24 url: ${{ inputs.prUrl }}
25 verdict: changes-requested
26 body: ${{ needs.review-pr.outputs.summary }}Approval-to-proceed gate
No artifact. Reviewers say yes or no.
1jobs:
2 approve-release:
3 uses: xema/review
4 with:
5 subject: null # or omit entirely
6 reviewers:
7 - kind: human
8 target: { userId: ${{ vars.releaseManager }} }
9 mandatory: true
10 policy:
11 kind: single
12 requireReason: always
13 iterationTimeoutSeconds: 86400
14 onIterationTimeout: rejectInputs
| Field | Type | Required | Description |
|---|---|---|---|
reviewId | string | optional | Stable id used as the per-iteration inquiry id prefix. Auto-injected as <runId>:<jobKey>:<matrixIndex> when omitted. |
subject | string OR array of strings | optional | Artifact(s) under review. Each entry is an arbitrary expression (typically ${{ needs.<step>.outputs.structuredOutput }}, the producing Agent job's promoted handoff). Omit for an approval-to-proceed gate. |
redraft | object | optional | Producer step to re-dispatch on reject. Shape { step: <stepName> }. Requires subject to be set. Omit for terminal reviews. |
reviewers | array (1+) | yes | Reviewer pool — humans, agents, endpoints, identity groups, sub-workflows. See Reviewer kinds below. |
policy | object | yes | Aggregation policy. See Reply policies below. |
iterationTimeoutSeconds | integer | yes | Per-iteration deadline (1–604800). Same semantics as decision-gate's timeoutSeconds. |
onIterationTimeout | approve | reject | escalate | yes | What happens when an iteration's deadline elapses. |
escalation | object | optional | Recipients added when all mandatory reviewers exhaust without approving. Shape { recipients: [...], timeoutSeconds: int }. |
subject shape
The most common pattern is a single expression resolving to the producing step's promoted output:
1subject: ${{ needs.draft.outputs.structuredOutput }}For multi-deliverable reviews (one decision spans several artifacts), pass an array:
1subject:
2 - ${{ needs.draft-spec.outputs.structuredOutput }}
3 - ${{ needs.draft-design.outputs.structuredOutput }}redraft.step rules
The compiler validates redraft.step at compile time:
- The named step must exist in the same workflow.
- The named step must use
xema/agent@<semver>(the only producer shape currently supported). - The named step must be in this review step's
needs:. - The named step cannot itself use a matrix strategy (redraft of one matrix entry is not defined).
redraftrequiressubjectto be set.
The compiler auto-extends the review step's needs: with the
producer step's transitive needs, so any ${{ needs.* }} expression
the producer's with: references resolves at dispatch.
Reviewer kinds
Identical to decision-gate recipients:
human, agent, endpoint, workflow, identity_group. Plus two
review-specific fields per recipient:
| Field | Type | Default | Description |
|---|---|---|---|
mandatory | boolean | true | When false, the recipient's verdict is recorded but does not gate. Useful for advisory reviewers. |
agentMaxIterations | integer | unbounded | Agent / workflow recipients only. After this many rejects from the same recipient, it is auto-demoted to advisory for the rest of the loop. Caps stuck-loop scenarios. |
1reviewers:
2 - kind: agent
3 target: { agentRef: gate-reviewer-architecture }
4 mandatory: true
5 agentMaxIterations: 3
6 - kind: agent
7 target: { agentRef: gate-reviewer-style }
8 mandatory: false # Advisory — voice without veto.
9 - kind: human
10 target: { userId: ${{ trigger.actorSubject }} }
11 mandatory: true # Humans always mandatory; never demoted.Reply policies
Same shape as the decision gate, plus an optional requireReason:
| Kind | Win condition | Lose condition |
|---|---|---|
single | First reply wins. | n/a |
any_of | First non-abstain reply wins. | n/a |
all_of | All mandatory recipients reply approve. | Any mandatory recipient rejects. |
m_of_n | At least m mandatory recipients approve. | Remaining mandatory recipients can no longer reach m. |
Advisory recipients (those with mandatory: false) do not gate the
policy — their verdicts are still recorded and shown to other reviewers
on the next iteration.
requireReason
Closed enum, defaults to on_reject:
| Value | Behavior |
|---|---|
always | Every verdict (approve, reject, abstain) requires a reason. |
on_reject | Only reject and abstain require a reason. |
never | Reason is optional on every verdict. |
The verdict UI on the Tasks page enforces this in the human form. Agent and endpoint deciders that omit a required reason are treated as ABSTAIN.
Iteration mechanics
Each iteration is a fresh decision-gate inquiry with a stable id of
<reviewId>:iter:<N>. The flow:
- Resolve the draft. Iter 1 uses the resolved
subjectdirectly (no producer call — the standalone draft step ran upstream). Iter 2+ re-dispatches theredraftaction with the prior reviewer feedback inagentContext.review.priorReviewerFeedback. - Apply demotion. Each agent / workflow reviewer in the demoted
set flips to
mandatory: false. - Check exhaustion. If no mandatory reviewer remains, apply
escalationif configured; else terminate asoutcome: exhausted. - Run the gate. Dispatch the decision-gate child workflow.
- Branch on the verdict.
approved/expired_approved→ returnoutcome: approved.expired_rejected→ returnoutcome: expired.cancelled→ returnoutcome: cancelled.rejectedwithredraftset → bump per-recipient reject counters, demote any that hitagentMaxIterations, loop.rejectedwithoutredraft(terminal review) → returnoutcome: rejected.
The loop boundary uses continueAsNew every 25 iterations to keep
runtime history bounded under runaway human-driven loops.
Outputs
| Field | Type | Description |
|---|---|---|
outcome | approved | rejected | expired | exhausted | cancelled | Terminal outcome. |
subjects | array | Resolved subjects mirrored from input. Empty for approval-to-proceed gates. |
iterations | integer | Number of iterations actually run (always ≥ 1). |
summary | string | Aggregated reviewer-side summary from the final iteration's verdicts (one line per reviewer with verdict + optional reason). Suitable for posting back to the source system (e.g. as an SCM review comment via xema/scm-post-review). |
finalDraft | object | The artifact reviewed in the final iteration. For iter-1 approval, it is the resolved subject. For iter-N approval, it is the redraft producer's output — same shape as xema/agent's outputs. Absent on approval-to-proceed gates. |
decisionTrail | array | Per-iteration record { iteration, draft, reviewerVerdicts, demotedAgents }. |
finalDraft mirrors whatever subject resolved to, so downstream consumers
should read the same shape they passed in. Note that finalDraft.deliverables
— the shape earlier revisions of this page recommended — is empty: the
xema/agent job output it came from was removed on 2026-08-29. See
the deliverables note.
Cancellation, timeouts, and escalation
- Cancellation. When the parent run is cancelled, the review
workflow propagates cancellation to the in-flight decision-gate
iteration; the inquiry transitions to
cancelled(visible on the Tasks page so reviewers stop deliberating); the review's output isoutcome: cancelledwith the partialdecisionTrail. - Iteration timeout. Each iteration's
iterationTimeoutSecondscontrols only the human-gate window for that iteration. Timeout applies the configuredonIterationTimeoutaction; the loop does not implicitly retry. - Reviewer exhaustion. If every mandatory reviewer is demoted to
advisory and no escalation is configured, the workflow returns
outcome: exhausted. Withescalationset, the listed recipients are added to the pool for the next iteration; the loop continues.
Common patterns
Mixed reviewers with redraft
Three agents (architecture, quality, governance) plus one human under
all_of. Each agent demoted after 3 rejects.
1review:
2 needs: [synthesize]
3 uses: xema/review
4 with:
5 subject: ${{ needs.synthesize.outputs.structuredOutput }}
6 redraft: { step: synthesize }
7 reviewers:
8 - { kind: agent, target: { agentRef: gate-reviewer-architecture }, mandatory: true, agentMaxIterations: 3 }
9 - { kind: agent, target: { agentRef: gate-reviewer-quality }, mandatory: true, agentMaxIterations: 3 }
10 - { kind: agent, target: { agentRef: gate-reviewer-governance }, mandatory: true, agentMaxIterations: 3 }
11 - { kind: human, target: { userId: ${{ trigger.actorSubject }} }, mandatory: true }
12 policy: { kind: all_of }
13 iterationTimeoutSeconds: 86400
14 onIterationTimeout: rejectMulti-deliverable review
One decision spans the spec + the design doc.
1review:
2 needs: [draft-spec, draft-design]
3 uses: xema/review
4 with:
5 subject:
6 - ${{ needs.draft-spec.outputs.structuredOutput }}
7 - ${{ needs.draft-design.outputs.structuredOutput }}
8 # No redraft when multiple producers are involved — handle reject
9 # by failing the workflow or branching downstream and re-running
10 # the upstream producers as a separate dispatch.
11 reviewers:
12 - { kind: human, target: { userId: ${{ trigger.actorSubject }} }, mandatory: true }
13 policy: { kind: single }
14 iterationTimeoutSeconds: 86400
15 onIterationTimeout: rejectm_of_n quorum
Three reviewers, two must approve.
1policy:
2 kind: m_of_n
3 m: 2
4 abstainTreatment: skipRequired justification on every verdict
1policy:
2 kind: single
3 requireReason: alwaysEscalation when reviewers exhaust
1escalation:
2 recipients:
3 - kind: identity_group
4 target: { groupId: ${{ vars.SECURITY_CHAMPIONS }} }
5 mandatory: true
6 timeoutSeconds: 86400How it differs from the decision gate
xema/review | xema/decision-gate | |
|---|---|---|
| Loop on reject | Yes (when redraft is set) | No (single inquiry) |
subject shape | Free-form (string / array of any expression) | Pinned subjectArtifacts (artifactId+versionId+version+hash) |
| Output shape | outcome + subjects + summary + finalDraft + decisionTrail | outcome + approvedArtifacts (pinned) + decisionTrail |
requireReason | Yes | No (always optional) |
Reviewer demotion (agentMaxIterations) | Yes | No |
xema/publish-kb integration | Read finalDraft.deliverables from the FINAL APPROVED iteration | Read pinned approvedArtifacts[*] directly |
Use the decision gate when downstream needs the pinned-version guarantee (the bytes the deciders approved are the bytes that get published). Use the review action when you want a redraft loop, a free-form subject, or an approval-to-proceed gate.
Previous: ← Decision Gate