Skip to content

Scripting & Automation

fuseraft orchestrations are not limited to interactive terminal use. fuseraft run is a normal CLI command with a task argument, a real exit code, and (with --json) a single machine-parseable result on stdout — the same shape as any other tool you’d shell out to from a script. This page covers running fuseraft from bash or Python, wiring it to external events (a webhook, a queue, a cron tick, a file landing in a watched directory), and the exact contract you can rely on when doing so.


Terminal window
fuseraft run --config pipeline.yaml --task-file task.md --json --ci --no-banner
  • --no-banner — skip the ASCII banner
  • --json — stdout becomes exactly one JSON summary line; every human-readable status line goes to stderr instead
  • --ci — after the session completes, read .fuseraft/artifacts/test-report.json and exit 2 if any acceptance criterion is FAIL
  • Exit code: 0 success, 1 the session failed (or a setup error before it started), 2 the session succeeded but --ci found a failing criterion

That’s the whole contract most scripts need. The rest of this page fills in the details, then walks through a complete worked example.


These are the fuseraft run flags relevant to scripted invocations. Full flag reference: CLI Reference → fuseraft run.

Flag Why it matters for scripts
-f, --task-file <path> Pass a long or multi-line task without shell-quoting gymnastics. Build the task text yourself (e.g. from an event payload) and write it to a temp file.
--json Stdout carries only the JSON summary; see The --json contract below.
--ci Fails the process (exit 2) when the orchestration’s own acceptance criteria didn’t pass — not just when the session crashed.
--no-banner Skip the ASCII banner. Redundant with --json (which already suppresses it) but harmless to include; useful on its own if you’re not using --json.
--work-dir <path> Pin the session to a specific directory instead of relying on the process’s CWD — important when a single long-running handler processes events for multiple projects/directories.
-o, --output <path> Save a Markdown transcript alongside the JSON summary, for audit trails.
-r, --resume <sessionId> Retry a session that was interrupted (e.g. the handler process was killed mid-run) instead of starting over.

Avoid --hitl, --devui, and an omitted task in scripts. Each either blocks on terminal input or opens a browser — none make sense in an unattended process. --json does not change this; it’s your responsibility not to combine them.


Enable JSON mode two ways:

  • Per invocation: pass --json on the command line.
  • Per config: set Output.Json: true in the orchestration config, so every run of that config behaves this way without needing the flag. See Configuration → Output. The --json flag always takes precedence if both are used.

Stream contract: when JSON mode is active, stdout carries only the final JSON summary — no banner, no turn panels, no spinner, no per-agent status. Every human-readable line, including startup diagnostics, goes to stderr. This makes stdout safe to pipe straight into jq or json.loads() without stripping anything first.

Summary schema and full field reference: CLI Reference → fuseraft run → --json output. In short: session_id, task, config, succeeded, error_message, exit_code, turns, elapsed_seconds, tokens.{input,output}, transcript_path, and ci.{passed,skipped,failed_criteria} when --ci was used.

A run can fail before a session ever starts — a bad --work-dir, a missing --spec file, an unresolvable --resume ID, or the config file itself failing to load. fuseraft’s contract for these:

  • --json flag set: jsonMode is known from the very first line of the command, before anything else runs. Every one of these early failures still emits exactly one JSON summary line to stdout (succeeded: false, error_message set, other fields zeroed) and all diagnostic text goes to stderr — the same guarantee as a normal completed run.
  • Only Output.Json: true in the config (no --json flag): JSON mode can’t be confirmed until the config has finished loading — the setting itself lives in the config. If the failure happens before that point, fuseraft cannot know whether to emit JSON, so it doesn’t: stdout is left completely empty (never wrong, never mixed with plain text) and the failure is reported via exit code plus a stderr message only. If the config loads successfully and something fails afterward, JSON mode is fully known and behaves exactly like the --json flag case above.

Either way, stdout never contains anything other than a well-formed JSON summary or nothing at all. A script’s parsing logic should be: try json.loads(stdout); if that fails (empty or non-JSON), treat it as a failure and fall back to the exit code plus whatever was captured on stderr.

Terminal window
# --json flag: JSON summary even for a setup error, no session ever started
$ fuseraft run -c pipeline.yaml --work-dir /no/such/dir --json --no-banner
{"session_id":null,"task":null,"config":"/abs/path/pipeline.yaml","succeeded":false,"error_message":"Work directory not found: /no/such/dir","exit_code":1,"turns":0,"elapsed_seconds":0,"tokens":{"input":0,"output":0},"transcript_path":null,"ci":null}
$ echo $?
1
Terminal window
# Output.Json: true only, same failure: stdout is empty, not corrupted
$ fuseraft run -c pipeline.yaml --work-dir /no/such/dir --no-banner
$ echo $?
1

If you control the invocation (which you almost always do, since you’re the one writing the wrapper script), pass --json explicitly rather than relying on Output.Json alone — it closes this last gap and gives you a JSON line for every outcome, not just successful ones.


Command 0 1 2
fuseraft run Session completed Session failed, or a setup error before it started Only with --ci: session completed but an acceptance criterion is FAIL
fuseraft validate Config is valid (warnings may still print) One or more errors found —
fuseraft schedule run All due jobs ticked without error A job failed —

fuseraft validate config.yaml --check-connectivity is worth running as a pre-flight step in CI before the first real fuseraft run — it makes a 1-token call to each configured model endpoint and confirms every API key actually works, so a pipeline fails fast on a misconfigured key instead of burning a full session first. See CLI Reference → fuseraft validate.


