Workflows
An agent in chat decides what happens next, every turn. That is the point of chat, and it is exactly wrong for the other half of real work: the release chore, the review pass, the data refresh you want to run the same way every time, reviewable before it runs and explainable after. A keelson workflow takes the control flow away from the agent and gives it to a YAML file. The file fixes what runs, in what order, under what conditions. Inside that fixed frame, individual nodes can still do agent work, with the leash declared in the same file.
What that file buys you, and what it costs, plays out below: how the graph runs, how data crosses between nodes, what a run leaves behind, and where the agency is bounded.
The graph is the contract
Section titled “The graph is the contract”A workflow is a set of nodes and depends_on edges, a directed acyclic
graph. The executor starts every node with no unmet dependencies at once, and
starts each remaining node the moment its own dependencies settle, so a slow
branch never holds up an independent one. A workflow can set
scheduling: layered to wait for whole topological layers instead. The
first-workflow tutorial walks this shape
end to end, with two independent nodes starting together.
Whether a node runs at all is decided by two declarations on the node, both evaluated against upstream results:
when:is a data gate: a condition over upstream output, like$check.output.status == 'ok', with==,!=, and numeric comparisons, joined by&&/||. A false gate skips the node.trigger_rule:is a dependency-merge rule: what pattern of upstream outcomes lets the node fire.
trigger_rule | The node runs when |
|---|---|
all_success (default) | Every dependency succeeded. |
one_success | At least one dependency succeeded, so a branch can rescue a failing sibling. |
none_failed_min_one_success | Nothing failed and at least one succeeded. |
all_done | Every dependency reached a terminal state, for collector nodes that summarize whatever happened. |
The details fail closed. A depends_on reference the validator somehow never
saw counts as failed, a malformed when: expression warns and skips its node,
and a node that declares an output_schema fails rather than passing a
mismatched payload downstream.
Three families of nodes
Section titled “Three families of nodes”Seven node kinds, three jobs:
| Family | Kinds | Character |
|---|---|---|
| Agent | prompt, command, loop | Run agent turns. Take model, provider, and tool gates. command runs a named, reusable prompt file from the home; loop repeats a prompt until a completion token appears, hard-bounded by max_iterations. |
| Deterministic | bash, script | Run code with no model in the path: bash for shell, script for a declared runtime such as Bun. Timeouts and exit codes decide success. |
| Control | approval, cancel | Shape the run itself. approval pauses for a human decision; cancel ends a branch deliberately, recording why. |
The taxonomy is the determinism dial. A workflow of bash and script nodes
is a build system. A workflow of prompt nodes with gates between them is a
supervised agent. Most useful workflows sit in the middle: deterministic
nodes establish facts, agent nodes do judgment work, and gates check the
judgment before anything irreversible happens.
How data moves
Section titled “How data moves”Upstream output reaches a downstream node through one of two channels, and the split is deliberate.
The workflow layer substitutes text. In prompts, when: conditions, and
cancel reasons, $nodeId.output expands to the upstream node’s output, and
$nodeId.output.field reaches into JSON output. In prompt and cancel bodies,
workflow inputs arrive the same way, as $inputs.<name> and $ARGUMENTS. A
when: condition resolves only $nodeId.output refs, and the loader rejects
$inputs.*, $ARGUMENTS, or $ARTIFACTS_DIR inside one outright: encode the
input through a producer node and compare its output. Substitution is a single
atomic pass: if an upstream output happens to contain $something.output,
the marker stays literal text instead of expanding again.
Shell nodes read the environment instead. A bash or script node runs
its body exactly as written in the file, with upstream output delivered as
KEELSON_NODE_<id>_OUTPUT environment variables. Nothing produced by a model
is ever spliced into shell source, so upstream text containing $(...) or
backticks is data, never code.
A run is a record
Section titled “A run is a record”Starting a workflow creates a run, and the run is durable: a row for the run,
a row per node with its output, timing, error, and the provider and model that
node actually ran on, all in the harness’s SQLite store. That last pair makes an
otherwise-unobservable auto node’s resolved served model visible in the run
detail whenever the provider reports it. The run also records a definitionHash,
the sha256 of the parsed workflow definition it executed, so an evaluation score
or a cost figure can be pinned to one exact version of that definition. Edit the
YAML in any way that changes a node, even whitespace inside an inline prompt, and
the next run carries a different hash; a resumed run is re-stamped with whatever
definition the resume ran. Content a node reads at execution time, such as the
Markdown file behind a command node, is outside the hash. From those per-node
timestamps, keelson workflow status and the run header in the browser derive a
critical-path figure: the longest dependency chain by node duration, shown
beside the wall clock, so the gap between the two reads as scheduling wait
rather than node work. The watch stream you see in the CLI and the browser is the
same event feed, emitted as each node starts, streams, and settles. The figure
shows every state a run can occupy:
Figure 1. The run lifecycle. Approval nodes are the only path into
paused, and the dashed edge is the honest one: a pause does
not survive a server restart.
The dashed edge is the honest one. An approval pause is held by the
running server process, not by the database. If the server restarts while a
run is paused or mid-flight, boot reconciliation finds the stale run and
marks it failed rather than pretending it can continue. Runs are durable as
records; failed and cancelled runs are resumable: the harness re-enters the
executor from the first incomplete node, seeding completed node outputs so
finished work is not repeated. A node marked always_run is the exception, it
re-runs on resume even though it succeeded, so a gate or validation re-checks
rather than replaying a stale pass.
Where the agency is bounded
Section titled “Where the agency is bounded”Every agent node runs inside limits that are declared, not improvised, and they layer from the file up to the operator:
- The file picks the provider and model per node, gates tools with
allowed_toolsanddenied_tools, bounds loops withmax_iterations, bounds stalls withidle_timeout, and pins output shape withoutput_schema. - The operator floor sits above the file.
KEELSON_WORKFLOW_PROVIDERpins which provider every prompt node uses, andKEELSON_WORKFLOW_TOOL_DENYLISTsubtracts tools no workflow may use, applied on top of whatever a node allows. - The human gate is a node. An
approvalnode holds the run before the irreversible step, and the reply becomes that node’s output, so downstream nodes can branch on what the human said.
Provider support for the finer controls varies, and the loader is honest
about it at load time: fields only one provider honors, like per-node
hooks, are flagged as warnings rather than silently accepted or rejected.
Where workflows come from
Section titled “Where workflows come from”The catalog draws from three layers, lowest precedence first: the bundled
starters that ship with keelson (read-only), your global
<home>/workflows/, and a project’s .keelson/workflows/. A later layer
shadows an earlier one by name, and installed ribs contribute their own at
activation, either built in code through the rib contract or shipped as plain
YAML files in the rib package’s workflows/ folder. On first run the bundled
starters are also copied into your home, so
they list as global once seeded, while the bundled layer keeps them
discoverable in a fresh checkout. The Workflows catalog
documents the ones that ship. A rib-contributed workflow can additionally bind its
run output to one of the rib’s snapshot keys, which is how a browser board’s
refresh button becomes a workflow run; that pipeline is the subject of
Snapshots and surfaces.
Where to go next
Section titled “Where to go next”- Run and author a workflow builds a four-node DAG and watches it execute.
- Projects and worktrees is where a run’s working directory comes from, and how an isolated run branches off your checkout.
- Memory and state covers what runs leave behind, and the memory hooks nodes can declare.
- The workflow node reference is the full node-by-node schema, and Authoring workflows has the control-flow recipes.