Reviews
Add human-in-the-loop approval steps to your workflows.
What it is. A gate that waits for a person. The run pauses at the node until someone approves, rejects, or fills in a form.
Use it when a human must sign off before the system acts: sending the email, posting the refund, publishing the page.
Works with workflows (review is a config on any node) and context.reviews (the response downstream).
A review-enabled node is a gate, not a step. When the run reaches it, the node's own work does not execute. The run pauses, a review appears in the project Inbox, and a reviewer responds:
- Approve resumes the run. The reviewer's submitted form data is recorded as the node's output and exposed to downstream nodes at
context.reviews.<nodeName>. - Reject fails the run with the rejection reason.
- No response within the review timeout fails the run. The default timeout is 7 days.
Because the review replaces the node's own execution, do the work in an upstream node and make the reviewed node a dedicated gate.
Enabling review on a node
Add review: true for simple approval, or use a full review config block:
node review_gate {
type: code
label: "Review before sending"
code: @ts {
return context.nodes.draft.output
}
review: {
enabled: true
title: "Review before sending"
description: "Approve the draft or request changes."
schema: @json {
{
"type": "object",
"required": ["approved"],
"properties": {
"approved": { "type": "boolean", "title": "Approve" },
"feedback": { "type": "string", "title": "Feedback" }
},
"additionalProperties": false
}
}
}
}The code: block here is a placeholder; it never runs. On approval the reviewer's form data ({ approved, feedback }) becomes the node's recorded output.
Review config fields
| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | No | Defaults to true when the review block is present. |
title | string | No | Title shown to the reviewer. |
description | string | No | Description shown to the reviewer. |
schema | @json { } block | No | JSON Schema for the review form. Validates the submitted form data and shapes context.reviews.<nodeName>. |
content | string | No | Additional content shown to the reviewer. |
actions | array | No | Custom buttons; each item has id, label, and outcome ("approve" | "reject"). |
Approve, reject, and timeout
- Approve accepts optional form data. When the review declares a
schema, the submitted data is validated against it; invalid data is rejected and the review stays pending. On approval the run resumes with the form data as the node's output. - Reject fails the run with the rejection reason. There is no reject-and-continue. To model "request changes", give the action an
approveoutcome, capture the decision in a schema field, and route downstream with a switch node. - Timeout. A pending review does not wait forever. If nobody responds within the review timeout (7 days by default), the run fails with
Review timed out.
A review can be answered once. After approval or rejection, it cannot be reopened.
Reviewers respond in the Cloud UI project Inbox, or programmatically through the SDK (swirls.client.reviews): list pending reviews, fetch one with its execution context, approve with form data, or reject with a reason.
Custom actions
When you need labeled actions beyond the default approve/reject flow, define actions. Remember that any reject outcome fails the run, so every path that should keep the run alive needs an approve outcome. Pair the actions with a review schema field (for example decision) and route in a switch node.
node editorial_review {
type: code
label: "Editorial review"
code: @ts {
return context.nodes.draft.output
}
review: {
enabled: true
title: "Editorial review"
schema: @json {
{
"type": "object",
"required": ["decision"],
"properties": {
"decision": { "type": "string", "enum": ["approve", "revise"] },
"editor_notes": { "type": "string" }
},
"additionalProperties": false
}
}
actions: [
{ id: "approve", label: "Approve & Publish", outcome: "approve" },
{ id: "revise", label: "Request Revisions", outcome: "approve" },
{ id: "reject", label: "Reject", outcome: "reject" }
]
}
}
node route_decision {
type: switch
label: "Route"
cases: ["publish", "request_revisions"]
router: @ts {
const decision = context.reviews.editorial_review?.decision
return decision === "approve" ? "publish" : "request_revisions"
}
}"Request Revisions" carries an approve outcome so the run continues into the switch. The "Reject" action ends the run with the rejection reason.
Accessing review data
In @ts { } blocks, use context.reviews.<nodeName> to access the reviewer's response:
@ts {
// In any node downstream of the gate
const approved = context.reviews.review_gate?.approved
const feedback = context.reviews.review_gate?.feedback ?? ""
}The shape follows the review schema, so use optional chaining. The LSP types context.reviews from the review schema, so you get autocomplete. See Context and Type Safety for details.
Full example: draft, review, route
This workflow drafts an email with an LLM, pauses at a gate node for human review, then routes to send or revise based on the reviewer's response. The draft happens upstream in draft; review_draft is a pure gate whose output is the reviewer's form data.
workflow email_with_review {
label: "Email with Review"
root {
type: code
label: "Entry"
inputSchema: @json {
{
"type": "object",
"required": ["name", "email", "topic"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"topic": { "type": "string" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["name", "email", "topic"],
"properties": {
"name": { "type": "string" },
"email": { "type": "string" },
"topic": { "type": "string" }
},
"additionalProperties": false
}
}
code: @ts {
const { name, email, topic } = context.nodes.root.input
return { name: name?.trim() ?? "", email: email?.trim() ?? "", topic: topic?.trim() ?? "" }
}
}
node draft {
type: ai
kind: object
label: "Draft email"
schema: @json {
{
"type": "object",
"required": ["subject", "body"],
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" }
},
"additionalProperties": false
}
}
model: "google/gemini-2.5-flash"
prompt: @ts {
return `Draft a professional email to ${context.nodes.root.output.name} about: ${context.nodes.root.output.topic}`
}
}
node review_draft {
type: code
label: "Review draft"
code: @ts { return context.nodes.draft.output }
review: {
enabled: true
title: "Review email"
description: "Approve or request changes."
schema: @json {
{
"type": "object",
"required": ["approved"],
"properties": {
"approved": { "type": "boolean", "title": "Send as-is" },
"feedback": { "type": "string", "title": "Changes requested" }
},
"additionalProperties": false
}
}
}
}
node route {
type: switch
label: "Route"
cases: ["send", "revise"]
router: @ts {
return context.reviews.review_draft?.approved ? "send" : "revise"
}
}
node send {
type: email
label: "Send"
from: @ts { return "[email protected]" }
to: @ts { return context.nodes.root.output.email }
subject: @ts { return context.nodes.draft.output.subject }
text: @ts { return context.nodes.draft.output.body }
}
node revise {
type: ai
kind: object
label: "Revise"
schema: @json {
{
"type": "object",
"required": ["subject", "body"],
"properties": {
"subject": { "type": "string" },
"body": { "type": "string" }
},
"additionalProperties": false
}
}
model: "google/gemini-2.5-flash"
prompt: @ts {
const draft = context.nodes.draft.output
const feedback = context.reviews.review_draft?.feedback ?? ""
return `Revise this email. Subject: ${draft.subject}. Body: ${draft.body}. Feedback: ${feedback}`
}
}
node send_revised {
type: email
label: "Send revised"
from: @ts { return "[email protected]" }
to: @ts { return context.nodes.root.output.email }
subject: @ts { return context.nodes.revise.output.subject }
text: @ts { return context.nodes.revise.output.body }
}
flow {
root -> draft
draft -> review_draft
review_draft -> route
route -["send"]-> send
route -["revise"]-> revise
revise -> send_revised
}
}Further reading
- Context and Type Safety: how
context.reviewsis typed from the review schema - Node types: switch node configuration