Appearance
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.outputWhen 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: 2The 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_REASONCapturing 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.