Skip to content

CLI Reference

The CLI has two jobs: run agent work and operate the durable lifecycle around it. Start with li agent; move to fan-out or flow only when the work needs more than one worker.

Command map

Run work

Command Purpose
li agent [MODEL] PROMPT Run or resume one worker
li o fanout [MODEL] PROMPT Decompose into independent workers, optionally synthesize
li o flow [MODEL] PROMPT Plan and execute a dependency-aware, reactive graph
li play NAME [ARGS] Run a reusable playbook (li o flow -p NAME)
li engine run KIND SPEC Run a built-in coding, hypothesis, planning, research, or review engine

Observe and control

Command Purpose
li monitor / li mon List or watch sessions, invocations, plays, shows, and runs
li wait ID... Block until any mix of durable entity IDs reaches a terminal state
li monitor run ID... Wait for scheduled runs and their chains; optionally keep watching
li agent status [ID] Read stable session/invocation status, optionally as JSON
li o ctl {status,pause,resume,msg} Inspect or steer a live flow by ID
li kill ID Terminate one running entity or sweep stale processes; play kills cannot reach their workers and exit non-zero saying so; show ids are not directly killable (details)

Reuse, coordination, and operation

Command Purpose
li team {create,list,show,send,receive} Durable team inboxes across processes
li casts [NAME] Inspect built-in roles or modes
li skill {NAME,list,show} Read installed static skill instructions
li plugin {list,info,trust,enable,disable} Inspect and activate trusted plugin bundles
li hooks {import,trust} Import Claude Code / Codex hook configs and trust the imported commands
li invoke {start,end,list} Group sessions under one higher-level invocation
li studio [start] Start the Studio backend and selected frontend mode
li schedule {list,get,limits,create,enable,disable,trigger,delete,runs} Manage schedules through the Studio API
li state {import,import-teams,ls,stats,checkpoint,vacuum,prune,doctor} Inspect and maintain StateDB
li dispatch {ls,show,ack,retry,purge} Operate the durable dispatch outbox
li stats runs Aggregate run reporting from StateDB
li mirror Mirror Claude Code transcripts into StateDB/Studio
li mcp Serve an MCP server that submits li runs as background jobs (details)
li doctor Check installation, dependencies, Studio reachability, and writable state

play, skill, and wait are compatibility-friendly top-level conveniences handled before the normal argparse registry, so they may not appear in the command list printed by li --help. They are supported surfaces and are documented here.

Reusable definitions can be project-local, user-global, or supplied by a trusted plugin:

Primitive Location Invocation
Agent profile .lionagi/agents/<name>/<name>.md li agent -a <name> / li o flow -a <name>
Skill (static ref) ~/.lionagi/skills/<name>/SKILL.md li skill <name>
Playbook (parametric flow) .lionagi/playbooks/, ~/.lionagi/playbooks/, or a trusted plugin li play <name>
Plugin bundle .lionagi/plugins/<name>/plugin.yaml li plugin info <name>

See the repository examples for minimal templates of each.


Shared run flags

Available on li agent, li o fanout, li o flow. Source: cli/_providers.py

Flag Default Notes
--yolo false Auto-approve provider tool calls
--bypass false Bypass Codex approvals and sandboxing; intended for already-isolated environments
--fast false Use Codex priority service tier when the account supports it
-v, --verbose false Stream real-time output; suppresses final print
--theme {light,dark} none Terminal theme
--effort LEVEL none Override effort; provider-specific limits are normalized or clamped. Gemini CLI folds effort into its resolved model tier; direct gemini API has no effort setting
--cwd DIR none Working directory for CLI endpoint
--timeout SECONDS none Hard wall-clock timeout; partial branches saved. Injects a [DEADLINE] preamble into the agent's first message so it can pace itself
--invocation ID none Group the session under an ID from li invoke start
--project NAME auto Override project detection from config/git metadata

MCP servers are resolved once per launch, from the directory the command was run in: --mcp-config PATH names a file, --no-mcp-config states that there are none, and otherwise the nearest .mcp.json at or above that directory is used. On li o fanout and li o flow, every worker the launch builds is handed that one result, so no worker starts with servers the run did not resolve; li agent resolves the same way and warns when it hands a leg no servers because no config was found. A set read from a file named with --mcp-config is exclusive: the agent starts with those servers and no others. A set found by the .mcp.json lookup is additive and joins the servers the provider already configures.

Model spec: provider/model[-effort] — for example claude/opus-4-7-high or codex/gpt-5.4-xhigh. Current bare aliases include claude → claude_code/sonnet, codex → codex/gpt-5.3-codex-spark, gemini-code → gemini_code/gemini-4-argon, and pi → pi/gemini-2.5-flash. Use gemini, without -code, for the direct Google API provider rather than the Gemini CLI backend.


li agent

One-shot agent turn or resumed conversation.

li agent [model] prompt [flags]
Arg/Flag Default Notes
model — Spec or alias. Omit with -r or -c.
prompt — Message to send.
--prompt TEXT none Prompt flag alternative to positionals
--prompt-file PATH none Read the prompt from a file; - reads stdin
-a, --agent NAME none Profile by name. Resolves .lionagi/agents/<NAME>/<NAME>.md first, then legacy .lionagi/agents/<NAME>.md. Sets model/effort/system/yolo.
-r, --resume BRANCH_ID none Resume prior branch.
-c, --continue-last false Resume most recent branch.
--preset coding none Wire the coding toolkit, path guards, and coding prompt; cwd defaults to the invocation directory
--form SPEC none Validate a YAML/JSON work-form before making any model call, then inject its typed values
--context-from REF none Inject distilled context from a prior session id, branch id, run id, or file path into the new branch's first instruction (above the prompt). Repeatable — refs concatenate in argv order, sharing one budget. cli/_context_from.py
--context-budget N 8000 Total token budget (~4 chars/token) for --context-from content, shared across all refs.
--idle-timeout SECONDS LIONAGI_WORKER_IDLE_TIMEOUT (600) Stop the run as stalled (WorkerLivenessError) when the worker emits nothing for this long after it has started answering. 1 to 2**53 - 1; 0 is refused. A profile's idle_timeout sets it for a role, and the flag wins over the profile. Raise it for a model that thinks long between outputs. It watches a CLI worker's streamed output; an API-model leg's turn is bounded by --timeout instead.
--resume-on-timeout false Resume a timed-out agent session once with a bounded continuation
--workload light\|heavy profile's workload, else heavy How the leg counts against the machine-wide agent caps and how a CLI worker is confined; see Agent caps and workload.

-r and -c are mutually exclusive. --context-from is rejected together with -r / -c (resume already carries the source context). Common flags apply.

