DSL Reference

Workflow files use the .cloche extension and live in .cloche/. The first step declared is the entry point. Graphs are validated at parse time: all results must be wired, no orphaned steps, and an entry point must exist.

Basic Structure

workflow "develop" {
  container {
    image         = "my-project:latest"
    agent_command = "claude"
  }

  step implement {
    prompt  = file(".cloche/prompts/implement.md")
    results = [success, fail]
  }

  step test {
    run     = "make test && make lint"
    results = [success, fail]
  }

  step fix {
    prompt       = file(".cloche/prompts/fix.md")
    max_attempts = 3
    results      = [success, fail, give-up]
  }

  implement:success -> test
  implement:fail    -> abort
  test:success      -> done
  test:fail         -> fix
  fix:success       -> test
  fix:fail          -> abort
  fix:give-up       -> abort
}

Step Configuration

A step must have exactly one of prompt, run, workflow_name, or poll.

Key Type Description
type identifier Explicit step type declaration. Rarely needed — step type is normally inferred from prompt, run, workflow_name, or poll.
prompt string or file("path") Prompt template. Makes this an agent step.
run string Shell command. Makes this a script step.
workflow_name string Workflow to dispatch by name. Makes this a workflow step.
poll string Shell command polled at a fixed interval. Makes this a poll step.
interval string Poll frequency as a Go duration, e.g. "5m", "1h". Required for poll steps.
results ident list Declared result names, e.g. [success, fail, give-up].
max_attempts integer Max retries before automatic give-up result.
timeout string Step timeout as Go duration, e.g. "30m", "2h". Default: 30m.
token-limit integer Maximum output tokens for this step. Produces a "token-limit" result (implicitly wired to abort) when exceeded. Default: 500 000. -1 disables enforcement; 0 aborts immediately without running the step. See Token Limits.
repository string Repository name (from [[repositories]] in config.toml) to pin this step to a specific repository’s workspace. When omitted, the runtime uses the project default.
agent_command string Agent binary name(s), comma-separated for fallback chains, e.g. "claude,gemini".
agent_args string Override default agent arguments.
agent identifier Reference a named agent declared in the workflow’s agent block.
usage_command string Shell command to capture token usage after agent step. Output must be JSON: {"input_tokens": N, "output_tokens": N}.
skip string Shell command to run before the step. Exit 0 = skip (follow the step’s first declared result wire); non-zero = run normally. 90s timeout.
prompt_step string For workflow steps: which preceding step’s output to use as the prompt.

The file() Function

file("path") reads the file at the given path relative to the working directory (/workspace/ in containers) at execution time, not parse time.

prompt = file(".cloche/prompts/implement.md")

Prompt Templates

Prompt files referenced by file("...") are evaluated as templates before the agent runs. The {{ }} syntax injects runtime values.

Form Meaning
{{ $name }} Variable lookup: built-in first, then KV store
{{! cmd }} Run cmd via sh -c; substitute stdout (30s timeout)
{{@ path }} Read file at path; substitute contents verbatim
$$ Literal $ — only inside {{! ... }}; left untouched elsewhere

Inside {{! }} and {{@ }} bodies, bare $name references resolve against the same built-in / KV tiers as {{ $name }} — e.g. {{@ $temp_file_dir/data.csv }} expands $temp_file_dir first, then reads the resulting path. Brace pairs inside a directive body are literal (not nested directives), and file contents / shell stdout are not re-templated.

Built-in variables:

Variable Value
$task_id Task identifier
$run_id Run identifier
$step_name Name of the current step
$workdir Working directory for the step
$prev_output Preceding step’s captured stdout
$task_description Content of the user prompt (--prompt flag)

Built-ins shadow KV keys with the same name. An unresolvable variable fails the step before the agent runs.

Agent Declarations

Declare reusable named agents at the workflow level and reference them from steps.

workflow "develop" {
  agent claude {
    command = "claude"
    args    = "-p --output-format stream-json"
  }

  agent codex {
    command = "codex"
    args    = "--full-auto"
  }

  step implement {
    prompt  = file(".cloche/prompts/implement.md")
    agent   = claude
    results = [success, fail]
  }

  step review {
    prompt  = file(".cloche/prompts/review.md")
    agent   = codex
    results = [success, fail]
  }

  implement:success -> review
  implement:fail    -> abort
  review:success    -> done
  review:fail       -> implement
}

