Skip to content

Workflow engine

Recipes, runs, and how a workflow turns into ordinary tasks — plus the coding pipeline that runs on top of it, its git handling, and the panel that drives it.

The workflow engine is how a repeatable process — a coding feature, a trip checklist, a recurring chore — becomes concrete tasks with state of their own. It lives in libs/storage/src/workflow.rs. The reader-facing description of what workflows do is in the user guide.

Recipes and runs

A recipe is the blueprint, stored as a graph. A run is one instance of it:

FieldMeaning
id, slugIdentity, and the name the recipe is referred to by.
versionMonotonically increasing. A recipe is immutable and versioned, so a run pins an exact one.
recipe_jsonThe graph itself: the nodes and the edges between them.
Run fieldMeaning
recipe_id, paramsWhich recipe, and this run’s values for its parameters.
statusactive, completed, or cancelled.
step_resultsNode id to result, written as each step finishes. The reserved @notes key holds a coding run’s ordered log of annotations, rejections, approvals, spec saves, and merges.
root_task_idFor a coding run, the feature task it is attached to. Its phase steps materialize as that task’s subtasks.
branch, base_branch, branch_statusThe feature branch, where it was cut from, and where it is in its life.

Versioning is the point of the recipe split: a run records the version it started on, so editing a recipe can never change the meaning of a run already in flight.

Nodes and edges

A recipe is a directed acyclic graph. Each RecipeNode is a step, and each RecipeEdge says when the next one is entered.

Node fieldMeaning
id, kindThe step, and whether it is an action or an event wait.
ai, approvalWhether an agent does the work, and whether a person has to sign it off.
phaseFor coding recipes: interview, spec, implement, review, or merge.
subtaskWhether the step becomes a subtask of the run’s root task rather than a standalone task.
retrigger_on_reject, retrigger_nodeWhether a rejection can reopen an earlier step, and which one if it is not simply the feeder.
Edge fieldMeaning
from, toThe step being left and the step being entered.
condition_typeon_complete, on_result, timer, or event.
condition_valueThe value the condition is matched against, for on_result edges.

How a step becomes a task

There is no separate workflow-task entity. A step is an ordinary task row carrying workflow_run_id and node_id, which is why a workflow step shows up in your list, can be ticked, and needs no special rendering.

  • Lazy — a step's row is created only once its prerequisites are satisfied, so a run does not dump its whole future onto the list.
  • Time waits — a downstream action task is created with blocked_until = completion + duration. The list hides anything more than two days out, so the waiting is invisible until it is nearly actionable rather than a wall of tasks you cannot start.
  • Event waits — a visible, tickable task, resolved by a webhook, a button, or you ticking it. One representation for "something outside has to happen", whatever the source.
  • Coding runs — the phase steps materialize as subtasks of the run's root task, so the feature and its phases stay one unit in the list.

The run lifecycle

Three operations drive a run. complete_workflow_step(task_id, result) marks a step's task done, records its result against the node, and then evaluates the outgoing edges to decide what to materialize next — the engine advances by completing steps, not by a scheduler ticking in the background. fire_workflow_event(run_id, name) resolves an event wait. cancel_workflow_run(run_id) tombstones every task belonging to the run and marks it cancelled.

Cancelling keeps the branch
A cancelled coding run stays visible with its branch name rather than disappearing, so the branch is cleaned up deliberately instead of being orphaned in your repository.

Rejection is the fourth path, and the one that makes review loops work. When an approval step rejects and the node allows it, the engine reopens an earlier step — the feeder, or a specific retrigger_node — and tombstones that step's still-pending children, so the reopened work starts from a clean slate rather than inheriting answers that were just rejected.

The coding pipeline

The coding workflow is a recipe whose nodes carry a phase. There are five:

  • interview
  • spec
  • implement
  • review
  • merge

The task details pane renders them as a stepper. Each phase's state is derived, not stored: Active means it has an open step, Done means every step in it is complete, otherwise Pending. The round number in labels like Round 2 · Implement is counted from the rejections recorded in the run's notes, so going back a round is visible as history rather than a silent reset.

