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:

  1. Redraft loop — subject + redraft.step. Reviewers vote on the producer's output; reject re-dispatches the producer; loop until approve or all reviewers exhaust.
  2. Terminal review — subject only. Reviewers vote once; reject exits with outcome: rejected and downstream branching takes over (use this when the artifact comes from outside the workflow — SCM PR, uploaded doc — and there is nothing to redraft).
  3. Approval-to-proceed gate — neither subject nor redraft. 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.

yaml
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.

yaml
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.

yaml
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: reject

Inputs

FieldTypeRequiredDescription
reviewIdstringoptionalStable id used as the per-iteration inquiry id prefix. Auto-injected as <runId>:<jobKey>:<matrixIndex> when omitted.
subjectstring OR array of stringsoptionalArtifact(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.
redraftobjectoptionalProducer step to re-dispatch on reject. Shape { step: <stepName> }. Requires subject to be set. Omit for terminal reviews.
reviewersarray (1+)yesReviewer pool — humans, agents, endpoints, identity groups, sub-workflows. See Reviewer kinds below.
policyobjectyesAggregation policy. See Reply policies below.
iterationTimeoutSecondsintegeryesPer-iteration deadline (1–604800). Same semantics as decision-gate's timeoutSeconds.
onIterationTimeoutapprove | reject | escalateyesWhat happens when an iteration's deadline elapses.
escalationobjectoptionalRecipients 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:

yaml
1subject: ${{ needs.draft.outputs.structuredOutput }}

For multi-deliverable reviews (one decision spans several artifacts), pass an array:

yaml
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).
  • redraft requires subject to 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:

FieldTypeDefaultDescription
mandatorybooleantrueWhen false, the recipient's verdict is recorded but does not gate. Useful for advisory reviewers.
agentMaxIterationsintegerunboundedAgent / 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.
yaml
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:

KindWin conditionLose condition
singleFirst reply wins.n/a
any_ofFirst non-abstain reply wins.n/a
all_ofAll mandatory recipients reply approve.Any mandatory recipient rejects.
m_of_nAt 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:

ValueBehavior
alwaysEvery verdict (approve, reject, abstain) requires a reason.
on_rejectOnly reject and abstain require a reason.
neverReason 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:

  1. Resolve the draft. Iter 1 uses the resolved subject directly (no producer call — the standalone draft step ran upstream). Iter 2+ re-dispatches the redraft action with the prior reviewer feedback in agentContext.review.priorReviewerFeedback.
  2. Apply demotion. Each agent / workflow reviewer in the demoted set flips to mandatory: false.
  3. Check exhaustion. If no mandatory reviewer remains, apply escalation if configured; else terminate as outcome: exhausted.
  4. Run the gate. Dispatch the decision-gate child workflow.
  5. Branch on the verdict.
    • approved / expired_approved → return outcome: approved.
    • expired_rejected → return outcome: expired.
    • cancelled → return outcome: cancelled.
    • rejected with redraft set → bump per-recipient reject counters, demote any that hit agentMaxIterations, loop.
    • rejected without redraft (terminal review) → return outcome: rejected.

The loop boundary uses continueAsNew every 25 iterations to keep runtime history bounded under runaway human-driven loops.


Outputs

FieldTypeDescription
outcomeapproved | rejected | expired | exhausted | cancelledTerminal outcome.
subjectsarrayResolved subjects mirrored from input. Empty for approval-to-proceed gates.
iterationsintegerNumber of iterations actually run (always ≥ 1).
summarystringAggregated 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).
finalDraftobjectThe 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.
decisionTrailarrayPer-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 is outcome: cancelled with the partial decisionTrail.
  • Iteration timeout. Each iteration's iterationTimeoutSeconds controls only the human-gate window for that iteration. Timeout applies the configured onIterationTimeout action; 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. With escalation set, 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.

yaml
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: reject

Multi-deliverable review

One decision spans the spec + the design doc.

yaml
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: reject

m_of_n quorum

Three reviewers, two must approve.

yaml
1policy:
2  kind: m_of_n
3  m: 2
4  abstainTreatment: skip

Required justification on every verdict

yaml
1policy:
2  kind: single
3  requireReason: always

Escalation when reviewers exhaust

yaml
1escalation:
2  recipients:
3    - kind: identity_group
4      target: { groupId: ${{ vars.SECURITY_CHAMPIONS }} }
5      mandatory: true
6  timeoutSeconds: 86400

How it differs from the decision gate

xema/reviewxema/decision-gate
Loop on rejectYes (when redraft is set)No (single inquiry)
subject shapeFree-form (string / array of any expression)Pinned subjectArtifacts (artifactId+versionId+version+hash)
Output shapeoutcome + subjects + summary + finalDraft + decisionTrailoutcome + approvedArtifacts (pinned) + decisionTrail
requireReasonYesNo (always optional)
Reviewer demotion (agentMaxIterations)YesNo
xema/publish-kb integrationRead finalDraft.deliverables from the FINAL APPROVED iterationRead 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