Agent block fields:

Field Required Description
command yes The agent binary to run.
args no Arguments passed to the agent command.

Resolution order (highest to lowest priority):

  1. Step-level agent_command / agent_args
  2. agent = <identifier> declaration
  3. Workflow-level config block
  4. CLOCHE_AGENT_COMMAND environment variable
  5. Default: claude

Duplicate agent names within a workflow are a parse error. An agent declaration without a command field is a parse error.

Configuration Blocks

container {} (container workflows)

workflow "develop" {
  container {
    id            = "dev-env"
    image         = "my-project:latest"
    agent_command = "claude"
    agent_args    = "-p --dangerously-skip-permissions"
    memory        = "4g"
    network_allow = ["docs.python.org", "api.example.com"]
  }
  ...
}

Recognized keys: id, image, agent_command, agent_args, memory, network_allow. Unknown keys are silently ignored.

network_allow is parsed but not yet enforced — containers currently run with unrestricted network access.

Step-level container {} — a container { … } block may also appear inside an individual step body. Keys set there override the workflow-level container {} defaults for that step only — useful for giving a single step a different image or network policy.

workflow "develop" {
  container {
    image         = "my-project:latest"
    agent_command = "claude"
  }

  step research {
    prompt = file(".cloche/prompts/research.md")
    container {
      network_allow = "api.example.com"
    }
    results = [success, fail]
  }
  ...
}

host {} (host workflows)

workflow "main" {
  host {
    agent_command = "claude"
  }
  ...
}

Recognized keys: agent_command, agent_args. Unknown keys are silently ignored. An empty host {} block is valid.

Repository Declarations

Repositories are configured in .cloche/config.toml as [[repositories]] entries, not in .cloche workflow files — there is no repository DSL block. Workflows declare which repositories they use with the repos field:

workflow "develop-backend" {
  repos = ["backend"]
  ...
}

repos is a list of repository names matching entries in config.toml. The runtime enforces it in two ways:

  • Container seeding — only the declared repositories are materialized into the container’s workspace copy, so workflows that need one repo of many don’t pay to copy the rest. Workflows sharing a container.id share one container (and one copy), so the copy contains the union of their repos declarations; if any sharing workflow declares no repos, all configured repositories are included.
  • Extraction — result branches and worktrees are created only for the declared repositories.

A workflow with no repos field gets every configured repository (the backward-compatible default). Declaring a name not present in config.toml is an error.

Step Environment Variables

Cloche injects environment variables into every step invocation. The available variables differ between host and container contexts.

Host steps

Variable Description
CLOCHE_PROJECT_DIR Absolute path to the project directory on the host.
CLOCHE_PREV_OUTPUT Path to the output file from the immediately preceding step.
CLOCHE_RUN_ID Workflow ID for this workflow execution.
CLOCHE_TASK_ID Task ID assigned by the daemon (set for the main phase).
CLOCHE_ATTEMPT_ID Attempt identifier for this run.
CLOCHE_GIT_AUTHOR_NAME Git author name from config. Only set when configured.
CLOCHE_GIT_AUTHOR_EMAIL Git author email from config. Only set when configured.
CLOCHE_GIT_SSH_COMMAND Pre-composed SSH command from config. Only set when configured.

Container steps

Variable Description
CLOCHE_RUN_ID Run ID for this workflow execution (e.g. a133:develop).
CLOCHE_TASK_ID Task ID assigned by the daemon. Set when the run is associated with a task.
CLOCHE_ATTEMPT_ID Attempt identifier for this container run. Used for unique container naming.
CLOCHE_PROJECT_DIR Working directory inside the container (/workspace).
CLOCHE_AGENT_COMMAND Overrides the default agent command inside the container.
CLOCHE_ADDR Daemon gRPC address (e.g. host.docker.internal:50051). Used by clo get/clo set.
ANTHROPIC_API_KEY Passed through from the host environment if set.

See Communicating Between Steps for how to pass data between steps using these variables, the KV store, and shared files.