A rejection reopens the interview step, which is what lets a feature be re-specified instead of patched after the fact.

Branches and worktrees

apps/taskstream-desktop/src/coding_git.rs shells out to git. Every function is synchronous and expects to be called from a blocking context, and failures carry git's own stderr so the step that failed can show you why.

OperationDoes
is_repo, changed_pathsWhether a directory is a repository, and which tracked or untracked files differ.
current_branch, branch_exists, switch_branchBranch inspection and checkout.
worktree_addAdds a worktree on a new branch cut from a base. This is where a run’s feature branch is actually created.
worktree_remove, worktree_pruneRemoves a worktree, refusing when it holds uncommitted work; prunes entries whose checkout was deleted by hand.
merge_no_ff, abort_merge, delete_branchMerge with a commit so the feature boundary stays visible, abort one, and delete a branch.
push_branch, stage_and_commitPush a branch and set its upstream; stage everything and commit.
resolve_remote, parse_github_remote, git_common_dir, ensure_excludedFind the remote and its owner/repo, locate the common git directory, and add the worktree path to git’s exclude file.

The feature branch is created by adding a worktree for it, not by switching the checkout you are working in — so a coding run cannot move the branch out from under your own editor. Before that, the worktree's path is written into git's exclude file, so the main checkout never shows the new files as untracked noise.

Removing a worktree refuses when it holds uncommitted changes: you are never silently deleted out of an edit. The branch itself survives removal, which is what keeps it in the panel's cleanup list.

Your credentials, not the API token
Pushing a branch uses your own git credentials. The provider's API token never reaches git, a remote URL, or a credential helper.

branch_status moves a run's branch through its life: proposed by the agent, active once created, then merged, deleted, or abandoned. The statuses exist so a branch can be accounted for after its run is gone.

The loopback tool server

The agent cannot touch the store directly. Instead the app hosts a minimal MCP server on loopback, bound to an ephemeral port on 127.0.0.1 with a per-process bearer token, started on a dedicated thread. It implements only the three methods the coding flow needs: initialize, tools/list, and tools/call.

ToolEffect
attach the specSave the interviewed spec onto the feature task.
create and interview sub-tasksSplit work out of the feature and start a run on it.
log annotationsAppend to the run’s ordered note log.
propose a branch nameRecord the branch the run should use. The app creates it once the spec is approved.
report phase completionMove the run on to the next phase.

Those calls mutate the store and then notify the foreground application to refresh, so the UI keeps up with what the agent is doing mid-phase instead of waiting for the turn to end. On the agent's side, the same tools are reached as MCP tools; the servers you configure yourself are a separate set — see Integrations.

Prompts

Phases are driven by prompts the app composes and drops into the agent pane's prompt box for you to edit before sending; nothing is sent on your behalf. The interview prompt is the substantial one, and it asks the agent to gather context by reading files and searching the codebase, ask clarifying questions over several rounds, write the spec to ./docs/spec/<slug>-spec.md, call save_spec when it is available, and reply with a summary and the path it chose.

A later round of an interview gets told which round it is and why the previous one was rejected, so a re-spec starts from the objection rather than from scratch. The implement prompt is much shorter — implement the approved spec — because by then the spec carries the detail.

When a feature has open subtasks, the interview prompt requires a decision on every one of them: either give it its own spec, or mark it explicitly covered by the feature's. Leaving one undecided is not allowed, which is what stops a feature from quietly shipping with unexamined sub-work.

The automations panel

AutomationsPanel is the UI over all of this, and it replaced the older workflows page rather than sitting beside it. It lists the available recipes as automations, lists active runs grouped by the recipe they came from, and — for an active run — presents the steps that need a decision, with the buttons the step's outgoing edges imply: approve or reject where the edge is on_result, complete where it is on_complete.

For a coding run it shows the current phase and gives you the branch housekeeping, including deleting a branch. It refreshes from the store rather than holding its own copy, and emits a changed event when a run moves or ownership changes, so the task list and the tag tree update too.

The data model covers the tables these runs are stored in, and the agent runtime covers the other side of the loopback server.

Edit this page on GitHub