For anything on a fixed schedule, fuseraft schedule is usually simpler than hand-rolling a cron entry that calls fuseraft run directly — it stores the job definition (config path, work dir, output path template) once in ~/.fuseraft/schedule/, and fuseraft schedule run is designed to be ticked every minute by cron or a systemd timer with no daemon required. See CLI Reference → fuseraft schedule for the full command set.

For an event that should run a specific job on demand — not wait for its next scheduled tick — use:

Terminal window
fuseraft schedule run --name my-job

This ignores the job’s schedule and enabled flag and runs it immediately, while still reusing the config/work-dir/output settings stored in the job definition.

For anything else — a webhook payload, a message off a queue, a file landing in a watched directory — the pattern is the same regardless of trigger source: your event handler builds a task (usually naming the specific input/output the event refers to) and shells out to fuseraft run --json --ci. See the worked example below.

Or: dispatch into a resident fuseraft serve daemon

Section titled “Or: dispatch into a resident fuseraft serve daemon”

Shelling out to fuseraft run per event is simple, but it pays the cost of rebuilding the whole agent team — including reconnecting every configured MCP server — on every single event. If events arrive frequently enough for that to matter, start fuseraft serve once and dispatch into it instead: it builds the orchestrator a single time and stays resident, so each event becomes a cheap dispatch_task MCP call (or a line written to its Unix socket) rather than a new process.

Terminal window
fuseraft serve --config .fuseraft/config/orchestration.yaml &
# Any MCP client works — this is the shape regardless of library.
result = await mcp_client.call_tool("dispatch_task", {"task": f"Process {event['path']}"})
session_id = result["sessionId"]
# Poll get_status/get_result, or fire-and-forget if you don't need the outcome.

This is also the natural target for a message-queue consumer (RabbitMQ, SQS, etc.): the consumer holds no fuseraft-specific dependency itself — it just calls dispatch_task over MCP (or writes a line to the attach socket) for each message it pulls off the queue, and fuseraft never needs to know the queue technology exists. See CLI Reference → fuseraft serve.


Worked example: an event-driven ETL pipeline

Section titled “Worked example: an event-driven ETL pipeline”

config/examples/etl-pipeline.yaml and scripts/run-pipeline.sh / scripts/run_pipeline.py are a complete, runnable version of this pattern — copy them as a starting point.

Two agents, run at most once each — a linear pipeline, not an open-ended chat:

Orchestration:
Name: EtlPipeline
Output:
Json: true # every invocation behaves as if --json was passed
Selection:
Type: sequential # Extractor, then Transformer, in that fixed order
Termination:
Type: composite
Strategies:
- Type: regex
Pattern: "PIPELINE_COMPLETE" # Transformer's completion signal
- Type: maxiterations
MaxIterations: 4 # hard stop if something loops
Validation:
TestReportPath: .fuseraft/artifacts/test-report.json # feeds --ci
Security:
FileSystemSandboxPath: .
ChangeEnvelope:
- "output/**" # Transformer may only write here
- ".fuseraft/artifacts/**"
Agents:
- Name: Extractor # reads + validates input, never writes
- Name: Transformer # normalizes, writes output, files the test report

Points worth calling out:

  • Output.Json: true means this config always reports structured results — nobody has to remember to pass --json when invoking it, which matters once several scripts/services call the same config.
  • Selection.Type: sequential with two agents means: Extractor runs turn 1, Transformer runs turn 2, done. There’s no keyword routing to configure — sequential just advances through the agent list in order.
  • Termination combines the Transformer’s own completion signal (PIPELINE_COMPLETE) with a hard MaxIterations cap, so a malfunctioning agent can’t loop forever in an unattended process.
  • Security.ChangeEnvelope restricts writes to output/** and the artifacts directory — since this runs unattended in response to external events, it shouldn’t be able to touch anything else in the sandboxed work dir even if an agent misbehaves. See Security.
  • Validation.TestReportPath is what makes --ci meaningful here: the Transformer is instructed to write a PASS/FAIL acceptance-criteria report before signalling completion, and --ci reads it after the session ends.

See the full file for the complete agent instructions. Full config schema: Configuration.

Both scripts do the same thing — build a task string naming the input/output paths, invoke fuseraft run --json --ci, parse the summary, and exit with fuseraft’s own exit code:

Terminal window
scripts/run-pipeline.sh <input-path> <output-path> [work-dir]
Terminal window
python3 scripts/run_pipeline.py <input-path> <output-path> [--work-dir DIR]

The Python version is also importable as a library function, which is the more useful form for a long-running event handler (a webhook server, a queue consumer) that shouldn’t fork a fresh interpreter per event:

from run_pipeline import run_pipeline
def on_file_uploaded(event):
result = run_pipeline(event["path"], f"output/{event['id']}.json")
if result["succeeded"] and result.get("ci", {}).get("passed", True):
notify_downstream(result)
else:
alert_oncall(result)

run_pipeline() returns the parsed JSON summary dict with exit_code added — a failed session (bad output, --ci FAIL, agent error) comes back as result["succeeded"] is False in the return value, not an exception, since callers need to branch on that as a normal, expected outcome. It only raises if fuseraft itself couldn’t be started (e.g. not on PATH).

Both scripts read FUSERAFT_BIN (default: fuseraft on PATH) and FUSERAFT_PIPELINE_CONFIG (default: config/examples/etl-pipeline.yaml) from the environment, so you can point them at a different binary or config without editing the script.

Terminal window
export ANTHROPIC_API_KEY=<your-key> # or the provider configured in the YAML
echo '[{"id":1,"first_name":"Ada","email":"ADA@EXAMPLE.COM "}]' > input.json
scripts/run-pipeline.sh input.json output/normalized.json .
echo "exit code: $?"
cat output/normalized.json