Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

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_skill with 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_tools finds one, describe_tool gives its exact signature, and tool_exec runs it in a script (return await tools.use("<toolbox>", "<tool>", ...)). bash is the engine's own shell, carrying the zero CLI over the same toolboxes and zm for committing. execute runs 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.