With persistence enabled, a fresh execution records its own generation before starting workers. Resuming a terminal branch uses its adopted persisted session and a new generation. A separate invocation cannot resume a session that is still running. --resume-on-timeout continues that same execution once: it keeps the session and generation, waits for the parent's owned work and database close, and starts a new diagnostic run directory through a single-use in-process handoff.

--context-from resolves the ref in order — session id, branch id, run id, then file path — erroring loudly on an unresolvable or ambiguous (2+ match) ref rather than spawning with silently-missing context. Distillation is mechanical (no LLM): a saved artifact/summary verbatim if it fits, else the initial instruction plus final assistant message, else a loudly-marked head/tail truncation.

li agent -a reviewer --bypass --context-from 20260420T110143-a1b2c3 --prompt-file review.md
li agent claude/sonnet "What does Branch.operate() do?"
# output:
Branch.operate() is the universal structured operation entry point...

[to resume] li agent -r 20260420T110143-a1b2c3 "..."

Python equivalent: branch.operate(instruction="...") → Branch API

Read status without starting a worker:

li agent status                         # latest agent-kind session in this project
li agent status SESSION_OR_INVOCATION   # full ID or unique prefix
li agent status SESSION_OR_INVOCATION --json

Agent caps and workload

Every CLI worker (codex, claude_code, gemini-cli, pi) takes a slot before its process starts, from li agent, li o fanout, li o flow or the Python API alike. Slots are lock files under $LIONAGI_HOME/leases/subagents/<pool>/, so every lionagi process sharing one LIONAGI_HOME counts against the same caps. With no cap set nothing is counted. ADR-0133 records the design.

Variable Caps Default
LIONAGI_MAX_SUBAGENTS every CLI worker unset (no cap)
LIONAGI_MAX_SUBAGENTS_HEAVY workers whose workload is heavy unset (no cap)
LIONAGI_MAX_SUBAGENTS_<ENGINE> one engine's workers: CODEX, CLAUDE, GEMINI, PI unset (no cap)
LIONAGI_SUBAGENT_ADMIT_WAIT_SECONDS how long a worker waits for a slot 600; 0 refuses at once

0, empty or unset means no cap. A value that is not a whole number refuses the worker rather than letting it start uncounted. A worker that waits out the whole window is refused with one line naming the pool, its cap and the pids holding it, and li agent exits 75 (EX_TEMPFAIL): the same command succeeds once a slot frees. The refusal is recorded on the run as SubagentAdmissionRefusedError, a retryable provider error. A pool stays full at the smallest cap its holders read, so a raised cap takes effect as the agents admitted under the lower one finish.

The slot belongs to the lionagi process that started the worker and is released when the worker's process group has ended, or when that process exits however it exits. A slot whose recorded worker is still running counts as held even if the process that took it is gone.

An agent lionagi started cannot start another agent through lionagi. Every CLI worker starts with LIONAGI_AGENT_DEPTH set over any value it would inherit: 1, or 0 for a li agent launch of a profile named in LIONAGI_SEAT_PROFILES. li agent, li o flow, li o fanout and every CLI worker spawn refuse at once, before taking a slot, in a process that inherited a depth of 1 or more or that runs under a process started with one. The second check covers tools an agent starts with an environment of its own choosing, such as the MCP servers codex starts with only the variables its config names. li o flow --background and the lion MCP server's agent, flow, fan-out and play submits check in the process that is asked, before anything detaches: a detached run checks again, but once its launcher has exited it no longer runs under the agent that asked. For the same reason li schedule create, enable, trigger and apply, the MCP server's schedule create, trigger and enable verbs, and li studio refuse inside an agent before the Studio daemon is asked or started, since the daemon starts its runs outside the agent's process tree. The refusal is NestedAgentRefusedError, a provider error that is not retryable; li agent and li o exit 1 with one line naming the process that carries the depth, and the MCP server answers with a conflict error and starts no job. A session nobody started through lionagi (a Claude Code or Codex session opened by hand) carries no depth and starts agents as before, and so does an agent launched with a profile named in LIONAGI_SEAT_PROFILES, which starts at depth 0. An agent such a profile starts waits for no slot its own ancestors hold: when they fill the pool, it is refused at once. The caps bound how many agents run; this bounds how deep they start each other, whether or not a cap is set.

