Skip to content

From Playbook to FlowOp

LionAGI exposes three related orchestration tiers. They are not three names for the same object:

Tier Question it answers Lifetime
Playbook What reusable defaults and prompt should this kind of run start from? A declarative .playbook.yaml file
FlowPlan What work and dependencies should this particular input produce? The planner's list[TaskAssignment] for one run
FlowOp What does one branch do next? One operation node in the live run graph

The code does not define a FlowPlan container class. The runtime representation is a list of TaskAssignment values with task, assignee, inputs, exit_criteria, depends_on, and modes. “FlowPlan” names that run-specific planning tier. Source: lionagi/orchestration/patterns.py and lionagi/casts/emission.py.

flowchart LR
    PB[Playbook + run input] --> Planner
    Planner --> FP[FlowPlan: TaskAssignments + dependencies]
    FP --> A[FlowOp on branch A]
    FP --> B[FlowOp on branch B]
    A --> C[FlowOp on branch C]
    A -. SpawnRequest when reactive .-> S[New FlowOp]

The playbook does not contain the DAG. The orchestrator model creates the FlowPlan from the playbook's prompt and the current input. The builder then turns every assignment into an operate node bound to a worker branch and wires its declared dependencies. With reactivity enabled, accepted SpawnRequest values add operations to that live graph without rerunning the initial planner. Source: lionagi/cli/orchestrate/flow.py (_run_flow_inner, _build_dag, and _execute_dag) and lionagi/orchestration/patterns.py (role_node_builder).

A complete example

Save this as .lionagi/playbooks/repo-audit.playbook.yaml:

model: codex
prompt: |
  Audit {input}. Inspect its public API, identify the highest-impact gap,
  and verify the final finding against the source.
max_ops: 4
reactive: "off"
with_synthesis: true
artifacts:
  expected:
    - id: audit-report
      path: synthesis.md
      required: true
      description: Final evidence-backed audit

Then preview and run it:

li play repo-audit . --dry-run
li play repo-audit . --save ./lion-results/repo-audit

The filename supplies the discovered playbook name. {input} receives the positional .. The actual plan is model-produced, but a valid FlowPlan for this input could have this shape:

assignments:
  - task: Inspect the public API and record evidence for gaps.
    assignee: researcher
    inputs: []
    exit_criteria: Every claimed gap cites source evidence.
    depends_on: []
    modes: []
  - task: Rank the documented gaps and verify the highest-impact finding.
    assignee: analyst
    inputs: [inspection findings]
    exit_criteria: One finding is selected and independently verified.
    depends_on: ["1"]
    modes: [evidential]

For the second assignment, one FlowOp is an operate invocation on the analyst's branch. Its instruction is the assignment's task; its incoming graph edge points to step 1; its context includes the original task and artifact locations. That node invocation—not the whole branch and not the whole plan—is the FlowOp. Source: lionagi/cli/orchestrate/_common.py (_build_worker_operate_node) and lionagi/cli/orchestrate/flow.py (_build_dag).

At present, inputs and exit_criteria remain plan metadata: _build_dag does not copy either field into the FlowOp instruction or context. Authors should put execution-critical constraints in the playbook prompt rather than assuming those two planner fields reach the worker. Source: lionagi/cli/orchestrate/flow.py.

--dry-run displays the planner's declared assignments and dependencies but does not build the run graph. --show-graph writes the graph only during post-execution finalization. Source: lionagi/cli/orchestrate/flow.py.

Choose the lightest surface

Need Surface What it adds
Independent perspectives or repeated copies of one task li o fanout / fanout.submit A bounded decomposition followed by dependency-free parallel nodes
Work whose later steps consume earlier results li o flow / flow.submit A run-specific dependency graph and optional reactive expansion
The same flow prompt and defaults reused by name li play / play.submit A saved declaration that still enters the normal planning path on every run

Prefer fan-out when the work is genuinely independent. In fanout.py, every worker node is built with depends_on=[], while flow.py normalizes and wires the planner's dependency references. A playbook improves reuse; it does not freeze a FlowPlan or remove the planning turn. li play is rewritten to li o flow -p NAME by lionagi/cli/main.py.

Recorded runs show flat fan-outs completing far more reliably than saved playbook runs. The dominant saved-playbook failure is a FlowPlan too large for its execution window. The mechanics make that risk concrete: the flow timeout covers planning, execution, and synthesis, and the worker budget is divided by the initial assignment count. Cap and preview planned flows; do not choose a playbook merely because the task sounds important. Source: lionagi/cli/orchestrate/flow.py (_run_flow and _run_flow_inner).

Reactive runs need spare capacity

reactive defaults to all. It can be off or a comma-separated role allowlist. An allowed worker may emit a SpawnRequest; the executor converts an accepted request into another FlowOp. Source: lionagi/cli/orchestrate/flow.py (_parse_reactive) and lionagi/casts/emission.py (SpawnRequest).

