Headless & Server Agent
Slo has two faces. The TUI is where you think — panes, spinners, a live tree of the agent's work. The headless runner is where you work at scale: no terminal to occupy, no blocking cursor, just structured bytes on stdout and a clean exit code. Same agent, same skills, same conventions — different surface.
Run headless when you want Slo inside a bash script, a CI step, a cron job, a webhook handler, or as a child process under an orchestrator. Everything the TUI can do, slo run can do without a human in the loop.
Step 1 — Your first headless run
The command is slo run. The prompt is required; pass it with -p:
slo run -p "What is the current date?"
Stdout is always JSONL — one JSON object per line, each a validated stream event. The --json flag is accepted for compatibility but the output format is the same with or without it:
slo run -p "What is the current date?" --json
{"kind":"text_start","iteration":0,"block_id":"blk-0"}
{"kind":"text_delta","iteration":0,"delta":"The current date is "}
{"kind":"text_delta","iteration":0,"delta":"2026-07-18."}
{"kind":"text_end","iteration":0,"content":"The current date is 2026-07-18."}
{"kind":"run_completed","iteration":0,"content":"The current date is 2026-07-18."}
The last line is always terminal: run_completed (success), run_failed, or run_aborted.
Step 2 — Consume the stream
Because every line is structured, you compose Slo with the usual Unix tools.
Extract just the final answer:
slo run -p "Summarize the architecture of src/" --json \
| jq -r 'select(.kind == "run_completed") | .content'
Stream the answer to your terminal in real time:
slo run -p "Write a haiku about Python" --json \
| jq -j 'select(.kind == "text_delta") | .delta'
Count tool calls by name:
slo run -p "Refactor utils.py" --json \
| jq -r 'select(.kind == "tool_call_end") | .tool_name' \
| sort | uniq -c | sort -rn
Drive it from Python:
import json, subprocess
proc = subprocess.Popen(
["slo", "run", "-p", "Analyze src/", "--json"],
stdout=subprocess.PIPE, text=True,
)
for line in proc.stdout:
event = json.loads(line)
if event["kind"] == "text_delta":
print(event.get("delta", ""), end="", flush=True)
elif event["kind"] in {"run_completed", "run_failed", "run_aborted"}:
print(f"\nTerminal: {event['kind']}")
proc.wait()
Step 3 — Exit codes
slo run maps the terminal event to a POSIX exit code, so scripts can branch on it:
| Terminal event | Exit code | Meaning |
|---|---|---|
run_completed | 0 | Success |
run_failed | 1 | Failure or agent error |
run_aborted | 2 | Aborted (SIGINT, timeout, orchestrator cancel) |
slo run -p "Run the linter" --json > lint.jsonl
case $? in
0) echo "Linter passed." ;;
1) echo "Linter failed or agent errored." ;;
2) echo "Run was aborted." ;;
esac
Step 4 — stdout vs stderr
The two streams are both structured and independently parseable:
- stdout carries the agent event stream (
text_delta,tool_call_end,run_completed, …). - stderr carries structured lifecycle JSONL —
headless_session_started,headless_run_completed,headless_run_failed, and friends — one JSON object per line.
slo run -p "Refactor utils.py" --json > output.jsonl 2> run.log
Common flags
| Flag | Default | What it does |
|---|---|---|
-p, --prompt | required | The prompt to execute. |
--model | preference | Override the model for this run (e.g. claude-opus-4-8, claude-haiku-4-5). |
--reasoning | model default | Reasoning level: off, minimal, low, medium, high, xhigh. |
--cwd | $PWD | Working directory the agent reads and writes against. |
--enable-history | off | Persist the session to disk (required for --resume). |
--resume <ref> | — | Resume a session by id or unique name (implies --enable-history). |
--session-id <val> | generated | Explicit session id (UUID or slug). |
--orchestrator | off | Attach a sub-agent pool — enables dynamic workflows. |
--append-system-prompt-file | — | Inject per-run context on top of the default system prompt. |
Slo as a server agent
Headless runs are stateless and fire-and-forget by default — perfect for a request/response service. Add --enable-history and a stable session id and Slo becomes a long-lived server agent that accumulates context across calls.
Scope every request to a project
slo run -p "List every file with a TODO comment" \
--cwd ~/projects/my-api \
--json > todos.jsonl
Thread one conversation across steps
Set SLO_SESSION_ID once and every slo run in that shell (or CI step) shares the same evolving session:
export SLO_SESSION_ID="pr-$(git rev-parse --short HEAD)"
slo run -p "What does this branch change?" --enable-history --json > changes.jsonl
slo run -p "Any obvious bugs in the changes?" --enable-history --json > bugs.jsonl
slo run -p "Write a PR description from the above" --enable-history --json > pr.jsonl
The third call can reference what the first two found — the history is threaded exactly as it is in the TUI. The only difference is that input comes from --prompt instead of a text box.
Bound the run
Wrap the whole invocation in a hard wall-clock limit; on SIGTERM Slo aborts cleanly with exit code 2:
timeout -k 15 300 slo run -p "Long analysis task" --json > out.jsonl
Use cases
- CI / pull-request review — a GitHub Actions step runs
slo runscoped to the checkout, reviews the diff, and posts the result. Branch on the exit code to fail the build. - Batch jobs — fan
slo runover a list of repos or files from a shell loop; capture each JSONL for an audit trail. - Webhook / service backend — a small server shells out to
slo run --cwd <repo> --enable-history --session-id <ticket>per request and streamstext_deltaevents back to the caller. - Scheduled tasks — the scheduler fires
slo runon a timer for daily summaries and repo-watch monitoring. - Orchestrated fan-out — add
--orchestratorand Slo spawns and supervises a fleet of headless sub-agents. See dynamic workflows.
Persisting and resuming
History is written under $SLO_HOME/sessions/history/<session_id>/:
~/.slo/sessions/history/<session_id>/
├── session.json # metadata: id, name, model, workdir, timestamps
├── events.jsonl # append-only canonical event log
└── transcript.md # human-readable derived transcript
Resume it later by id or unique name — --resume implies --enable-history, so you don't need both:
slo run -p "Continue the refactoring" --resume pr-9f3a1c --json
Next steps
- Dynamic workflows —
--orchestratormode and theslo visualizemonitor. - Schedulers — run headless jobs on macOS
launchdor Linuxcron.