Architecture /docs/workflow-engine

Workflow Engine

Yoke (Yoke) — Workflow Engine

The Workflow Engine runs named, customizable, multi-step processes on top of the ordinary agent loop -- branch, describe, split, execute (in parallel when possible), require tests + docs, lint, and commit -- with the entire run (every prompt, model response, user confirmation, and command executed) persisted under .yoke/workflows/ .

It ships with one built-in workflow, elixir , covering the seven steps most Elixir feature work needs, and is designed so you can define your own from scratch or by editing a copy of elixir .


Quick start

# See what's available (built-in + anything you've already defined)
/workflow list

# Run the bundled "elixir" workflow for a task
/workflow run elixir Add JWT-based session refresh to the auth module

# Check on a run later (or after a crash / Ctrl+C)
/workflow status
/workflow status elixir-1730000000-ab12cd

# Continue a run that failed or was interrupted
/workflow resume elixir-1730000000-ab12cd

# Give up on a run for good
/workflow abort elixir-1730000000-ab12cd

# Scaffold your own workflow, starting from the elixir template
/workflow init my-team-flow --from elixir

The model can also trigger a workflow itself mid-conversation via the run_workflow tool, if you explicitly ask it to ("run the elixir workflow for this").


The elixir workflow, step by step

Step type What it does



① branch Creates and checks out yoke/elixir/ off the current branch. If the current branch isn't main / master , it warns and asks for confirmation before branching off it anyway.
② task_description Summarizes your request into a structured Markdown spec (Goal / Requirements / Acceptance Criteria), saved to the run's task_description.md .
③ task_split Asks the model whether the task can be split into 2-5 independent, non-clashing subtasks. If it proposes a split, you're asked to approve it before anything runs in parallel.
④ tests_and_docs After the task (or each subtask) is implemented, requires tests and documentation updates for the change, then actually runs mix test to verify it -- not just trusting the model's claim.
⑤ (ambient, not a step) Every prompt sent during the run is prefixed with this workflow's elixir_workflow -scoped rules (see Rules below), by default nudging toward oeditus_credo / propwise / credo conventions.
⑥ lint Hard-gates on mix format --check-formatted and mix credo diff before the workflow may proceed to commit , plus a richer non-gating report from /linter saved as an artifact.
⑦ commit Formats, stages everything except .yoke/ itself, and commits with a comprehensive message drafted from the diff -- the same "please commit" convention Yoke already follows for a manual /commit .

Non-clashing parallel execution

An LLM-proposed "non-clashing" split is a best-effort heuristic, not a guarantee. So when a split is accepted, each subtask gets its own git worktree on its own branch ( /subtask/ ) and its own Brain.Session actor rooted at that worktree. Concurrent agent processes can then never write to the same files on disk, even if the model's split wasn't perfectly non-overlapping -- filesystem isolation, not just a well-intentioned plan, is what actually prevents clashes.

Each subtask runs its own tests-and-docs pass before being merged back. Once every subtask finishes, their branches are merged into the workflow branch one at a time; a merge conflict halts the workflow with the conflicting worktree left in place for manual resolution (resolve it, commit the merge, then /workflow resume ) rather than discarding anything.

If the model decides the task shouldn't be split (or you decline the proposed split), it simply runs as a single task on the workflow's own branch -- no worktrees involved.


Everything is persisted under .yoke/workflows/

.yoke/workflows/
  definitions/
    elixir.json              # materialized on first use -- edit freely
    <your-custom-workflow>.json
  runs/
    <workflow>-<timestamp>-<id>/
      workflow.json            # exact step spec this run was started with
      state.json               # status/step/branch checkpoint (resumable)
      transcript.jsonl          # every prompt, response, confirmation, command
      task_description.md       # step ②'s output
      split_plan.json           # step ③'s proposal + accept/reject decision
      subtasks/<id>/             # one per accepted parallel subtask
        branch.txt
        worktree.txt
        result.md
      artifacts/                 # lint_report.txt, etc.

workflow.json is a frozen snapshot: editing .yoke/workflows/definitions/elixir.json later never changes how an already-started run is interpreted, including on resume. Like the rest of .yoke/ , this directory should stay gitignored (Yoke's own commit step explicitly excludes .yoke/ from staging as a safety net even if your .gitignore doesn't).


Writing a custom workflow

A workflow definition is a small JSON document:

{
  "name": "my-team-flow",
  "description": "Our team's variant of the elixir workflow.",
  "rules_scope": "my_team_flow",
  "base_branch_prefixes": ["main"],
  "steps": [
    { "type": "branch", "branch_prefix": "yoke/my-team-flow" },
    { "type": "task_description" },
    { "type": "prompt", "template": "Before writing any code for: {{task_description}}, check docs/ADRs/ for a relevant architecture decision record and follow it." },
    { "type": "task_split" },
    { "type": "tests_and_docs" },
    { "type": "lint", "tools": "all" },
    { "type": "commit" }
  ]
}

Built-in step types: branch , task_description , task_split , tests_and_docs , lint , commit . The free-form prompt step is the customization escape hatch -- its "template" string is sent as an instruction to a session on the workflow's branch, with , , , and placeholders interpolated.

Definitions are discovered in priority order:

  1. Workspace: .yoke/workflows/definitions/.json
  2. Global (shared across projects): ~/.yoke/workflows/definitions/.json
  3. Bundled with Yoke (currently just elixir ), materialized into the workspace tier the first time you use it.

Use /workflow init [--from