Basic Concepts
Cloche interprets ‘.cloche’ files containing a DSL (domain specific language) for defining workflows. The syntax is intended to be simple and flexible, acting as the glue binding the steps in your workflow together.
Environment
Cloche Workflows run either on the Host or within a Docker Container. The run environment is bound to the entire Workflow and is the primary mechanism by which we can isolate riskier actions (such as yolo-mode LLM development) to a containerized environment, while also keeping access to the host machine where necessary for things like merging changes back to the main repository.
The environment is specified by including a mandatory host{} or container{} configuration block. The blocks may be left empty if not overiding the default setting values, but one of them must be present in the
workflow definition:
workflow "host-side" {
host {}
}
workflow "container-side" {
container {}
}
Containerized Workspace
Containerized environments copy your project into /workspace/ within the container. This is in lieu of a container mount to avoid parallel tasks from conflicting while modifying files - and to keep your host file
system safe should the LLM erroneously decide to run rm -rf / --no-really-i-mean-it.
You don’t need to put this step in your Dockerfile - Cloche will automatically copy your project into the container when it starts up. (This also keeps the project code up-to-date with the Host without rebuilding the container every time.)
Container Isolation
Your project directory is never modified by the container. Changes are only extracted back via docker cp to a git branch after the run completes, so you always review the agent’s changes before merging. Auth files (~/.claude/ and ~/.claude.json) are copied — not bind-mounted — so each container gets its own isolated copy. Override files from .cloche/overrides/ are applied on top of /workspace/ at container start.
For a deeper look at the security model, network controls, and credential handling, see the Safety Guide.
Workflows
Workflows are the primary concept in Cloche. Every behavior Cloche triggers for you must be defined as part of a Workflow.
Workflow files must be inside the <project>/.cloche/ directory. They may have any file name, but the extension must be .cloche e.g. main.cloche or review.cloche. You can define workflows
in any *.cloche file, putting all your definitions in a single file or splitting them up with a single workflow defined per file.
Workflow files define a collection of
- Steps: The individual units of work
- Wires: The connections between Steps
- Configuration: Settings which control the workflow environment
workflow "develop" {
// Comments begin with double-slash
// This workflow runs on the container and specifies the name of the image to use.
container {
image = "my-project:latest" // The default is whatever is in .cloche/Dockerfile, but you can overide for different workflows
}
// The first step defined is the entry point to the workflow. After this, order doesn't matter.
step implement {
prompt = file(".cloche/prompts/implement.md") // Prompts can be inline strings, or a file reference.
results = [success, fail] // By default, an exit code of 0 means success and all others are fail.
}
step test {
run = "make test" // 'run' defines a 'script' step which executes the command or file provided.
results = [success, fail]
}
// Wires define the connections between Steps
implement:success -> test // Here, the 'success' wire connects implement to test.
implement:fail -> abort
test:success -> done // 'done' and 'abort' are special "terminal" states. They don't exist as Steps. They're how we signal to Cloche that the workflow is finished.
test:fail -> abort
}
Steps
A step is a single executable unit of work. Cloche infers the type from which field it declares:
prompt: Executes a Prompt using the configured LLM Agentrun: Executes a command in the workflow’s Environmentworkflow_name: Starts another Workflowpoll: Pauses the workflow until the poll script triggers the next wire
The step {} block declares its type by specifying one of these fields and the string or file needed.
Prompt Steps
A prompt step invokes an LLM agent with a task description. The agent runs autonomously inside the container (or on the host, for host workflows) and reports its outcome by printing a CLOCHE_RESULT:<name> marker to stdout.
step design {
prompt = file(".cloche/prompts/design.md")
results = [success, fail]
}
The prompt field can be an inline string or a file(...) reference. By convention, prompt files live in .cloche/prompts/ and are written in Markdown.
Script Steps
A script step runs a shell command. Exit code 0 maps to success, any non-zero exit maps to fail, unless the script explicitly prints CLOCHE_RESULT:<name> to override.
step test {
run = "make test 2>&1"
results = [success, fail]
}
Script steps are the right choice for deterministic checks: running tests, linters, validators, build commands, or any shell pipeline with a clear pass/fail outcome.
Poll Steps
A poll step pauses a workflow until an external input is available. Cloche runs the script at the specified fixed interval and suspends the workflow until the script reports a decision. This ensures that e.g. containers and their
run state are preserved to support iterative workflow processes. This is the right primitive for awaiting human-facing approval gates, code-review, CI result checks, or anywhere the pipeline needs to pause for an external source.
Important: Poll steps have more manual Wiring requirements than other Step types. Unlike other Step types, we need to support a pending case - no results are available yet, so we need to stay in this workflow step.
The polling script reports exit wire decisions the same way as any other Step — by printing CLOCHE_RESULT:<wire name> to stdout. The key difference is what happens when no marker is printed: exit 0 with no marker
means pending and Cloche will poll again after interval. Non-zero exits with no marker follows the fail wire.
In order to select a non-pending, non-failure wire, the Poll step script must specify the exit wire by printing CLOCHE_RESULT:<wire name> to stdout. e.g. in the code-review exaple below, there are four wires
defined: pending, fail, approved, fix. The first two will be triggered as defaults based on exit code alone. The latter must be printed to STDOUT (regardless of exit code - the explicit CLOCHE_RESULT: text takes
precedence) once the review feedback is recieved.
step code-review {
poll = "bash .cloche/scripts/check-pr-review.sh"
interval = "5m"
timeout = "48h"
results = [approved, fix] // 'fix' could forward to 'address-feedback' which in turn could return to this step, creating a human-in-the-loop review flow.
}
Wires
Wires are the named edges that connect steps. Each step declares its possible results, and a wire maps a specific step:result pair to the next step to run.
implement:success -> test
implement:fail -> abort
test:success -> done
test:fail -> fix
fix:success -> test
fix:fail -> abort
done and abort are built-in terminal targets. Every workflow must have at least one wire reaching done - you should probably have something wired to abort as well, but if your workflow can only succeed, it’s fine.
You can specify multiple targets for the same wire to create parallel flows (e.g. run ‘code-review’, ’lint-check’ and ‘integration-test’ steps at onces) by creating multiple lines:
test:success -> code-review
test:success -> lint-check
cancel-and-cleanup:success -> abort // cleanup workflow for abort cases
cancel-and-cleanup:fail -> abort
You can also define a Step must await multiple Steps completion using collect with all or any keywords:
collect all(code-review:success, lint-check:success) -> done // wait for all to succeed before we're done
collect any(code-review:fail, lint-check:fail) -> cancel-and-cleanup // trigger early termination
Configuration
Within a workflow are also some configuration options to tune your workflow’s Steps to your specifications
Agent Block
An agent is a named LLM harness: a command plus optional arguments that Cloche runs when executing prompt steps. Declaring agents lets prompt steps share the same configuration and makes it simple to use different LLMS for different purposes.
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
}
If a prompt step does not reference an explicit agent, Cloche falls back to the workflow-level default (container { agent_command = ... } or host { agent_command = ... }), then to the CLOCHE_AGENT_COMMAND environment variable, and finally to claude. See Agent Setup for the full resolution order and for how to configure authentication for each supported agent.
Orchestration Model
Cloche’s orchestration loop (cloche loop) uses a two-phase model to discover and execute work automatically.
Phases
| Phase | Workflow name | Purpose |
|---|---|---|
| 1 | list-tasks |
Discover available work. Final step output is parsed as JSONL (one task per line). |
| 2 | main |
Do the work. Receives a task ID via the CLOCHE_TASK_ID env var. |
Only main is required. If list-tasks is absent, the daemon runs main continuously using a sentinel task (no task ID tracking).
The list-tasks workflow output is JSONL. Each line is a JSON object:
| Field | Required | Description |
|---|---|---|
id |
yes | Unique task identifier |
status |
yes | One of open, closed, in-progress |
title |
no | Short summary |
description |
no | Full description |
metadata |
no | Arbitrary key-value pairs |
The daemon picks the first task with status open and runs main with that task ID. Tasks are deduplicated within a configurable timeout window (see Configuration Reference) to prevent rapid reassignment.
Task claiming and cleanup
Cloche discovers tasks but does not manage external tracker state. Your workflow scripts are responsible for:
- Claiming a task — the
mainworkflow typically starts with a script step that marks the task as in-progress in your tracker (e.g. assigning a GitHub issue, moving a Jira ticket). - Cleanup on completion — a finalization step that closes the issue, pushes the branch, opens a PR, or whatever your pipeline requires.
- Cleanup on failure — unclaiming the task so it returns to the open pool for a future attempt.
Releasing stale tasks
An optional release-task host workflow can be defined. It is not part of the automatic loop — it is invoked on demand (e.g. from the web dashboard’s Release button) to return a stale claimed task back to open status. It receives CLOCHE_TASK_ID for the task to release.
Learn More
From here, the DSL Reference walks through the complete syntax, and the GitHub Issues tutorial shows how to compose these primitives into a full automation pipeline with a human-in-the-loop review cycle.