Builtin Library
README.md
workflow
Run a workflow, and answer what it asks.
A workflow is a program that holds the shape of a job: its phases, its branches, what runs at the same time as what, and what each step is given. It executes in the engine and it decides all of that itself. You do not drive it — you answer the questions it stops to ask, and it carries on.
That inversion is the point. A job with eight steps written as instructions becomes eight things an agent has to remember at once, and by the last one the first is gone. Written as a workflow, the shape lives in the program and no agent has to hold it.
The loop
workflow.start begin a run — returns immediately, the run is already going
workflow.next take the next request it is waiting on, claimed as you read it
workflow.renew keep a claim while its work goes on
workflow.answer hand back a result; the run resumes from where it parked
workflow.correct replace an answer the run has not read yet
workflow.fail say why a request cannot be answered
workflow.status what every run is doing, waiting on, and returned
A request that carries a schema takes only an answer of that shape:
workflow.answer refuses any other, naming the field and the shape it needs,
and the request stays yours to answer again. Give the schema to the subagent
with the prompt.
An answer is final once the run has read it, which it does within a moment
of it being given. workflow.correct replaces an answer the run has not read
yet, held to the same schema; once the run has read it, the call refuses and
says the answer was already consumed.
workflow.answer, workflow.correct, workflow.fail and workflow.renew
each refuse a request they cannot take by naming it and its state: an id no
run holds (with a pointer at workflow.status), an answer already given (and
whether the run has read it), a run that has ended, a request that failed or
was cancelled. The shell exits non-zero on a refusal.
A request is announced on your tool responses once, when the run asks for it,
and once more if a claim on it runs out. It then waits for whoever asks for it:
workflow.status lists every request still waiting, and workflow.next hands
out the next one, so when you finish one request, take the next. A request you
have claimed is yours to work on in peace: your own calls keep it, and you are
told once when it is about to run out, and once if it does. A run that ends
says so once: a failed run names why, and every request it was still waiting
on is ended with it, so nothing is left to take or answer.
Every request is a subagent's job
The prompt workflow.next hands over is written to be one agent's entire
task. It comes as a file, prompt_path, so a claim stays small however long
the job is: give a fresh subagent that file as its task, and answer with what
that subagent returns.
Doing the work yourself instead defeats the workflow: the whole run's context — every phase, every result, every prompt — ends up in one conversation, which is the exact failure a workflow exists to prevent. Your context should hold the prompts you dispatched and the results you got back, and nothing else.
When several requests are waiting at once, the workflow opened them together because they do not depend on each other. Dispatch them together.
Tell the subagent how to find things
A request says what to do, not how to work here. Put that in the task you dispatch:
- Open the skill for the job first.
agent_skillwith no argument lists what this world knows how to do. Skills advertise themselves on every tool response — one that covers the job is a job somebody already worked out, and an agent that builds around it produces something the engine never loads. - Then the tools.
search_toolsfinds one,describe_toolgives its exact signature, andtool_execruns it in a script (return await tools.use("<toolbox>", "<tool>", ...)).bashis the engine's own shell, carrying thezeroCLI over the same toolboxes andzmfor committing.executeruns Luau for what no tool covers. - What it builds is content the world loads, not a script somebody runs by hand. A file that only does something when executed manually is a note about the world rather than part of it.
Every one of those was learned by leaving it out: subagents given a plausible-sounding but wrong discovery flow wrote loose scripts, never opened the skill describing their exact task, and never had the shell.
The subagents share one engine
Every subagent works in the same engine, the same world, the same Luau VM and the same viewport as the others and as you. Nothing about running in parallel gives any of them a private copy.
So each request owns its own files and writes nobody else's. None of them stops or restarts the engine, clears the scene, or deletes what it did not create — the run is not over when one step is, and the next step inherits whatever the last one left. Anything a step starts in the background it also stops before it returns, because a leftover driver loop makes the next step's measurements wrong.
A run outlives whoever started it, and the engine
Runs live in the engine, not in a session. Close the tab, lose the connection,
hand over to somebody else: the run is still parked exactly where it was, and
whoever asks workflow.next gets the request that was waiting. Each run keeps
its record under /zero/source/local/workflow_runs/, which this machine keeps
for the world and the user, so when the engine itself restarts on the world the
run comes back in the phase it had reached, with every answer it was given and
every claim under the same request id, and carries on. workflow.status
counts how many times an engine read it back as restored.
A claim is a lease. workflow.next states how long it holds (lease_seconds,
and expires_in for what is left), counted from the last tool call the session
that claimed it makes on the engine: a session working through one long turn
keeps its claims by working, whatever tools the work calls. workflow.renew
starts the lease over before a stretch with no engine calls, and every
workflow.next you make renews the claims you already hold. A claim whose
session makes no call for the length of the lease returns to the queue, so a
runner that disappears mid-request costs that request and not the run; its
holder takes it back with workflow.renew while nobody else has taken it.