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
    prompt: |
      Summarize this morning's Slack into a single brief.

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

        $draft-summary.output

  - id: post-to-slack
    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
    depends_on: [draft-summary]
    approval:
      message: |
        Here's the morning brief. Approve to post it.
      on_reject:
        prompt: |
          Revise the draft based on the rejection reason: $REJECTION_REASON
        max_attempts: 2

The on_reject.prompt runs with the rejection reason in $REJECTION_REASON, so the rewrite can address what was wrong.

yaml
  - id: draft-summary
    prompt: |
      Summarize this morning's Slack.
      The previous draft was rejected. 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
    approval:
      capture_response: true
      message: |
        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.