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.

slo run streaming validated JSONL events to stdout, then piped through jq to extract the final answer.

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 eventExit codeMeaning
run_completed0Success
run_failed1Failure or agent error
run_aborted2Aborted (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

FlagDefaultWhat it does
-p, --promptrequiredThe prompt to execute.
--modelpreferenceOverride the model for this run (e.g. claude-opus-4-8, claude-haiku-4-5).
--reasoningmodel defaultReasoning level: off, minimal, low, medium, high, xhigh.
--cwd$PWDWorking directory the agent reads and writes against.
--enable-historyoffPersist the session to disk (required for --resume).
--resume <ref>Resume a session by id or unique name (implies --enable-history).
--session-id <val>generatedExplicit session id (UUID or slug).
--orchestratoroffAttach a sub-agent pool — enables dynamic workflows.
--append-system-prompt-fileInject 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.

Slo threaded across three headless steps with one session id — the server-agent pattern.

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 run scoped to the checkout, reviews the diff, and posts the result. Branch on the exit code to fail the build.
  • Batch jobs — fan slo run over 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 streams text_delta events back to the caller.
  • Scheduled tasks — the scheduler fires slo run on a timer for daily summaries and repo-watch monitoring.
  • Orchestrated fan-out — add --orchestrator and 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--orchestrator mode and the slo visualize monitor.
  • Schedulers — run headless jobs on macOS launchd or Linux cron.