Skip to content

Output Contract & Built-in Verbs

The output: block is typed and it is both documentation and a runtime check — values passed to return: must match. Anything in the final payload not declared in output: is dropped on the way out.

output:
total: !int
rows: !obj-list
summary: !obj
steps:
- return:
total: "${= len(products.rows)}"
rows: "${products.rows}"
summary: { fetched-at: "${= str(_invocation-id)}" }

The contract is what makes a flow callable from REST, MCP, the CLI, and other flows with the same guarantees — every caller knows the shape it gets back.

In the simple form, top-level keys map directly to declared output fields. The full output: step form lets you set a MIME type, attach a cache policy, or redirect:

# Presigned-URL handoff
- return:
redirect: "${upload.url}"
status: 302

If you declare an output: field that no return: populates, the runtime contract check fails the flow at the end. Either populate the field or remove it from the contract.

Three verbs you’ll use constantly that aren’t operators:

# Log a line at info level
- log: "Loaded ${= len(items)} items from ${source}"
# Single variable assignment (mutable var or persist key)
- $counter: "${= counter + 1}"
# Batch assignment — multiple keys in one step, committed atomically
- set:
cursor: "${products.last-id}"
last-sync: "${= str(_invocation-id)}"
seen-count: "${= seen-count + len(products.rows)}"
  • log: writes to the invocation’s log stream, readable live via zen logs <invocation-id> or after the fact via REST.
  • $var: assigns a single vars: or persist: key.
  • set: is preferred over multiple $var: lines when updating several variables at once because it commits them atomically.

The other terminating verb is halt: — which stops without producing output, in contrast to return:.