Flows are how acpx runs multi-step ACP work without turning one giant prompt into the workflow engine. They are TypeScript modules that the acpx/flows runtime executes step by step, persisting state under ~/.acpx/flows/runs/.
Flows are an experimental, opt-in surface. The authoring API is in
acpx/flows; flows do not change how persistent sessions orprompt/execwork.
#When to use flows
Reach for a flow when one prompt is not enough — typically because:
- you need a deterministic branch (classify, then route)
- one ACP turn should not also run shell commands or call the GitHub API
- you want each step to be inspectable and replayable
- the workflow is the same across runs, but the input changes
For one-off asks, acpx codex 'do the thing' is the right tool. For "run this 6-step PR triage on every PR matching a query," a flow is the right tool.
#Run a flow
acpx flow run ./my-flow.ts
acpx flow run ./my-flow.ts --input-file ./flow-input.json
acpx flow run ./my-flow.ts --input-json '{"task":"FIX: …"}'
acpx flow run ./my-flow.ts --default-agent claude
acpx --timeout 1800 flow run ./my-flow.ts
What happens:
- The runtime loads the flow module from disk.
- A run id is generated and a run directory is created at
~/.acpx/flows/runs/<runId>/. - Steps execute in topological order. ACP steps reuse one implicit main session by default.
- Run state (graph, ACP transcripts, artifacts, errors) is persisted as the run progresses.
- The runtime exits when the graph terminates or a checkpoint pauses.
--input-json and --input-file are mutually exclusive ways to provide flow input. --default-agent supplies the default agent profile for acp nodes that do not pin one.
#Node types
Flows are graphs. Each node is one of:
| Node | Purpose |
|---|---|
acp | A model-shaped step — runs an ACP turn against an agent session. |
action | A deterministic runtime-owned step — typically a shell command or HTTP call. |
compute | A pure local function — shape inputs, route, format, derive values. |
decision | A constrained-choice ACP branch — wraps acp + parse + switch for typed routing. |
checkpoint | A pause point that requires something outside the runtime (human review, external trigger). |
Edges connect nodes. decisionEdge() produces typed edges out of a decision() node so the routing is explicit and replayable.
The runtime owns:
- graph execution and step ordering
- liveness and timeouts
- ACP session lifecycle
- persistence and replay
- routing through
decisionoutcomes
The agent owns reasoning, summarization, and tool calls inside acp and decision nodes. The flow file does not implement the workflow engine — it declares it.
#Authoring surface
Define a flow with defineFlow from acpx/flows:
import { defineFlow, acp, action, compute, decision } from "acpx/flows";
export default defineFlow({
id: "triage",
input: { task: "string" },
steps: {
classify: decision({
agent: "codex",
prompt: ({ task }) => `Classify: ${task}\nLabels: bug | feat | doc`,
choices: ["bug", "feat", "doc"],
}),
fix: acp({ prompt: ({ task }) => `Implement and verify: ${task}` }),
write_doc: acp({ prompt: ({ task }) => `Draft docs entry for: ${task}` }),
},
edges: [
["classify", "fix", (out) => out === "bug" || out === "feat"],
["classify", "write_doc", (out) => out === "doc"],
],
});
The example above is illustrative — see examples/flows/branch.flow.ts for the canonical small decision() example.
#Workspace isolation
acp nodes can pin a per-step working directory:
acp({
cwd: "${workdir}/.work-tree",
prompt: ({ task }) => `Run inside the prepped tree: ${task}`,
});
This lets a flow action step (e.g., git worktree add) prepare an isolated workspace, then have downstream acp nodes operate inside that cwd. examples/flows/workdir.flow.ts shows the pattern end-to-end.
#Permissions
Flows can declare an explicit permission requirement. If a flow needs approve-all and you forget the flag, acpx fails fast before the first step runs and prints the flag to add:
acpx flow run examples/flows/pr-triage/pr-triage.flow.ts \
--input-json '{"repo":"openclaw/acpx","prNumber":150}'
# error: this flow requires --approve-all
# correct
acpx --approve-all flow run examples/flows/pr-triage/pr-triage.flow.ts \
--input-json '{"repo":"openclaw/acpx","prNumber":150}'
This is a guardrail for flows that make real changes — the PR-triage example can comment on or close GitHub PRs against a live repo.
#Run persistence
Each run produces a bundle under ~/.acpx/flows/runs/<runId>/:
- step-by-step graph state with inputs and outputs
- ACP transcripts for every
acpanddecisionstep - artifacts written by
actionsteps (when the step opts in) - final result or error
Bundles are immutable once a run terminates. They are the input for the replay viewer.
#Timeouts
acp and action nodes use the global --timeout value as their default per-step timeout. If --timeout is not set, flows default to 15 minutes per active step. Override per step in the flow definition when needed.
#Replay viewer
examples/flows/replay-viewer/ is a browser app that visualizes saved run bundles:
- React Flow graph with per-node status
- recent-runs picker (live over WebSocket — in-progress runs update without refresh)
- ACP session inspection per step
- rewind/scrub through the run timeline
Run from the repo root:
pnpm viewer
The viewer is read-only. It opens a saved bundle and lets you inspect what happened; it does not re-run the flow.
#Example flows in the source tree
Under examples/flows/:
echo.flow.ts— minimal one-step ACP flow that returns a JSON replybranch.flow.ts—decision()+decisionEdge()constrained-choice classification, then a deterministic branchshell.flow.ts— one runtime-owned shellactionreturning structured JSONworkdir.flow.ts—actionprepares a worktree,acpruns inside that cwdtwo-turn.flow.ts— same-session ACP example that uses tools across multiple stepspr-triage/pr-triage.flow.ts— larger end-to-end example with a written spec; can comment on or close real GitHub PRs against a live repo
The PR-triage example declares an explicit approve-all requirement, so it must be run with --approve-all.
#Practical examples
# Smallest possible run
acpx flow run examples/flows/echo.flow.ts \
--input-json '{"request":"Summarize this repo in one sentence."}'
# decision()/decisionEdge() routing
acpx flow run examples/flows/branch.flow.ts \
--input-json '{"task":"FIX: add a regression test for the reconnect bug"}'
# Runtime-owned shell action
acpx flow run examples/flows/shell.flow.ts \
--input-json '{"text":"hello from shell"}'
# Multi-turn same-session work
acpx flow run examples/flows/two-turn.flow.ts \
--input-json '{"topic":"How should we validate a new ACP adapter?"}'
# Live PR triage (declares approve-all)
acpx --approve-all flow run examples/flows/pr-triage/pr-triage.flow.ts \
--input-json '{"repo":"openclaw/acpx","prNumber":150}'
#See also
- Architecture: acpx flows — full design doc.
- Flow trace replay — replay format spec.
- Flow permission requirements — fail-fast permission gating.
examples/flows/in the source tree — runnable flow examples and a colocatedREADME.