Skip to content

Approval steps

Some steps shouldn't fire without a human glance first. Posting to a public channel. Sending an email to a customer. Spending money. An approval step pauses the workflow, asks for a yes-or-no, and continues based on the answer.

The prompt shows up wherever you happen to be watching the run, your terminal, the Web UI, Slack, Telegram, or your agent's chat surface, so you can approve from whichever surface is closest to hand. Reject the step and you can choose what happens next: fail the whole workflow, or send the rejection back with your reasoning so the previous step takes another swing.

When to use approvals vs. just trust the model

Use approvals when the next step is a one-way door (sending email, posting publicly, executing a transaction). Don't use them on every step; a workflow that asks for human approval six times is a workflow you'll stop using by Friday.

A useful rule: if the model can recover from being wrong by trying again, no approval needed. If the model being wrong means a customer sees a bad message, gate it.

Advanced: the YAML schema and run behavior

The shape

yaml
nodes:
  - id: draft-summary
    type: prompt
    prompt: |
      Summarize this morning's Slack into a single brief.

  - id: review-summary
    type: approval
    depends_on: [draft-summary]
    prompt: |
      Here's the morning brief. Approve to post it to #leadership.

      $draft-summary.output

  - id: post-to-slack
    type: prompt
    depends_on: [review-summary]
    prompt: |
      Post the approved brief to #leadership: $draft-summary.output

When the workflow hits review-summary, it stops. The prompt text shows up wherever the workflow is being watched (your terminal, the Web UI's run detail, your agent's chat surface). You reply approve or reject. The workflow resumes accordingly.

On rejection

By default, a rejected approval node fails the whole workflow. You can override with on_reject::

yaml
  - id: review-summary
    type: approval
    depends_on: [draft-summary]
    on_reject:
      action: rework
      max_retries: 2
    prompt: |
      Here's the morning brief. Approve to post it.

The rework action sends the rejection back to the upstream node with the rejection reason in $REJECTION_REASON, so the rewrite can address what was wrong.

yaml
  - id: draft-summary
    type: prompt
    prompt: |
      Summarize this morning's Slack.
      {{#REJECTION_REASON}}
      The previous draft was rejected. Reason: $REJECTION_REASON
      {{/REJECTION_REASON}}

Capturing the reply

If you want the human's reply to feed into downstream work (not just yes/no), set capture_response: true:

yaml
  - id: review-summary
    type: approval
    capture_response: true
    prompt: |
      Here's the brief. Approve to post, or reply with edits.

Whatever the reviewer typed lands in $review-summary.output.

Where the prompt shows up

The approval prompt is delivered through whatever surface is watching the run.

  • Terminal. Z.E.N. prints the prompt and waits for approve / reject.
  • Web UI. The run detail page shows the prompt with approve and reject buttons.
  • Slack / Telegram. If the workflow was dispatched from there, the prompt arrives as a message; reply approve or reject.
  • Agent dispatch. Your agent gets the prompt as part of the streaming response and can either ask you or decide on its own.

AI that follows a recipe, not a conversation.