Container IDs

Every container workflow has a container id used to identify which shared container to use per run attempt. Set via the id key in container {}. Default id is _default.

Workflows sharing the same id share a container per attempt:

workflow "develop" {
  container {
    id    = "dev-env"
    image = "my-project:latest"
  }
  ...
}

workflow "review" {
  container {
    id = "dev-env"  // shares the same container as "develop"
  }
  ...
}

cloche validate enforces consistent config for workflows sharing a container id.

Wiring

Connect steps with step:result -> next_step:

implement:success -> test
implement:fail    -> abort
test:success      -> done
test:fail         -> fix

Reserved result names. done and abort are terminal targets, not steps. parked is also reserved: steps cannot declare or wire it. It is produced by the help-channel park mechanism when a step’s clo ask / ask_user call goes unanswered past park_after — the run suspends (state parked) instead of completing or failing, and resumes from that step when the pending help thread receives a reply. See cloche threads.

Token Limits

Cloche can automatically abort a run when an agent exhausts a token budget. Token limits are injected implicitly — every step automatically gains a token-limit result and wire that routes to abort unless overridden. Only output tokens count against the limit; input tokens do not.

Workflow-level token limit — set a per-run budget that caps cumulative output tokens across all steps:

workflow "develop" {
  token-limit = 100000
  ...
}

Step-level token limit — override the budget for a specific step:

step implement {
  prompt      = file(".cloche/prompts/implement.md")
  token-limit = 50000
  results     = [success, fail]
}

Default values:

  • Per-step: 500 000 output tokens
  • Workflow-level: 2 000 000 output tokens

Sentinels: token-limit = -1 disables enforcement (unlimited output tokens); token-limit = 0 aborts immediately without running the step/workflow.

Overriding the abort target — by default a token-limit event routes to abort. A global wire directive, placed at the workflow level alongside other wiring lines (not inside a step block), redirects it — e.g. to a cleanup or summary step:

token-limit -> release-task

Every step whose token-limit result has not been wired explicitly will route there instead of abort.

Note

Only token-limit -> is supported as a global wire shorthand. A timeout -> <target> line at the workflow level is parsed without error but has no effect. To redirect timeout events for a specific step, use a step-level wire: <stepname>:timeout -> <target>.

Retry Loops

Wire failures back to earlier steps and use max_attempts to cap retries. When attempts are exhausted, the step returns give-up:

step fix {
  prompt       = file(".cloche/prompts/fix.md")
  max_attempts = 2
  results      = [success, fail, give-up]
}

test:fail   -> fix
fix:success -> test   // retry the test
fix:fail    -> abort
fix:give-up -> abort

Poll Steps

A poll step pauses a workflow until an external decision is available — a code review, an approval gate, a CI run, or any event the pipeline needs to wait for. The orchestrator polls a shell command at a fixed interval until the script reports a decision, the step times out, or the script fails.

Poll steps work in both host and container workflows. In a host workflow the daemon’s orchestration loop drives the polling. In a container workflow the in-container agent runs a standalone polling loop.

step code-review {
  poll     = "scripts/check-pr-review.sh"
  interval = "5m"
  timeout  = "48h"
  results  = [approved, fix]
}

code-review:approved -> merge
code-review:fix      -> address-feedback
code-review:timeout  -> escalate     // or omit; default routes to abort

The polling script reports a decision by printing CLOCHE_RESULT:<wire-name> to stdout, the same mechanism as script steps. The key difference is what happens when no marker is printed:

Exit code CLOCHE_RESULT marker Outcome
0 none Pending — poll again after interval.
0 <name> Decision — follow the named wire.
non-zero none Failure — follow the fail wire.
non-zero <name> Decision — follow the named wire. Non-zero exit is ignored when a marker is present.

Polling cadence

The default timeout for poll steps is 72h (vs. 30m for other step types). The first poll fires immediately; subsequent polls fire no sooner than interval after the previous poll completes. interval is a no-sooner-than constraint, not a strict schedule — actual poll times depend on the loop tick rate and on how long the previous invocation took, so expect polls to land within ~30 seconds of the ideal time.

