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"
  outputSchema: @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.

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"
    outputSchema: @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"
    outputSchema: @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"
    outputSchema: @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