Workflows
ShippingThe unit of serious work: expertise captured as a Nix-built DAG of sandboxed steps, where the hard decisions pass through committees and every run is a record you can replay.
Not every wish deserves a workflow — the orchestrator routes quick things straight to an agent. A workflow is what you reach for when the work is serious: multi-step, reviewable, worth doing the same way twice. It is a directed acyclic graph of steps, defined in Nix, executed by the engine with the same discipline a build system applies to derivations.
Defined in Nix, discovered from flakes Shipping
A workflow is a flake artifact like everything else in Tixim: a Nix file
that assembles steps, edges, instructions, and tools into a fixture the
daemon discovers from its inventory (tix.workflows.<name>). Arguments are
templated into steps at run time — paths, branch names, and prior outputs
flow through filters like slugify and tojson instead of through string
concatenation in prompts. The recipe is code: versioned, diffable,
reviewable, and shared by pinning a flake.
Real recipes get big — our landing workflow is 38 steps and 86 edges in one file — and that is the point: every one of those edges is a decision that no agent has to remember, because the graph remembers it. The recipe documents its own control flow in its header:
Typed at the door Shipping
Workflows take arguments, and arguments have types — strings, lists, enums, and directories that carry their own mount declaration, so the sandbox composition follows from the schema instead of from convention. Defaults can be templated from other arguments. Condensed from the real landing workflow:
mkWorkflowFixture {
name = "land-feature";
description = "Land {{ args['feature-branch'] | join(', ') }} onto {{ args['base-ref'] }} …";
arguments = [
{
name = "source-repo";
type = "directory"; # a mount, not a string: path-var, access,
required = true; # and mapping all declared in the schema
mount = { mapping = "transparent"; path-var = "SOURCE_REPO"; access = "rw"; };
}
{ name = "feature-branch"; type = "list"; required = true; }
{ name = "rebase"; type = "enum"; default = "auto"; } # auto|always|never|managed
{
name = "aggregate-branch";
type = "str";
default-template =
"lf-aggregate/{{ args['base-ref'] | slugify }}/{{ args['feature-branch'] | join('+') | slugify }}";
}
];
}
Like everything in Nix, workflows are fail-fast and check early. A
missing required argument, a value outside its enum, a directory that
doesn’t exist — the submit is rejected before a single container starts.
The checks keep running at that altitude all the way down: every
harness#model pairing is validated at dispatch, and the dispatch step
proves its endgame is reachable at second zero — our landing workflow
verifies it will actually be able to advance the target checkout before
the run, because discovering that at the end would mean the merge “would
fail after the full review panel had run. Fail now.” A wrong argument
should cost milliseconds, not a review panel.
Anatomy of a step Shipping
Steps come in three kinds. Script steps run a program — no model, no tokens, deterministic. Agent steps put a model in a harness with declared tools. Subgraph steps embed a whole child workflow, so recipes compose like functions. All three run inside the same sandbox stack — rootless container, seccomp, a Landlock ruleset that limits reads to the step’s pinned closure — described on the security page. That container is the default runtime, with MicroVM and gVisor step tiers planned for work that warrants a harder boundary.
The engine’s edge semantics carry the control flow that would otherwise
live in prose: branch conditions route around work that isn’t needed,
always-run steps fire for cleanup even when predecessors fail, declared
success exit codes turn “assessed: incomplete” into a routable outcome
rather than a dead run, and a re-verification step can declare that its
success supersedes an earlier failure — so a repaired run resumes instead
of staying latched red.
Concurrency is declared, not hoped for. A step that touches a shared resource takes a lease on it, and the daemon’s allocator queues conflicting runs instead of letting them race:
leases = [{
resource = "git:merge:{{ args['target-repo'] }}:{{ args['base-ref'] }}";
mode = "exclusive";
acquire-timeout-secs = 600;
}];
Agents where judgment lives, capsules where it doesn’t Shipping
Agent steps pick a harness and model per step — a cheap model for mechanical work, an escalation ladder for hard problems — and the pairing is validated at dispatch, so a typo fails in seconds instead of silently running the wrong model for an hour. Tools are declared in Rhai on the host side; the heaviest lesson of our own development is baked into the pattern: git plumbing and file shuffling run as host-side capsules — one-second scripts, zero tokens — so models are spent only where judgment is actually required.
When the stakes justify it, decisions go through committees: an implementer, an independent trainee, reviewers who score the result and return ACK or NACK. Nothing lands on one model’s say-so.
Runs are records Shipping
A run is event-sourced: every step start, output, container lifecycle event, and branch decision is recorded and replayable. Logs carry timestamps, stream, and worker identity as structured fields — never parsed back out of text. Budgets meter tokens and cost per run; the scheduler admits steps against real host capacity; the TUI shows the DAG, per-step provenance (harness, model, duration, exit), and live CPU/memory/token graphs while the run is in flight.
This is what the result DAG of a real landing run looks like — skipped branches where cached results made work unnecessary, worker steps with their harness and model on record, and two failed reviewers sitting next to the respawned one that succeeded:

That evidence trail is what makes the whole loop work: when a run fails, the autopsy is in the record, and the fix lands in the recipe — the story told in Debug the recipe, not the run. Runs also post experiences, so what a workflow learned outlives the run that learned it.