Headless & server agent

The TUI is where you think out loud with Slo. slo run is the same agent with no UI: you give it a task, it streams JSON events to stdout, and it exits with a code your script can check. Same skills, same conventions.

Use it in a shell script, a CI job, a cron entry, a webhook handler, or as a sub-agent under an orchestrator.

slo run streaming JSON events, then piped through jq to pull out the final answer.

Use headless workflows for jobs with several steps, such as planning, implementation, testing and review. Each phase can run in a focused sub-agent while you check progress separately.

From the TUI, ask:

"Use headless dynamic workflows to run code-test-review for task 12.b in specifications/12-refunds/."

Slo launches the workflow in the background and returns your prompt. Use slo visualize to watch it and /workflows to manage it. If a workflow stalls or fails, /workflows resume <workflow-id> resumes it against its saved history.

From a script, use slo run --orchestrator with --enable-history. A shell invocation stays in the foreground unless you arrange otherwise; "headless" means no UI, not automatic shell detachment. See Dynamic workflows for a complete example.

Your first headless run

slo run -p "What is the current date?"

Every line on stdout is one JSON event:

{"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 one of three: run_completed, run_failed or run_aborted.

--json is accepted for compatibility, but the output is always JSON lines either way.

Using the output

Because every line is JSON, the usual tools work.

Just the final answer:

slo run -p "Summarise the architecture of src/" \
  | jq -r 'select(.kind == "run_completed") | .content'

Print the answer as it streams:

slo run -p "Write a haiku about Python" \
  | jq -j 'select(.kind == "text_delta") | .delta'

Count tool calls by name:

slo run -p "Refactor utils.py" \
  | jq -r 'select(.kind == "tool_call_end") | .tool_name' \
  | sort | uniq -c | sort -rn

From Python:

import json, subprocess

proc = subprocess.Popen(
    ["slo", "run", "-p", "Analyse src/"],
    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"\nFinished: {event['kind']}")
proc.wait()

Exit codes

Last eventExit codeMeaning
run_completed0It worked.
run_failed1Something went wrong.
run_aborted2It was stopped: by SIGTERM, a timeout wrapper, /workflows abort, or its parent process ending.
slo run -p "Run the linter and fix what it finds" > lint.jsonl
case $? in
  0) echo "Done." ;;
  1) echo "Failed." ;;
  2) echo "Stopped." ;;
esac

stdout and stderr

Both are JSON lines, and you can read them separately:

  • stdout is the agent's work: text, tool calls, the final result.
  • stderr is the run's own lifecycle: when it started, a heartbeat every 15 seconds, how it ended.
slo run -p "Refactor utils.py" > output.jsonl 2> run.log

Flags you'll use

FlagDefaultWhat it does
-p, --promptrequiredThe task.
--modelyour saved choiceModel for this run, such as claude-opus-4-8.
--reasoningthe model's defaultoff, minimal, low, medium, high or xhigh.
--cwdcurrent folderFolder to work in.
--enable-historyoffSave the session so you can come back to it.
--resume <id or name>Continue a saved session. Turns history on.
--session-id <id>a new idChoose the session id.
--orchestratoroffAllow sub-agents, for dynamic workflows.
--append-system-prompt-fileAdd extra instructions for this run.

Every flag is in the CLI reference.

Slo as a server agent

On its own, each slo run is a one-off. Give several runs the same session id with history on, and they become one ongoing conversation. That's how you use Slo as a backend for a service.

Scope each request to a project

slo run -p "List every file with a TODO comment" --cwd ~/projects/my-api > todos.jsonl

Carry one conversation across steps

Set SLO_SESSION_ID once and every slo run in that shell shares the conversation:

export SLO_SESSION_ID="pr-$(git rev-parse --short HEAD)"

slo run --enable-history -p "What does this branch change?"          > changes.jsonl
slo run --enable-history -p "Any obvious bugs in those changes?"     > bugs.jsonl
slo run --enable-history -p "Write a PR description from the above." > pr.jsonl

The third call knows what the first two found, just as it would in the TUI.

Three headless steps sharing one session id.

Put a time limit on it

There's no timeout flag, so use timeout. When it sends SIGTERM, Slo stops cleanly and exits with 2:

timeout -k 15 300 slo run -p "Long analysis task" > out.jsonl

Ideas

  • Review in CI. Run slo run on every merge request and fail the job on the exit code or a fixed last line. See Put Slo on autopilot.
  • Batch jobs. Loop over repositories or files and keep each JSON log as a record.
  • A service backend. Shell out to slo run --cwd <repo> --enable-history --session-id <ticket> per request and stream the text_delta events back.
  • Scheduled tasks. The scheduler runs slo run on a timer for you.
  • Fan out. Add --orchestrator and Slo runs and supervises sub-agents itself.

Saved sessions

With history on, a session is saved under ~/.slo/sessions/history/<session-id>/:

├── session.json     id, name, model, folder, timestamps
├── events.jsonl     everything that happened, append-only
└── transcript.md    a readable copy

Continue it later by id or name:

slo run -p "Carry on with the refactoring" --resume pr-9f3a1c