Overlapping invocations. If a poll is still running when the next interval comes due, that interval is skipped. If a single invocation runs longer than 4x interval (three consecutive skips), the step produces a fail result and follows the fail wire.

Script execution environment

Poll scripts run under sh -c. In a host workflow the working directory is the project’s main git worktree. In a container workflow it is /workspace/. The following variables are injected on every invocation:

Variable Host Container Description
CLOCHE_PROJECT_DIR yes yes Absolute path to the project directory.
CLOCHE_PREV_OUTPUT yes no Path to the output file from the immediately preceding step.
CLOCHE_RUN_ID yes yes Run ID for the current workflow run.
CLOCHE_TASK_ID yes no Task ID being processed (if launched from a task).
CLOCHE_ATTEMPT_ID yes no Attempt ID for the current run attempt.

Reading run context

Poll scripts can read values written to the run’s KV store by earlier steps via cloche get <key> (host-side) or clo get <key> (container-side). The KV store persists for the lifetime of the run.

# In an earlier container step (e.g. create-pr):
clo set pr_id 1234

# In the polling script:
pr_id=$(cloche get pr_id)
state=$(gh pr view "$pr_id" --json state -q .state)
case "$state" in
  MERGED) echo "CLOCHE_RESULT:approved" ;;
  CLOSED) echo "CLOCHE_RESULT:rejected" ;;
  *)      ;;  # still open → exit 0 with no marker → pending
esac

Idempotency

Poll scripts are invoked once per interval for the lifetime of the step — which can easily be dozens or hundreds of invocations over a 72-hour window. Any side effect (posting a comment, sending a notification, creating a ticket) must be guarded so it runs at most once. Use the KV store to record that the side effect has occurred:

if [ "$(cloche get notified_reviewer)" != "yes" ]; then
  slack-notify "@reviewer PR ready"
  cloche set notified_reviewer yes
fi

Visibility in cloche list / cloche status

While a poll step is active, its run’s state is set to waiting, which cloche list and cloche status surface distinctly from running. The daemon also records last_poll_at and the step name so you can see how long the step has been waiting and when it last polled.

Parallel Branches (Fanout)

Wire one result to multiple targets for concurrent execution:

test:success -> lint
test:success -> quality

Collect (Join)

Synchronize parallel branches with collect:

collect all(lint:success, quality:success) -> done
collect any(lint:success, quality:success) -> done

all fires when every condition is met. any fires when at least one is.

Comments

Line comments use //:

// This is a comment
implement:success -> test  // inline comment

Examples

Host Workflow

workflow "list-tasks" {
  host {}

  step get-tasks {
    run     = "python3 .cloche/scripts/get-tasks.py"
    results = [success, fail]
  }

  get-tasks:success -> done
  get-tasks:fail    -> abort
}

workflow "main" {
  host {}

  step claim-task {
    run     = "python3 .cloche/scripts/claim-task.py"
    results = [success, fail]
  }

  step develop {
    workflow_name = "develop"
    results       = [success, fail]
  }

  step finalize {
    run     = "python3 .cloche/scripts/finalize.py"
    results = [success, fail]
  }

  claim-task:success -> develop
  claim-task:fail    -> abort
  develop:success    -> finalize
  develop:fail       -> finalize
  finalize:success   -> done
  finalize:fail      -> abort
}

Container Workflow with Parallel Validation

workflow "develop" {
  step implement {
    prompt  = file("prompts/implement.md")
    results = [success, fail]
  }

  step test {
    run     = "bundle exec rake test 2>&1"
    results = [success, fail]
  }

  step lint {
    run     = "bundle exec rubocop 2>&1"
    results = [success, fail]
  }

  step quality {
    run     = "python3 scripts/quality-check.py 2>&1"
    results = [success, fail]
  }

  step fix {
    prompt       = file("prompts/fix.md")
    max_attempts = 2
    results      = [success, fail, give-up]
  }

  implement:success -> test
  implement:fail    -> abort

  test:success -> lint
  test:success -> quality
  test:fail    -> fix

  lint:fail    -> fix
  quality:fail -> fix
  collect all(lint:success, quality:success) -> done

  fix:success  -> test
  fix:fail     -> abort
  fix:give-up  -> abort
}