SWIRLS_
Language

Reviews

Add human-in-the-loop approval steps to your workflows.

Reviews pause graph execution at a node until a human approves it. Reviewers can see the node's output, fill out a form, and approve or reject. The review response is then available to downstream nodes via context.reviews.

Enabling review on a node

Add review: true for simple approval, or use a full review config block:

node check_output {
  type: code
  label: "Check output"
  schema: @json {
    {
      "type": "object",
      "required": ["draft"],
      "properties": { "draft": { "type": "string" } },
      "additionalProperties": false
    }
  }
  code: @ts {
    return { draft: "Generated content here..." }
  }
  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
      }
    }
  }
}

Review config fields

FieldTypeRequiredDescription
enabledbooleanYesEnable or disable the review step.
titlestringNoTitle shown to the reviewer.
descriptionstringNoDescription shown to the reviewer.
schema@json { } blockNoJSON Schema for the review form.
contentstringNoAdditional content shown to the reviewer.
actionsarrayNoCustom buttons; each item has id, label, and outcome ("approve" | "reject").

Custom actions

When you need labeled actions beyond the default approve/reject flow, define actions. Pair them with a review schema that includes a decision field (or similar) if you route in a switch node.

node editorial_review {
  type: code
  label: "Editorial review"
  schema: @json {
    {
      "type": "object",
      "required": ["title", "summary"],
      "properties": {
        "title": { "type": "string" },
        "summary": { "type": "string" }
      }
    }
  }
  code: @ts {
    return { title: context.nodes.root.output.title, summary: "..." }
  }
  review: {
    enabled: true
    title: "Editorial review"
    schema: @json {
      {
        "type": "object",
        "required": ["decision"],
        "properties": {
          "decision": { "type": "string", "enum": ["approve", "revise", "reject"] },
          "editor_notes": { "type": "string" }
        },
        "additionalProperties": false
      }
    }
    actions: [
      { id: "approve", label: "Approve & Publish", outcome: "approve" },
      { id: "revise", label: "Request Revisions", outcome: "reject" },
      { id: "reject", label: "Reject", outcome: "reject" }
    ]
  }
}

node route_decision {
  type: switch
  label: "Route"
  cases: ["publish", "request_revisions", "rejected"]
  router: @ts {
    const decision = context.reviews.editorial_review?.decision
    if (decision === "approve") return "publish"
    if (decision === "revise") return "request_revisions"
    return "rejected"
  }
}

Accessing review data

In @ts { } blocks, use context.reviews.<nodeName> to access the reviewer's response:

@ts {
  // In a downstream node or the reviewed node itself
  const approved = context.reviews.check_output?.approved
  const feedback = context.reviews.check_output?.feedback ?? ""
}

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 graph drafts an email with an LLM, pauses for human review, then routes to send or revise based on the reviewer's response.

graph 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: "gpt-4o-mini"
    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"
    schema: @json {
      {
        "type": "object",
        "required": ["subject", "body"],
        "properties": {
          "subject": { "type": "string" },
          "body": { "type": "string" }
        },
        "additionalProperties": false
      }
    }
    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: "gpt-4o-mini"
    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

On this page