arc: flows
A flow is one named bundle of the settings a task's route needs: which worker runs its attempts, which gate profile verifies the result, how many of its tasks may run at once, and its defaults for allow_noop and commit_dirty.
It is not a new worker kind and not a new state machine: every flow's worker is the radian worker (radian acp) with its own arguments, one program of its own, or an ACP agent (the worker), and the claim, the delivery check, the gate, the merge and the ledger are the same for every flow.
One repository can hold several flows, so coding, image and writing tasks can share one plan: a plan knows nothing about flows, and after may join tasks of different flows.
The authority for every key and field is config.toml and the task fields; this page says what a flow is for and shows a complete setup.
A complete example: code and image#
A repository with a code flow for its source tree and an image flow for its artwork. code runs the radian worker and gates with a commands profile; image runs its own command worker and gates with review + human. The repository runs two attempts at once, one per flow:
schema_version = 1
[limits]
concurrency = 2
[gate]
profile = "code-check"
[gate.profiles.code-check]
stages = [{ stage = "commands", required = true }]
commands = ["cargo nextest run --workspace"]
[gate.profiles.image-review]
stages = [{ stage = "review", required = true }, { stage = "human", required = true }]
[flows.code]
concurrency = 1
gate_profile = "code-check"
exec_args = ["--model", "coding-model"]
[flows.image]
concurrency = 1
gate_profile = "image-review"
command = ["/usr/local/bin/image-worker", "--assets", "art"]
commit_dirty = true
codestarts the radian worker with--model coding-modelon every attempt, and its gate runs thecode-checkprofile: onecommandsstage on the gate's own checkout.imagestarts/usr/local/bin/image-workerinstead of the radian worker, and its gate runsimage-review: areviewsession, then the task stops atreview.awaiting_humanuntil you merge or reject it. The review uses the reviewerimage-reviewnames; a profile without areviewerkey uses thedefaultreviewer, which is what[gate.review]configures when the file has no[gate.reviewers.default]section. A command worker receives the brief on standard input and writes itsResultto standard output; another agent as the worker gives the contract. Itscommit_dirty = truelets the wrapper commit what the worker leaves uncommitted, so the worker may write its files and print acompletedResultwithout running git itself.[limits] concurrency = 2lets two attempts run in the repository, and each flow'sconcurrency = 1lets onecodeand oneimageattempt run at the same time.
Every key's type, default, allowed values and effect time is in the flows.<name>.* rows.
The worker's session, brief and checks are in the worker, the stages in the gate, and the claim and the queue in the scheduling loop.
A flow can run an ACP agent instead of the radian worker or its own command: set acp to the program and its arguments, and the wrapper becomes the ACP client (see An ACP agent as the worker).
With the Codex adapter installed globally, the image flow above reads as one line:
[flows.image]
concurrency = 1
gate_profile = "image-review"
acp = ["codex-acp"]
commit_dirty = true
A flow sets one worker line: acp, command or a non-empty exec_args, never more than one, and acp cannot be combined with [worker] rework_session = "resume".
How a task picks its flow#
A task's flow field names one [flows.<name>] section; a task whose file has no flow field runs default.
radian arc add task --flow <name> and the MCP add_task tool set it when the task is created (see add task).
The field may change only before the task's first attempt; after that, a different worker is a new task with supersedes.
radian arc list and the arc TUI show a task's flow only when the repository defines a flow other than default; a repository whose config.toml has no such flow prints no flow column, and the task page always names it (flow default when the file has none).
A flow the file does not define is not a silent fallback: the claim of a ready task refuses and names the flow (task \<task>` uses flow `<flow>`, which `config.toml` does not define), so radian arc startexits3and the task staystodo.ready. radian arc run's summary repeats that line for a ready task, and so does the queue line radian arc confirm --dry-runprints for a proposed one.radian arc list's task line, radian arc show's queue_reason` and the arc TUI print no reason for it: they name a slot only when a full limit holds the task, and a missing flow is not a limit (see the queue reason).
The flow's settings reach the attempt at different moments, each stated in its own row:
- the worker (
acp,commandorexec_args) is chosen at the claim and recorded in the attempt'sstart.json, so aconfig.tomlchange takes effect at the next claim; - the gate's profile is resolved when the gate is planned, and the attempt's
profile.jsonrecords the profile the verification ran under, so a later merge is approved and integrated by that profile; allow_noopis read at the claim of each attempt, when the delivery check runs;commit_dirtyis read at the same claim: when it istrue, acompletedworker's uncommitted or untracked changes are committed on the branch before that attempt's delivery check.
The two concurrency limits#
Two limits apply to every claim, and both must have room:
| Limit | Row | What it counts |
|---|---|---|
[limits] concurrency |
limits.concurrency |
every task in doing in the repository, whatever its flow. |
[flows.<name>] concurrency |
flows.<name>.concurrency |
the tasks in doing of that one flow. |
With the example above, one code and one image attempt run together, and a third task of either flow waits; two code tasks may never run at once, even though the repository total would allow them.
A ready task whose flow is full is skipped, so it never holds up another flow: the same pass claims a ready task of a flow that still has room further down the claim order.
A running gate takes no slot of either kind.
When a limit holds a ready task, the queue line names the limit that is full: waiting for a free <flow> slot (for example waiting for a free image slot) when the flow is full, else waiting for a free concurrency slot when the repository total is full; when both are full, the flow's reason wins.
The same words are radian arc run's summary, the task line radian arc list prints, radian arc show's queue_reason field, and the reason the arc TUI draws on the task's row (see the queue reason and the TUI).
A file with no [flows] table has no per-flow limit at all: its default-flow tasks count against limits.concurrency alone, exactly as before flows existed.
A file that writes any [flows] section, the implicit default flow included, holds every flow to its own concurrency.
Binary deliverables#
A flow whose work is images, audio or any other binary file commits those files as ordinary git commits. The delivery check is unchanged: the attempt must end on a commit that descends from the tip it started on, with a clean tree, and the change is what that commit range contains (the checks after the worker exits). Git LFS and reference-only deliverables — a commit that names a path or a hash in external storage instead of the bytes — are not supported; arc would need to learn the pointer format and the external store first, and no repository needs that yet.
The five questions#
Each flow's entry answers one question for itself, and the other four have one answer for every flow:
- What starts its tasks? This is the flow's own answer. In this build every flow starts the same way — from
afterandauto: the scheduling pass claims atodo.readytask when every task in itsafterlist isdone.mergedand its plan'sautolevel allows a start (runormerge), or you start it by hand withradian arc start. There is no clock and no external event yet, and a flow cannot make itself start:autobelongs to the plan. A flow would answer this question differently only once a trigger exists. - What does it read and write? Its worker's worktree for the task and the branch
work/<task>. The worktree is made from the task'sbase_shaat the first claim and reused by later attempts, where the profile'sbefore_attemptsteps run first (amerge-mainentry merges the newest main into it), so the worker reads that worktree as it is and writes only inside it, then pushes only its own branch (the session and the brief, the.radian/arc/layout). - What follows it? The gate, and then
review.awaiting_human: the task's flow decides the profile's stages and the profile'sreviewer, but the transition is the same for every flow (the gate). A failure goes toreview.rework_pending, and the merge atauto mergefollows the same path. - Where does a person come back in? At the gate's
humanstage, which every flow either lists in its profile or does not, and then atreview.awaiting_human: you merge the task withradian arc merge, reject it with a note withradian arc reject, or end it withradian arc cancel. - What stops two attempts doing the same work? The claim: one transaction under
state.lockthat moves the task out oftodo.readyand writes a freshtoken, and a result is applied only when its token matches the task's current attempt. A second pass that reads the same task finds it claimed and starts nothing.