--workload (or a profile's workload) sets the class:

Workload Counts against Worker confinement
heavy (default) the total cap, the heavy cap and its engine cap as configured (--yolo, --bypass, profile settings)
light the total cap and its engine cap codex: --sandbox workspace-write with no extra writable roots, no network, and the account's apps and plugins turned off, and settings that would lift the sandbox, account_connectors among them, are refused; the sandbox still keeps the temporary directories writable, and the MCP servers the leg is given run outside it; gemini: --sandbox; claude: runs without Edit, Write and NotebookEdit (Bash stays, and Claude Code has no filesystem sandbox, so this is a tool-list boundary only: a Bash command can still write anywhere); pi: refused, it has no sandbox lionagi can pin

A resumed leg (-r, -c) runs with the class this launch resolves: --workload, else the profile's, else heavy. Resuming a light leg without -a or --workload light therefore runs it heavy; pass the flag again to keep it light.

In li o flow and li o fanout each worker takes its own profile's class, whatever the orchestrator's. A light codex worker is given no extra writable directory, so it writes only its artifact directory, and under --team-mode it is refused before it starts. An independent agent a light worker asks for without naming an assignee runs on the orchestrator's engine with the worker's class; when that engine is another one, an API model included, the request is refused. --retry-failed refuses to run such an agent again, and a resume without the flag refuses every failed node, so a run in which one failed is re-run from scratch.

li monitor prints the held slots per pool under its table, for example subagents: all 3/30, heavy 1/6, codex 2/4, using the caps in its own environment, and li monitor --machine carries the same data under subagents.


li team

Persistent inbox messaging. Teams are stored at ~/.lionagi/teams/{team_id}.json under fcntl.flock.

li team create NAME -m MEMBERS
li team list     [alias: ls]
li team show TEAM
li team send CONTENT -t TEAM --to RECIPIENTS [--from NAME] [--from-op OP]
li team receive  -t TEAM [--as MEMBER]   [alias: recv]

li team create

Arg/Flag Required Notes
name yes Team name
-m, --members yes Comma-separated member names

Source: cli/team.py

li team create "docs-team" -m "researcher,writer,reviewer"
# output:
Created team 'docs-team' (7fa0d9abbf5b)
  Members: researcher, writer, reviewer
  File: ~/.lionagi/teams/7fa0d9abbf5b.json

list — sorted by mtime; shows ID, name, members, and message count. show TEAM — all messages with timestamps and read_by. TEAM = ID, prefix, or name.

li team send

Arg/Flag Required Default Notes
content yes — Message text (positional)
--team, -t yes — Team ID or name
--to yes — all or comma-separated names
--from no _cli Sender name
--from-op no none Op id; ties signal to a specific flow invocation

Source: cli/team.py

li team send "Research done — see research.md" \
  --team 7fa0d9abbf5b --to writer --from researcher --from-op o1

li team receive

Flag Required Default Notes
--team, -t yes — Team ID or name
--as no none Mark as read for this member; omit = see all

Source: cli/team.py

li team receive --team 7fa0d9abbf5b --as writer

Python equivalent: session.send() / session.receive() → api/team.md


li o fanout

Three-phase: orchestrator decomposes → N workers in parallel → optional synthesis.

li o fanout [model] prompt [flags]
Flag Default Notes
-a, --agent NAME none Orchestrator profile.
-n, --num-workers N 3 Maximum number of assignments generated
--workers M1,M2,... none Worker model pool, assigned round-robin; excess specs warn when the pool is larger than -n
--max-concurrent N 0 Max concurrent (0 = all)
--with-synthesis [MODEL] false Enable synthesis; bare = orchestrator model
--synthesis-prompt TEXT none Override synthesis instruction
--output {text,json} text Output format
--save DIR none Write artifacts here
--team-mode [NAME] none Create persistent team; bare = "fanout"

Source: cli/orchestrate/__init__.py. Common flags apply.

li o fanout claude/opus-high "Audit lionagi/session/ for stale API surface" \
  -n 3 --with-synthesis --save ./audit-out
# output:
Phase 1: Orchestrator decomposing task into 3 agent requests...
Phase 1 done (3.2s): 3 requests generated.
Phase 2: Fanning out to 3 workers: [claude/opus, claude/opus, claude/opus]
Phase 2 done (14.1s).
Saved 3 worker results to /path/to/audit-out
Phase 3: Synthesis [claude/opus]...
Saved to /path/to/audit-out

Worker outputs are worker_1.md … worker_N.md in the artifact root. Synthesis is written to synthesis.md. Resume cancelled workers with li agent -r BRANCH_ID.


li o flow

Auto-DAG pipeline. The orchestrator plans an initial list of TaskAssignment entries with assignees, dependencies, and execution modes; the engine executes them with dependency-aware parallelism. When reactivity is enabled, workers can emit SpawnRequest follow-up work without re-running the initial planner.

li o flow [model] prompt [flags]
Flag Default Notes
-a, --agent NAME none Orchestrator profile. Resolves .lionagi/agents/<NAME>/<NAME>.md first, then legacy .lionagi/agents/<NAME>.md.
-f, --file PATH none Load flow spec from YAML/JSON file. File values are defaults; CLI flags override.
-p, --playbook NAME none Resolve a project-local, user-global, or trusted-plugin playbook. Declared args are injected as additional flags.
--with-synthesis [MODEL] false Final synthesis after all ops
--max-concurrent N 0 Max concurrent agents per phase (0 = all)
--max-agents N 0 Shared cap for planned and reactive ops (0 = no shared ceiling; reactive spawns are capped at 20)
--dry-run false Plan DAG and print; no execution
--show-graph false Render DAG as matplotlib PNG into --save dir
--bare false Ignore agent profiles; all workers use CLI model
--background false Subprocess run; requires --save; writes <save>/flow.log and prints the monitorable session ID
--output {text,json} text Output format
--save DIR none Artifact dir; required for --background
--team-mode [NAME] none Create a FRESH team every invocation (new UUID). Bare = "flow".
--team-attach NAME none Upsert: attach to existing team by NAME (preserving message history) or create if missing. Mutex with --team-mode.
--team-max-rounds N 2 Extra reactive wake-up rounds for unread teammate messages after active workers finish
--workers M1,M2,... none Mixed worker model pool; preserves each role's profile and overrides model routing
--pack PATH none Per-role routing pack used when --workers is absent
--max-ops N 0 Shared cap for planned and reactive ops (0 = no shared ceiling; reactive spawns are capped at 20); --max-agents is deprecated
--reactive MODE all Roles allowed to emit SpawnRequest: all, off, or a comma-separated role list
--resume ID none Restart a checkpointed flow without re-planning; does not read other planning flags
--allow-degraded-context false Permit resumed inherited-context operations to run with empty predecessor history
--retry-failed false Re-run the ops a resumed checkpoint recorded as failed, instead of refusing; their reactive children from the superseded attempt are dropped
--notify CMD none Run a terminal callback template with status/invocation payload values

-f and -p are mutually exclusive. --team-mode and --team-attach are mutually exclusive. Source: cli/orchestrate/__init__.py. --background re-invokes python -m lionagi.cli without itself. Common flags apply.

Team lifecycle summary

Goal Flag Behavior
One-off parallel workers, no shared history --team-mode [NAME] New UUID every invocation. Messages posted; team discarded conceptually.
Persistent thread across invocations --team-attach NAME First call creates; subsequent calls attach to the same team (same UUID, same history). No pre-step required — you never have to li team create first.
li o flow claude/opus "Write and test a CLI arg parser for a new subcommand" \
  --save ./parser-work --with-synthesis

Use --dry-run to inspect assignments, dependencies, and resolved model/mode routing before running. Artifact directories are <save>/{agent_id}/. Python equivalent: Builder + Session.flow() → api/flow.md

Checkpoint resume and live control are intentionally separate:

li o flow --resume RUN_OR_SESSION_ID      # prior process ended; replay checkpoint
li o ctl resume RUN_OR_SESSION_ID         # process is alive but paused

li o ctl

Address read/control operations to a durable ID:

li o ctl status ID
li o ctl pause ID
li o ctl resume ID
li o ctl msg ID "Prioritize correctness over breadth"

status reads sessions, invocations, plays, and branch-backed sessions. pause, resume, and msg queue control for a running flow; msg is available for flows using context-mode operator steering. Use li o ctl SUBCOMMAND --help for the command-specific ID and JSON options.

Foreground flows print Session: <full ID> after recording the session, before planning. Background launch prints the full session ID assigned to its child. Control actions accept an exact recorded session, invocation, play or run ID, an unambiguous 8-character hex prefix, or an unambiguous prefix of a run ID (the ID li agent prints) that reaches the T after its date, such as 20260801T0201. Any other string is looked up as an exact id first; a string shorter than eight characters or carrying a non-hex character is treated as a prefix only when it reaches that T and begins a recorded run ID, and one that begins both a run ID and another ID is refused as ambiguous. A run-id prefix that stops short of the T, such as 2 or 2026, therefore cannot act on a run; an eight-character hex string is read as a printed id prefix, so 20260801 acts on the session whose printed id it begins, exactly as any other eight-hex prefix does — unless a recorded run id also begins with those eight digits, which makes the string ambiguous and refused. Shorter prefixes remain available to status, which does not act on a run. An exact stored ID need not be a UUID. --invocation must name an existing invocation registered with li invoke start; unknown values fail before flow setup/planning or background launch, rather than silently disabling persistence. Both li o flow and li o fanout check it before anything is spawned. The flag does not register an alias.

While a flow is waiting for DAG execution, controls are deferred until its DAG execution poller starts, including pre-planning, graph construction and resumed run windows. The poller checks about every two seconds during DAG execution; this is not a delivery deadline or a guarantee before the first operation. Controls will not be applied after the poller stops. Flows whose poller has stopped, and older flows without control-delivery tracking, refuse new controls. The stop marker is published before the poll loop stops, and enqueue re-checks it in the same statement that inserts the control, so a run that has lost its poller refuses rather than queueing against a consumer that is gone. A control admitted just before the marker lands can miss the last poll; when the run's teardown writes its terminal status, every control still queued and unclaimed is closed as rejected rather than left pending. Teardown also closes them if a later teardown step fails after that status write. That teardown path closes the flow/play queue: a process killed outright runs no teardown, and neither li kill nor a reaper sweeps the queue, so a control queued in that window stays pending and li o ctl status renders it as never landed. li o ctl resolve closes only a row a consumer claimed and never reported back. Marker writes are best-effort: a failed stop-marker write can leave a stale active marker, so enqueue is never a delivery guarantee. Check status for application.

Pause/resume acts at operation boundaries before the concurrency limiter. Operations already past the pause gate, including those waiting for limiter capacity, may still start; running operations are not interrupted. Messages affect only operations prepared after delivery and are rejected if none remain pending. Applied pause/resume writes paused/executing with one retry after a phase-write failure; repeated write failure can leave telemetry stale. The paths that end a flow run clear current_phase in the same statement as the terminal status; a legacy or out-of-band terminal row can keep a stored phase, and reads hide one on a terminal row. Flow runs are sessions: use li monitor --type session, or --type invocation for registered invocations.


Playbooks (-f, -p, li play)

A playbook is the declarative tier of orchestration: a reusable YAML input to the flow planner. It is not the plan itself. Each invocation combines the playbook with that run's input, the orchestrator produces a run-specific FlowPlan, and each plan assignment becomes a branch-bound FlowOp. Source: lionhub/src/lionhub/orchestration/patterns.py and cli/orchestrate/flow.py.

Bare names resolve project-local .lionagi/playbooks/ first, then user-global ~/.lionagi/playbooks/, then active trusted plugins. Use <plugin>/<name> to select a plugin playbook explicitly.

Playbook YAML shape

description: Parametric audit pattern    # shown by `li play NAME --help`
argument-hint: '[--mode MODE]'           # used only when `args:` is absent

model: claude-code/opus-4-7
agent: orchestrator
effort: high
max_ops: 6
reactive: "off"

args:                       # typed schema; becomes real CLI flags
  mode:
    type: str               # str | int | float | bool
    default: dry
    help: "audit mode: dry | security | dead-code"

prompt: |
  Run a {mode} audit of {input}. Cite source evidence for every finding.

A playbook carries two kinds of key, read by different code.

Runtime fields configure the flow: model, agent, effort, prompt, workers, max_ops, deprecated max_agents, with_synthesis, bare, dry_run, show_graph, save, team_mode, team_attach, reactive, and artifacts. _validate_spec_fields in cli/orchestrate/__init__.py checks the type and bounds of whichever of these are present. It is a type check, not a field list: it does not enumerate a closed set and does not reject unknown keys. See the orchestration guide for types, bounds, and runtime effects. Quote reactive: "off"; bare YAML off is a boolean, but the loader requires this field to be a string.

Declaration fields describe the playbook's own interface: description, args, and argument-hint. They are read on a separate path. args is checked by _validate_args_schema, which fails the run on a malformed schema, and is turned into parser flags by inject_playbook_schema_into_parser. When args is absent, argument-hint is parsed into the same schema shape by _parse_argument_hint. The discovered playbook name comes from the filename.

Template interpolation

Inside prompt:, three rules:

  1. {input} → the positional prompt text passed on the CLI.
  2. {arg_name} → a declared arg (CLI override beats playbook default).
  3. If the template has no {...} placeholders, the positional text is appended after a blank line.

Invocation

# Long form
li o flow -p audit --mode security "the auth service"

# Sugar
li play audit --mode security "the auth service"
li play list                        # list all discovered playbooks
li play audit --help                # show description, declared args, and usage
li play check audit                 # validate declared playbook artifacts/dependencies
li play status [ID]                 # latest play/flow status, or one durable ID
li play --resume ID                 # resume a checkpointed flow

li play list

Lists project-local, user-global, and active trusted-plugin playbooks. Plugin entries are namespaced as <plugin>/<name>.

li play list

li play NAME --help

Prints the playbook's description, a usage line built from argument-hint when one is declared, and an Arguments: block listing each declared arg with its type and default.

li play audit --help

--help is checked before any flags are forwarded to li o flow, so it never starts execution.

Submission is not validation

play.submit returns an allocated run ID after spawning the CLI child. The child then loads the playbook and calls _validate_spec_fields; a run ID is therefore a handle, not proof that the YAML is valid or that a result exists. Read it with job.status and job.output. Source: lionhub/src/lionhub/serve/mcp/jobs.py and cli/orchestrate/__init__.py.

Ad-hoc specs (-f)

For one-off specs not worth installing globally:

li o flow -f ./my-spec.yaml "target"

-f takes an absolute or relative path. -p takes a discovered bare name or an explicit <plugin>/<name> token. They are mutually exclusive.

The orchestration guide follows one playbook through its FlowPlan and a single FlowOp.


Skills (li skill)

A skill is static reference content the agent pulls on demand. Format is identical to Claude Code skills — you can symlink one source file into both ~/.claude/skills/<name>/SKILL.md and ~/.lionagi/skills/<name>/SKILL.md.

~/.lionagi/skills/commit/SKILL.md
---
name: commit
description: Conventional Commits style guide + safety rules.
---

# Commit conventions

... body ...

Commands

li skill NAME          # print body (post-frontmatter) to stdout
li skill list          # list installed skills
li skill show NAME     # print full file (frontmatter + body)

An orchestrator agent can shell out to li skill <name>, capture stdout, and inject the result into its own context — no extra protocol required.

See the skill examples for templates.


li monitor

Observe play/agent/run progress in real time. Replaces fragile file-polling and log-tailing with a single surface. Source: cli/monitor.py (add_monitor_subparser). Alias: li mon.

li monitor                      # table of all running entities
li monitor <id>                 # detail view for one run/play/agent/invocation
li monitor --watch              # live-refresh table
li monitor --watch <id>         # live-refresh detail view
li monitor --since 1h           # entities updated in the last hour, including terminal rows
li monitor --type session       # filter table by entity type
li monitor --type command --since 1h  # recent command actions, including failed fires
li monitor --project myproject  # filter sessions by project
Arg/Flag Default Notes
id none Entity ID or prefix; omit for the table view
-w, --watch false Live-refresh every --refresh seconds
--refresh SECS 2 Refresh interval for --watch
--since WINDOW active entities only Time window: 30m, 1h, 2d; includes terminal rows updated in the window
-t, --type none One of session, invocation, show, play, command. command narrows to invocations launched as a command action
-p, --project none Filter sessions by project name

Flow runs are sessions: use --type session, even when the table's type column labels the row flow. Use --type invocation for records registered through li invoke start. flow is not a supported --type value.

A session detail view includes a khive injection block when an opted-in profile recorded non-zero recall, injection, failure, degradation, or writeback counters. degraded_turns distinguishes a provider-contained transport failure from a successful recall that returned no context. The block contains aggregate counts, not injected source text.

For scripts, use a waiter instead of scraping the watch display:

li wait SESSION_ID PLAY_ID                 # any durable entity kinds; mixed IDs allowed
li wait ID_A,ID_B --interval 2

li monitor run SCHEDULE_RUN_ID             # follows on_success/on_fail children by default
li monitor run SCHEDULE_RUN_ID --no-chain  # wait for only the literal ID
li monitor run SCHEDULE_RUN_ID --follow    # keep watching for later schedule runs
li monitor run SCHEDULE_RUN_ID --max-wait 0

li wait accepts run, session, play, flow-invocation, and scheduled-run IDs or unique prefixes. li monitor run follows a watched run's scheduler chain by default; --no-chain disables that behavior. After the initial set drains, --follow keeps the monitor open and prints newly created schedule runs. The initial wait defaults to a bounded 900 seconds.


li kill

Terminate a running entity by id, or sweep stale entities whose OS process is already dead. Source: cli/kill.py (add_kill_subparser).

li kill abc123                        # kill by id prefix
li kill <session-id>                  # stop a worker process
li kill abc123 --reason 'stuck'
li kill abc123 --recursive            # kill + direct children (session -> invocation)
li kill --all-stale                   # sweep dead-PID sessions/invocations
li kill --all-stale --threshold 3600  # only rows older than 1h
li kill --all-stale --dry-run
Arg/Flag Default Notes
id none Entity ID or prefix: run/session/invocation/play
--reason "" Recorded in status_transitions
--recursive false Also kill direct child entities
--all-stale false Sweep stale sessions/invocations (and their child-derived plays/shows)
--threshold SECS 3600 Only sweep entities started more than this long ago
--dry-run false Only valid with --all-stale; prints without cancelling
--grace SECS 5.0 Wait after SIGTERM before escalating to SIGKILL

--recursive scope boundary. Recursion only reaches PID-bearing workers, and an orchestrator row reaches them only through a link it recorded:

  • Killing a play reaches its worker chain only if the play row records the session it started, in plays.session_id. One path binds that column: the Studio show importer, which resolves the session by name when it mirrors a show directory. A play created by a live run leaves it unset, and a worker session stores no play reference either, so there is no key to resolve those workers by. In that case the kill marks the play row blocked, prints an error saying no worker was stopped, and exits 1 — a play kill never reports success it did not achieve. Kill the worker session ids directly (li monitor lists them). --recursive is not needed for either case: a play row carries no PID of its own, so resolving its workers is the kill.
  • Killing a session with --recursive also cancels its linked invocation.
  • A show id cannot be killed directly today: only running rows are killable, and show rows persist as active (never running), so li kill <show-id> is rejected as already-terminal, with or without --recursive.

To stop everything under a show, kill the play id or session id directly (li monitor <show-id> lists its plays). --all-stale covers the abandoned case only as far as the recorded links allow: a play older than --threshold whose recorded worker session has gone terminal is marked blocked; a play that records no worker session is left alone, because age by itself cannot tell an abandoned play from one still doing hours of work. The sweep prints one line naming how many rows it skipped for that reason, and reports them as skipped_unlinked_plays in its closing counts. A show row is marked aborted only once it is older than --threshold and all of its plays are terminal.


li invoke

Group the sessions a skill spawns (e.g. /show, /codex-pr-review) into one parent invocation record, so the runs list and Studio dashboard collapse "14 sessions" into a single row. Opt-in — sessions spawned without --invocation behave exactly as before. See the CLI internals. Source: cli/invoke.py.

INV=$(li invoke start --skill show --prompt "resolve lionagi issues")
li play backend  ... --invocation "$INV"
li play frontend ... --invocation "$INV"
li invoke end "$INV" --status completed
Subcommand Flags Notes
start --skill (required), --plugin, --prompt, --metadata Opens an invocation; prints its id to stdout
end ID --status (default completed), --metadata Closes it with a canonical terminal status
list --skill, --status, --limit (default 20) Lists recent invocations

end closes a running invocation, and it records --metadata on a row an earlier close already put in the requested status: that same-status close merges the payload without moving the stored status or end time, and exits 0; the same close with no --metadata also exits 0, since there is nothing left to record. --metadata merges key by key into the invocation's stored metadata, and the merge runs inside the store's own write, so a payload another closer records in between is merged with rather than overwritten and both closes keep their keys. Keys merge per RFC 7396: a nested object merges into the stored object, a key set to null is removed, and an array or scalar replaces the stored value. A stored value that is not an object (an old row whose metadata is a bare string, for instance) is kept under _discarded_node_metadata. An end that names a different status than the stored one — the row was cancelled, reaped, or closed by a sibling launch while the work ran — records neither the requested --status nor its --metadata. That refusal is reported on stderr and exits 1, and the stored status another writer recorded is left exactly as it is. The status write and the metadata merge are separate writes in that order, so a reader can see the terminal status a moment before the closer's metadata lands. Treat a zero exit as proof the status and metadata were saved.


li hooks

Import an existing Claude Code or Codex hooks configuration into this project's .lionagi/settings.yaml hooks_external: block, then record trust for the imported commands so they are allowed to execute. Trust is hash-pinned: approval is recorded against the content-hashed argv, so a command that changes after import must be re-approved before it runs. Source: cli/hooks.py.

li hooks import claude                   # reads .claude/settings.json
li hooks import codex .codex/hooks.json  # explicit config path
li hooks trust                           # review and approve pending commands
li hooks trust --yes                     # record trust without the prompt
Subcommand Flags Notes
import SOURCE [PATH] --cwd SOURCE is claude or codex; PATH defaults to .claude/settings.json or .codex/hooks.json
trust --cwd, --yes Lists pending imported hook commands and records approval (content-hashed argv)

li mcp

Serve an MCP server over stdio. The server is a control plane over this CLI: it submits li runs as detached background jobs and answers questions about them, so a submit returns a run_id immediately instead of blocking and the run keeps going in its own process group, surviving an MCP-server restart. Requires the mcp extra (pip install 'lionagi[mcp]'). Source: lionhub/src/lionhub/serve/mcp/.

li mcp            # serve over stdio (same as: python -m lionhub.serve.mcp)

The server advertises a single tool, request, and every operation is a namespaced verb passed to it. Seeing one entry in a client's tools/list is correct. Call request with help=true for the catalog of verbs.

Register it with any MCP client (e.g. an .mcp.json). The key here is the local name your client uses to launch the server; the name the server reports over the protocol is lion:

{
  "mcpServers": {
    "lion": { "command": "li", "args": ["mcp"] }
  }
}

Job records live under ~/.lionagi/mcp/jobs/<run_id>/; the authoritative run state is the CLI's own ~/.lionagi/runs/<run_id>/. In job.status, the top-level status is authoritative: the embedded run manifest is advisory and its own status may lag.

The verb catalog, the request result contract, and a worked submit-and-poll example are in the MCP server reference.

When a run's process is gone and nothing recorded how it came out

A background run normally records its own end: the CLI's terminal hook writes it, and a run stopped by li kill leaves it in the lifecycle store, which the server caches onto the job record. A run whose process dies before either of those happens leaves nothing behind at all — no surviving producer can ever write its end.

Where an observation positively establishes that the run's process is gone — the recorded pid holds no process, it disappears between two probes, or a live process holds the number and started at a different time, so it is a different process — job.status, job.list and job.wait record that end themselves and then report it:

Field Value
terminal true
outcome indeterminate
reason_code process_gone_without_outcome
reason_detail terminal_write_lost, persistence_degraded, signal_persistence_degraded or null
terminal_source mcp_orphan_reaper

outcome: "indeterminate" means the process is conclusively gone and no authoritative outcome was reported. It does not mean the work failed: the run may well have finished what it was doing before it died, and nothing survived to say either way. failed stays reserved for a reported terminal status classified as a failure, and a caller may retry a failed run under its own policy. Do not automatically retry such a run — an external side effect it never got to report may already have committed.

terminal_source says what wrote the end: cli_terminal_hook (the run's own terminal hook), lifecycle_cache (an end read back from the lifecycle store), spawn_failure (the spawn was caught failing), mcp_kill (the run was killed through this server), or mcp_orphan_reaper (this server, from the conclusive observation above). It is null on records written before the field existed.

For a run ended this way, finished_at is when the loss was established and recorded, not when the process exited — nothing surviving can report that instant. Any duration derived from it is therefore an upper bound on how long the run actually ran.

liveness_conclusion on job.status says what the observation established: process_gone, alive, or unknown. Only process_gone can end a run. unknown — a pid the OS cannot be asked about, a denied or unreadable identity probe — never does, and such a run stays non-terminal and advisory (possibly_orphaned), reported by job.wait under stopped_without_end.

job.wait's all_terminal means every valid requested run has a recorded end, including runs whose outcome is indeterminate. It does not mean every run succeeded or reported an outcome; read each entry's outcome for that.

Terminal notices

When a background run finishes, the server records its terminal status and, if a delivery command is configured, runs it — on every terminal state, including failure and kill. Nothing is configured out of the box, so the default is no notice. The command is an argv list run directly (never through a shell); the placeholders {run_id}, {status}, {label}, and {target} are substituted into its arguments, and the same fields are offered as a JSON object on stdin. {sender} names the actor signing the notice, {reason_code} carries the run's terminal reason when one was recorded, and {reason_detail} its finer detail; a reason placeholder with no value becomes empty.

Configure the command once via lionagi's own notify.on_terminal setting (~/.lionagi/settings.yaml or a project .lionagi/settings.yaml):

notify:
  on_terminal:
    adapter:
      kind: exec
      argv: ["/usr/local/bin/notify-run", "{run_id}", "{status}", "{target}"]

Or per submit: notify overrides the delivery command for one run (a JSON argv list), notify_seat fills the {target} placeholder, and notify_sender fills {sender}, naming the actor signing the notice. A submission that names either but has no delivery command from notify, the environment or the settings still runs, and its submit handle carries a notify_warning saying nothing will be sent. By default an explicit sender is checked against the submitting directory, as before. A deployment that wants callers to attest from their run directories sets LIONAGI_MCP_SENDER_ROOTS to an OS-path-separated list of absolute paths where the server is declared; then a declared sender is checked against the run directory the submission names in cwd, which must sit under one of those roots and hold .khive/config.toml. A submission that names no cwd is checked against the submitting directory, as before. The notice is delivered from the directory that was checked, so the identity it claims is the one it is signed with. The environment variables LIONAGI_MCP_NOTIFY_COMMAND, LIONAGI_MCP_NOTIFY_TARGET and LIONAGI_MCP_NOTIFY_SENDER set a process-wide default, and LIONAGI_MCP_NOTIFY_CWD names the directory delivery runs in instead of the checked (or submitting) one. These are the server operator's settings and are used as given: the sender check applies only to a notify_sender passed on the submit. With no sender from either, a delivery command that takes {sender} is not run, and one that does not reports whatever identity it resolves from the directory it runs in. Delivery outcome is recorded on the job and surfaced in job.status (notify_delivery), so a notice that failed to send is visible rather than silently lost. job.list carries the same outcome collapsed to one word in notify_delivery_state — delivered, delivered_unverified when it exited zero but that command shape's zero exit doesn't prove a send, failed when the notifier reported its own failure, unknown when an attempt's final result was interrupted or it was stopped for running past its deadline, or none when no notifier was configured — so a run whose notice never went out is spotted while scanning runs, not only when one is looked up. The full job.status.notify_delivery object is null only for a non-terminal run.

Spawning and the MCP server set

The four spawn verbs (agent.submit, flow.submit, fanout.submit, play.submit) accept exclude_mcp_servers, a list of server names to remove from the MCP config the submission resolves before its immutable snapshot is written. Every name must exist in that config — a name it does not declare is refused rather than silently ignored — and the parameter cannot be combined with mcp_config or no_mcp_config, since neither leaves a resolved set to filter. The removed names are reported as excluded_mcp_servers on the handle, the job record, and job.status.

A run is write-capable when its agent.submit arguments or its agent profile set yolo or bypass, or when it resumes or continues a branch, which reloads the provider settings that branch kept. Every flow.submit, fanout.submit and play.submit run is write-capable too, because it casts its workers onto profiles while it runs. By default a write-capable run gets no separate khive actor, and a submission without cwd is accepted as before. A deployment that wants each writable run to have its own identity sets LIONAGI_MCP_CHILD_ACTOR_SUFFIX to a non-empty suffix. Then a write-capable run submitted with a cwd gets the actor <submitting actor><suffix> (for example, with a suffix of :child, <submitting actor>:child), minted into <cwd>/.khive/config.toml before the run starts, so a writable run does not sign as the submitting identity. The mint copies the submitting directory's config and rewrites only its [actor] table; it refuses when the run has no own directory, when the submitting directory has no actor config, or when the run directory already holds a config other than exactly the one it would mint; a config that is exactly that one is reused as it is, and one minted before the submitting config changed is refused until it is removed. job.status reports the result as child_actor, child_actor_config and submitting_actor.


li handshake

Report what this installation is, for a program deciding whether it can talk to it. Pair it with --machine, since a program is the only caller this is for.

li handshake --machine
{"ok": true, "contract_version": 1,
 "data": {"contract_version": 1, "min_supported_version": 1,
          "implementation": "lionagi", "implementation_version": "0.30.2",
          "module": "/path/to/lionagi/cli"},
 "error": null}

contract_version is the machine-result contract this build speaks and min_supported_version is the oldest it still accepts. A caller checks both once at startup, then validates contract_version on every envelope afterwards — the binary at a pinned path is replaced during normal operation, so a handshake governs registration and never stands in for per-response checking.

module is where the code being served actually lives, which answers "is this the checkout I think it is" without guessing from a version number.


li runs

List the runs recorded on disk and what each one wrote.

li runs --machine [--limit N]

Each entry carries the run id, its state root, its artifact root, and the artifacts found there. It reports which runs EXIST and what they left behind, not whether any of them finished or succeeded — for that, ask li job status or the MCP request operation job.status, which carry the terminal and outcome derivations.

The artifact list is wrapped in the availability shape, so a directory that could not be read is reported as unavailable with a reason rather than as a run that produced nothing.


li lifecycle

Report what the lifecycle store records about one CLI run.

li lifecycle <run-id> --machine

This is the one path from a run id to the rows the lifecycle writers actually write. A normal teardown records an end; so does li kill, which writes the row and signals the process without touching the MCP job record or the run manifest. A caller holding only a run id and reading only those two would see a dead process with no recorded end, which is what an orphaned run looks like.

The answer is read-only and carries its own availability. An established answer with found: false means no session was ever recorded under this id. An unavailable one means the store could not be read at all, which is not a statement about the run. A caller that collapsed the two would report a run as finished, or as never started, on the strength of a database it never opened.

The store consulted is the one LIONAGI_STATE_DB_URL names when it is set, and the default otherwise — including for the question of whether a store exists at all, so a configured store is never reported missing because the default path is absent.


Machine mode: --machine

Any command that reaches the dispatcher accepts --machine, which turns its output into exactly one JSON object on stdout:

{"ok": true, "contract_version": 1, "data": {...}, "error": null}

Exactly one of data and error is present, and error.kind is a closed set a caller may branch on. Diagnostics, progress and warnings go to stderr, so stdout carries the object and nothing else — a caller can parse it without scanning for where the JSON starts.

Anything derived from a read that can fail is wrapped rather than flattened:

{"available": false, "value": null, "reason_code": "unreadable",
 "detail": "permission denied"}

This keeps "there are no artifacts" and "the artifacts directory could not be read" from sharing an encoding, which is the difference between a caller reporting an empty result and reporting a broken one.

Check the exit status before parsing. 78 means nothing executed — the environment could not run the work at all — and stdout must not be parsed on it, because attributing an environment fault to the submitted work is the misattribution the code exists to prevent.

The full contract, including how status, terminal and outcome divide the question of whether a run is over, is in ADR-0106.


li engine run

Run a domain-specific multi-agent engine pipeline without writing Python. Progress events stream to stderr; the final result is emitted as JSON on stdout for piping. Run records persist in the StateDB engine_runs table. Source: cli/engine.py. Provider workers belong to the engine's persisted signal-session row; a supplied --session-id remains a parent association. Owned worker work drains before the engine/session is terminalized and its database closes.

li engine run research 'What are the latest advances in GQA?'
li engine run review   'See artifact.py' --model claude/sonnet
li engine run coding   'Implement a BFS traversal' --test-cmd 'pytest'
li engine run hypothesis 'Finding: X causes Y' --export-dir ./out
li engine run planning 'Build a REST API'
Arg/Flag Default Notes
kind — Engine kind (e.g. research, review, coding, hypothesis, planning)
spec — Main input: topic / artifact / spec / findings / prompt
--test-cmd CMD none Validation command; required for the coding kind
--export-dir DIR none Output directory (coding, hypothesis)
--model MODEL default Provider/model override
--max-depth N kind default Max recursion/expansion depth
--max-agents N none Cap on spawned sub-agents
--session-id ID none Associate with an existing StateDB session
--no-persist false Skip writing the run record to StateDB

Agent profile layout

A profile is resolved by name. Two layouts are supported:

~/.lionagi/agents/
    orchestrator/                      # preferred — directory layout
        orchestrator.md                # main profile
        patterns/                      # optional supplementary references
            empaco.md
        refs/
            commit-conventions.md
    legacy.md                          # flat layout — backward compat

li agent -a NAME and li o flow -a NAME check for <NAME>/<NAME>.md first and fall back to <NAME>.md. Supplementary files beside the main profile are not injected into the initial system prompt — the agent reads them on demand (via direct file reads or li skill).

Project-local .lionagi/agents/ takes precedence over ~/.lionagi/agents/.

See the agent examples for minimal/ and with-refs/ templates.

Profile format

A profile is YAML frontmatter followed by a markdown body (the system prompt). Source: cli/_agents.py (AgentProfile).

---
model: claude_code/opus
effort: high
yolo: true
---

You are an implementer. Write production code, not stubs...

All frontmatter fields are optional; matching CLI flags override them at invocation.

Field Notes
model Provider/model spec (e.g. claude_code/opus, codex/gpt-5.4-xhigh)
effort Reasoning effort level (e.g. high, xhigh)
yolo Auto-approve tool calls
fast_mode Route via the OpenAI priority tier (codex only)
contain_account_integrations Codex only. true turns the signed-in account's apps and plugins features off for this role (--disable apps --disable plugins). Default false
account_connectors Codex only. List of the account's app connectors the role is for. Naming any leaves both features as the account and config set them, even when contained. Never written into run records or branch snapshots
contain_user_settings Claude only, for this role's workers in li o flow and li o fanout, and for every Claude agent such a worker spawns, whatever role it names: a codex one runs with --ignore-user-config, as every codex worker does, under its own role's sandbox and approval settings, and a spawn on any other CLI is refused. true has the CLI load no settings files for the worker (--setting-sources ""): your user, project and local settings do not apply to it, so your own hooks, PreToolUse guards included, and your permission rules stop gating that worker, and its plugins, subagent definitions and slash commands are gone too. The files stay on disk, where the worker's own Read and Bash tools can still open them. In their place the worker gets the built-in tools Read, Glob, Grep, Edit, Write, Bash, WebFetch and WebSearch and only the MCP servers the run hands it, none when the run resolves no set, runs those without asking (--permission-mode dontAsk, which also replaces yolo), and cannot start Agent or Task subagents. The flag is read from the block's top-level keys as YAML parses them, its key matched as written, so a key nested under another setting, or spelled in another letter case or with dashes, is not the flag, and when no top-level key sets the flag such a key warns that it does not: an indented block, a quoted or ? key and a << merge key all count, an explicit key wins over a merged one, and a key given more than once, of which YAML keeps the last, warns and leaves the flag off only when every one reads false. A value other than true or false warns and contains: an empty one, null, 0 and the YAML 1.1 words off, no, yes and on included. A profile whose frontmatter does not read as YAML warns that every setting in it is ignored, and contains when the block names contain_user_settings anywhere; a block with no closing --- runs to the end of the file. A block with no settings in it, empty or comments only, neither warns nor contains, even when a comment names the flag. A byte-order mark or another invisible format character ahead of the frontmatter does not hide it. Any other character on the delimiter's line hides the block and every setting in it, with a warning when the block reads as settings, names the flag, or does not read and ends at a closing ---. Such a block contains when it sets the flag, whatever value it gives, or names it anywhere while reading as a list or a line of text or not reading at all, as a frontmatter that does not read does: a block that opens the profile was written as its frontmatter and none of its settings apply, so the worker is contained rather than run on your settings, and your own hooks and permission rules do not gate it until the profile is fixed. Text on lines of its own ahead of the first line that is exactly --- leaves the profile all prompt, as before, and nothing after it applies, the flag included, so its worker runs on your settings even where a block after that text sets the flag to true: containing it would drop your hooks and permission rules for a profile that earlier releases ran as a prompt, on a block that is not read. Every block after that text, closed or not, that has a line setting the flag as a key, reads as settings that set it, or does not read and names it, warns that its worker is not contained; a block there runs from a line of three or more dashes to the next such line. Containment is unchanged for li agent -a, which prints the same profile warnings. Default false
timeout Default hard timeout in seconds
idle_timeout Seconds of silence from an answering worker before the run counts as stalled; defaults to LIONAGI_WORKER_IDLE_TIMEOUT (600). --idle-timeout overrides it
workload light or heavy (default). Sets how the role's CLI workers count against the agent caps and how they are confined; see Agent caps and workload. Any other value reads as heavy with a warning. --workload overrides it, and orchestration workers take their own profile's value
resume_on_timeout Set to once for one bounded automatic continuation
lion_system Prepend LION_SYSTEM_MESSAGE to the body (default: true)
artifact_defaults Expected-artifact defaults; see ADR-0064

When lion_system: true, the global Lion system preamble is prepended to the body to form the system prompt. Set it to false for a verbatim body (e.g. when the profile already carries its own complete system prompt).


Run-ID and persistence

Task-producing agent, fan-out, flow, and playbook invocations allocate a run directory. Administrative commands such as doctor and monitor do not. Run IDs use the format YYYYMMDDTHHMMSS-{6hex}. Source: cli/_runs.py.

~/.lionagi/runs/{run_id}/
  run.json                        manifest (command, branches, artifact_root)
  branches/{branch_id}.json       branch snapshot — resumable via -r / -c
  stream/{branch_id}.buffer.jsonl live chunk buffer during streaming
  artifacts/                      deliverables — only when --save was NOT given

Authoritative state always lives under ~/.lionagi/runs/{run_id}/, so any branch is resumable from anywhere. User-facing artifacts (per-agent working dirs, synthesis.md, flow.log, flow_dag.png) land in the --save directory when one is provided, otherwise in artifacts/ under the run dir. The --save directory is not authoritative state — deleting it does not break -r.

Pre-run-scoped sessions (legacy ~/.lionagi/logs/agents/{provider}/{branch_id}) are still read as a fallback on resume.

Resume any prior branch:

li agent -r 20260420T110143-a1b2c3 "follow up"
li agent -c "continue most recent"

Env Vars

Variable Purpose Source
LIONAGI_RUN_ID When explicitly set for a task-producing child process, reuse the supplied run ID cli/_runs.py
LIONAGI_HOME Override ~/.lionagi/ base dir lionagi/utils.py
LIONAGI_WORKER_LIVENESS_TIMEOUT Seconds run() waits for a CLI worker's first stream chunk before retrying once, then raising WorkerLivenessError; default 120, 0 disables. Applied by default only to endpoints that stream output early (claude_code, codex) — buffered endpoints (gemini-cli, pi) are unaffected unless liveness_timeout is passed explicitly to run() lionagi/operations/run/run.py
LIONAGI_WORKER_IDLE_TIMEOUT Maximum silence between chunks from an early-streaming CLI worker; resets per chunk, defaults to 600, and 0 disables. A miss after partial output raises WorkerLivenessError (worker.stream_idle) without retry. Buffered endpoints are unaffected unless idle_timeout is passed explicitly to run() lionagi/operations/run/run.py
LIONAGI_MAX_SUBAGENTS, LIONAGI_MAX_SUBAGENTS_HEAVY, LIONAGI_MAX_SUBAGENTS_<ENGINE> Machine-wide caps on CLI workers; unset or 0 means no cap. See Agent caps and workload lionagi/providers/_subagent_admission.py
LIONAGI_SUBAGENT_ADMIT_WAIT_SECONDS How long a CLI worker waits for a slot before it is refused; default 600, 0 refuses at once lionagi/providers/_subagent_admission.py
OPENAI_API_KEY OpenAI REST API key (for iModel, not for codex CLI alias) lionagi/config.py
ANTHROPIC_API_KEY Anthropic REST API key (for iModel; claude alias uses claude login instead) lionagi/config.py
GEMINI_API_KEY Gemini API key (gemini provider, not gemini-code CLI auth) lionagi/config.py
GROQ_API_KEY Groq key lionagi/config.py

Sources: cli/agent.py · cli/team.py · cli/orchestrate/__init__.py · cli/orchestrate/fanout.py · cli/orchestrate/flow.py · cli/_providers.py · cli/_runs.py

Next: Python API reference