For a positive max_ops ceiling, the fresh-run arithmetic is:

spawn capacity = max(0, max_ops - initial planned assignments)

A four-assignment plan under max_ops: 4 therefore has zero spawn capacity, so its workers are not granted the spawn tool. The reactive state and effective max_spawn are recorded in the flow checkpoint and Studio run metadata. Size the ceiling as:

max_ops = intended initial plan + intended reactive spawns

For example, budget six operations for an expected four-node plan plus two possible follow-ups. With no explicit ceiling (max_ops: 0), initial planning is uncapped by this setting but the executor still limits reactive spawns to 20. Restored spawns consume the same budget on resume. Source: lionagi/cli/orchestrate/flow.py (_execute_dag).

Quote the disabled value in YAML:

reactive: "off"

PyYAML parses bare off as boolean false, while the playbook loader requires reactive to be a string. Because validation occurs in the spawned CLI child, an MCP play.submit can return a run ID before that type error ends the run. Source: lionagi/cli/orchestrate/__init__.py, lionagi/mcp/dispatch.py, and lionagi/mcp/jobs.py.

Playbook field reference

The runtime fields below are the ones _validate_spec_fields in lionagi/cli/orchestrate/__init__.py type-checks when they are present. That function is a type and bounds check, not a field list: it enumerates no closed set and rejects no unknown key, so its absence from that function does not make a key inert. The declaration fields are listed separately after the table because a different code path reads them. CLI flags override file defaults.

Field Accepted value Runtime effect
model string Default orchestrator model
agent string Default orchestrator profile
effort none, minimal, low, medium, high, xhigh, max, or ultra Default reasoning effort
prompt string Prompt template; {input} is replaced by positional input
workers integer 1–32 Maximum concurrently running planned operations when the CLI concurrency flag is unset
max_ops integer 0–50 Preferred total-operation ceiling; 0 leaves initial planning uncapped but retains the default reactive-spawn limit
max_agents integer 0–50 Deprecated alias for max_ops
with_synthesis boolean or model string Enable final synthesis, optionally with that model
bare boolean Ignore worker profiles and use the CLI model
dry_run boolean Plan and print without building or executing a graph
show_graph boolean Write the graph visualization during finalization
save string Default artifact directory
team_mode string Create a fresh named team
team_attach string Attach to or create a persistent named team; mutually exclusive with team_mode
reactive string all, off, or a comma-separated role allowlist
artifacts mapping with expected list Declare output files verified at completion

Each artifacts.expected entry requires an alphanumeric, _, or - id and a relative, non-glob path. required defaults to true; description is optional. Source: lionagi/state/artifact_verifier.py.

Declaration fields

These describe the playbook's own command-line interface rather than the run, and they are read on a separate path from the table above.

Field Accepted value Effect
description string Printed by li play NAME --help
args mapping of name to {type, default, help} Becomes real CLI flags; type is str, int, float, or bool
argument-hint string such as '[--mode MODE] [--strict]' Parsed into the same schema shape, used only when args is absent

args is checked by _validate_args_schema, which fails the run on a malformed schema, and injected as parser flags by inject_playbook_schema_into_parser. argument-hint is parsed by _parse_argument_hint. A declared arg is substituted into prompt as {arg_name}, with a CLI value overriding the playbook default. Source: lionagi/cli/orchestrate/__init__.py and lionagi/cli/main.py.

The loader currently runs _validate_spec_fields only after the child process has loaded the file. Submission resolving the playbook and returning a run ID is not a successful validation. Source: lionagi/cli/orchestrate/__init__.py, lionagi/mcp/projection.py, and lionagi/mcp/jobs.py.

Observe a submitted run

The MCP submit verbs are asynchronous handles. fanout.submit, flow.submit, and play.submit return an allocated run_id, process metadata, and current spawn state—not the worker result. Source: lionagi/mcp/jobs.py (submit).

Use that ID to read the three different things callers commonly conflate:

{"ops":[{"op":"job.status","args":{"run_id":"<run_id>"}}]}

job.status answers lifecycle and liveness. Branch on terminal and outcome, not merely the open-ended display status.

{"ops":[{"op":"job.output","args":{"run_id":"<run_id>"}}]}

job.output returns the console tail plus the persisted artifact list and an artifacts_state that distinguishes an empty list from an unreadable one. Use job.wait when the caller should block for a bounded interval instead of polling. Source: lionagi/mcp/dispatch.py and lionagi/mcp/jobs.py (status, output, and wait).

For an interactive CLI flow, use li o ctl status ID for lifecycle state and the printed --save directory for artifacts. A --background launch instead prints a session ID and writes flow.log; that path is separate from the MCP submit contract. Source: lionagi/cli/orchestrate/__init__.py.

Next, learn how to observe, control, and resume durable work.