01

The Event Pipeline

How CAO detects agent state and delivers messages at the right time — without polling.

The Event Pipeline

The fundamentals course introduced the Event Bus as the glue between CAO's components. Now let's look at what actually flows through it — and the five specialized services that make message delivery feel instant.

Think of it as a newsroom. Reporters (FIFOs) feed raw stories into a wire service (EventBus), editors (StatusMonitor) decide what is breaking news, and the dispatch desk (InboxService) routes the right story to the right person at the right time.

F
FifoManager (Reporters)

Reads raw bytes from named FIFOs at ~/.aws/cli-agent-orchestrator/fifos/{id}.fifo. Coalesces rapid-fire chunks within a 50ms window before publishing.

B
EventBus (Wire Service)

Async pub/sub backbone. Wildcard topic matching (terminal.*.output), bounded queues (1024 default), drop-and-log on overflow.

S
StatusMonitor (Editors)

Maintains an 8KB rolling buffer per terminal. Runs provider-specific pattern matching to detect IDLE, PROCESSING, COMPLETED, WAITING_USER_ANSWER, or ERROR. Uses debounced edge detection with a sticky latch.

I
InboxService (Dispatch Desk)

Routes the right message at the right time. When a terminal reaches IDLE or COMPLETED, delivers one pending message from its inbox.

L
LogWriter (Archivist)

Subscribes to terminal.*.output alongside everyone else. Batches writes to per-terminal log files so you always have a full record.

Watch a Chunk Travel

Follow a single piece of terminal output as it flows through every stage of the pipeline. Notice how the EventBus appears twice -- once routing raw output, once routing the derived status change. This two-phase publish is the key to decoupling detection from delivery.

P
pipe-pane
F
FifoManager
B
EventBus
S
StatusMonitor
I
InboxService
L
LogWriter
Click "Next Step" to begin
Step 0 / 7
💡
Key Insight: Two-Phase Publish

The EventBus carries two kinds of topics: terminal.{id}.output for raw data, and terminal.{id}.status for derived state changes. StatusMonitor subscribes to the first and publishes the second. InboxService only subscribes to the second -- it never sees raw output.

The Sticky Latch

StatusMonitor uses debounced edge detection -- it does not re-evaluate status on every single chunk. Instead it waits for output to settle, then checks. And once it determines a terminal is IDLE, it applies a "sticky latch":

🔒
Why a Latch?

Once an agent reaches IDLE, the StatusMonitor latches that status. It will not drop to PROCESSING just because the TUI redraws a progress bar or refreshes the screen. Any new input sent to the terminal unlocks the latch and allows a transition back to PROCESSING — that includes handoff, assign, inbox delivery, and cao session send.

⚠️
Without the Latch

A TUI redraw would momentarily look like new output, causing StatusMonitor to flip the terminal to PROCESSING. InboxService would then hold messages hostage until the next IDLE detection until the next genuine idle detection.

Here is the core loop, simplified. The real code adds debouncing, buffer rotation, and the latch logic, but the publish/subscribe pattern remains the same:

Python
# services/status_monitor.py (simplified)
queue = bus.subscribe("terminal.*.output")  # wildcard!
async for topic, data in queue:
    terminal_id = topic.split(".")[1]
    buffer[terminal_id] += data
    status = provider.get_status(buffer[terminal_id])
    if status != last_status[terminal_id]:
        bus.publish(f"terminal.{terminal_id}.status", {"status": status})
Plain English
Subscribe to ALL terminal output using a wildcard pattern.
When new data arrives, extract the terminal ID from the topic.
Append the data to that terminal's rolling buffer.
Ask the provider-specific detector what state the terminal is in.
If the state changed from last time, announce the new status on the bus.
02

Workflows

Declarative YAML pipelines that turn multi-step agent tasks into repeatable, validated, resumable sequences.

Why Workflows?

The supervisor pattern with assign() is powerful — it lets a creative agent decompose problems on the fly, delegate to workers in parallel, and adapt to unexpected results. But sometimes you do not want creativity. You want a fixed pipeline that runs the same steps, in the same order, every time.

Workflows give you exactly that: a declarative YAML spec that defines each step, its inputs, its outputs, and how data flows between them. No improvisation, no skipped steps, no variation between runs.

Supervisor + assign()

Flexible and creative. The supervisor decides what to do next based on results. Great for novel, open-ended tasks.

Workflows

Deterministic and repeatable. Steps are fixed in advance. Inputs are typed (string, int, bool, path) and step outputs can declare a JSON Schema. Runs are journaled and resumable.

Anatomy of a Workflow

A workflow spec is a flat YAML file with top-level metadata and a list of steps. Specs live in ~/.aws/cli-agent-orchestrator/workflows/.

workflow spec (YAML)
name: code-review-pipeline
description: Automated PR review pipeline
inputs:
  pr_url:
    type: string
    required: true
steps:
  - id: fetch-diff
    provider: claude_code
    agent: developer
    prompt: "Fetch the diff from {{workflow.inputs.pr_url}} and summarize"
    output_schema:
      type: object
      properties:
        summary: {type: string}

  - id: review-security
    provider: claude_code
    agent: reviewer
    prompt: "Review for security issues: {{steps.fetch-diff.output.summary}}"

  - id: review-quality
    provider: kiro_cli
    agent: reviewer
    prompt: "Review for code quality: {{steps.fetch-diff.output.summary}}"
Plain English
name: — workflow identifier used to run it
description: — human-readable summary
inputs: — dictionary of typed parameters
Each input key has type and required fields
 
 
steps: — sequential list of steps to execute
id: — unique step identifier used to reference outputs
provider: — which AI CLI runs this step (required)
agent: — agent profile for this step
prompt: — message sent to the agent; supports {{}} interpolation
output_schema: — required if later steps reference this step's output; the agent returns it via workflow_return
 
Step 2: id: gives it a unique name for later reference
provider: can differ per step (here claude_code)
{{steps.fetch-diff.output.summary}} — references a field from a previous step's output (full path required: {{steps.STEP_ID.output.FIELD}})
 
Step 3: uses a different provider (kiro_cli)
Same agent profile but different provider
Template syntax: {{workflow.inputs.NAME}} for inputs, {{steps.STEP_ID.output.FIELD}} for step outputs

Running a Workflow

From authoring to monitoring — the lifecycle of a workflow run:

1
Validate the spec

cao workflow validate my-pipeline.yaml — catches grammar errors, duplicate step ids, and constraint violations (max 100 steps, 256KB limit) before you run anything. Template references are resolved at run time, so validate will not catch a typo in {{steps.…}}.

2
Start the run

cao workflow run code-review-pipeline --input pr_url=https://... — launches the workflow with the required inputs. This blocks until the run finishes, then prints the run id and per-step results. Pass --run-id to pre-assign an id you can poll from another shell.

3
Steps execute sequentially

Each step launches an agent via run_agent_step (same substrate as handoff). Steps run one at a time, each with a 600-second timeout and up to 3 retries.

4
Data flows between steps

Step outputs are captured and available to later steps via {{steps.STEP_ID.output.FIELD}} template expressions (the full path is required: {{workflow.inputs.NAME}} for inputs, {{steps.STEP_ID.output.FIELD}} for step outputs). Outputs are validated against JSON Schema if defined.

5
Monitor progress

cao workflow status RUN_ID — see which step is currently executing, which have completed, and the overall run state.

Resume from crash

CAO journals every step completion. If a run is interrupted — process killed, machine reboots, network drops — cao workflow resume RUN_ID keeps every completed step and re-runs the interrupted step and anything after it on a fresh terminal.

Check Your Understanding

1. When would you choose a workflow over a supervisor with assign()?

03

Skills & Install

Reusable behavior modules that agents load on demand, and the install system that wires everything together.

Skills

A skill is a folder containing a SKILL.md file with structured instructions that an agent can load at runtime. Skills live in ~/.aws/cli-agent-orchestrator/skills/ and are managed via the CLI: cao skills add, cao skills remove, cao skills list.

When an agent needs a skill, it calls the load_skill(name) MCP tool. For Kiro CLI, skills are delivered natively via skill:// resources in the agent JSON. Kiro CLI and OpenCode CLI discover skills natively; other providers load them on demand through the MCP server.

What's in a Skill

A folder with a SKILL.md file containing step-by-step instructions, constraints, and examples. The agent receives this content verbatim when it loads the skill.

How Agents Discover Them

Agents call load_skill(name) at runtime. The MCP server resolves the name to the skill folder, reads SKILL.md, and returns its content directly into the agent's context.

Built-in Skills

CAO ships 11 built-in skills, including supervisor and worker protocols, memory usage, workflow authoring, session management, and agent routing. These cover the most common multi-agent patterns out of the box.

+
cao skills add ./my-custom-skill

Copies the skill folder into the skills directory, making it available to all agents immediately.

cao skills remove my-custom-skill

Removes the skill from the skills directory. Running agents that already loaded it are unaffected.

cao skills list

Shows all installed skills with their names and descriptions.

Installing Agents

The cao install command takes a profile name, local path, or URL and sets it up for your provider automatically. No manual JSON editing or config wrangling required.

1
Install the profile

cao install code_supervisor — Resolves a built-in profile and writes a context file to agent-context/. Also accepts local paths (./my-agent.md) or URLs from an allowlisted host such as raw.githubusercontent.com.

2
Provider-specific setup happens automatically

For Kiro CLI, CAO writes the agent JSON with skill:// resources. For Claude Code and Codex, it writes a context file (no provider-specific config needed). You never touch provider configs directly.

3
Set secrets with cao env

cao env set ANTHROPIC_API_KEY sk-... — Stores the variable in ~/.aws/cli-agent-orchestrator/.env. Profiles reference them with ${VAR} syntax.

4
Verify the installation

cao profile list — Confirms the profile is registered and lists the name, source, and description. Use cao profile show <name> for the provider, role, and tool access.

terminal
# Install from built-in registry
cao install code_supervisor

# Install from a local file
cao install ./my-agent.md

# Install from URL with env var
cao install https://raw.githubusercontent.com/org/repo/main/agent.md \
  --env TEAM=platform

# Manage environment variables
cao env set ANTHROPIC_API_KEY sk-...
cao env list
cao env unset ANTHROPIC_API_KEY
What each command does
Downloads and registers the built-in code_supervisor profile
Installs a custom profile from a local markdown file
Fetches a remote profile and embeds the TEAM env var into it
Stores, lists, and removes environment variables used by profiles
Skills Compose with Profiles

A profile says WHAT the agent is — its role, provider, and system prompt. Skills say HOW it should behave in specific situations. A single profile can reference multiple skills, and the same skill can be shared across many profiles. This separation keeps agent definitions clean and behavior reusable.

04

Automation & Ops

Run agents on a schedule, orchestrate them from the outside, and build zero-human-in-the-loop pipelines.

Scheduled Flows

Scheduled Flows let you run agent tasks on a cron-like schedule without any human trigger. Define a flow as a Markdown file with YAML frontmatter, register it with the CLI, and CAO's background daemon handles the rest.

Flow File (nightly-review.md)
---
name: nightly-review
schedule: "0 2 * * *"
agent_profile: code_supervisor
provider: claude_code
---

Review all PRs opened today and
summarize findings.
Plain English
 
Name this flow "nightly-review".
Run every day at 2:00 AM.
Use the code_supervisor profile.
Run inside Claude Code.
 
The body becomes the agent's task prompt.
 

The server's background daemon checks every 60 seconds for flows whose cron expression matches. Sessions are auto-named cao-flow-{name}.

CLI commands: cao schedule add FILE, cao schedule list, cao schedule run NAME, cao schedule enable/disable NAME, cao schedule remove NAME.

Flows can include a script step that runs first and outputs JSON. If it returns {"execute": false}, the flow is skipped for that cycle — useful for conditional execution based on external state.

The Ops MCP Server

CAO ships two MCP servers with very different purposes. Understanding which is which is essential for building integrations.

cao-mcp-server

Runs INSIDE agent sessions.

Provides tools like handoff, assign, and send_message so agents can orchestrate each other during a workflow.

The internal intercom system between coworkers.

cao-ops-mcp-server

Runs OUTSIDE — for external control.

Lets CI/CD pipelines, meta-agents, or scripts manage CAO itself: launch sessions, send messages, read output, shut things down.

The building's reception desk — controls who enters and exits.

Ops server tools:

  • list_profiles — discover available agent profiles
  • install_profile — install a profile by name or URL
  • launch_session — start a new CAO session
  • send_session_message — deliver a message to a running session
  • get_terminal_status — check if a terminal is idle, processing, etc.
  • get_terminal_output — read recent terminal output
  • read_session_output — read a terminal's captured output by session name
  • get_profile_details — get detailed information about a specific profile
  • list_sessions — list all active CAO sessions
  • get_session_info — get detailed info about a specific session
  • shutdown_session — gracefully stop a session

Fully Autonomous Operation

When an agent hits a permission prompt or asks for clarification, it enters WAITING_USER_ANSWER status. Normally a human would respond. But the answer_user_prompt tool lets another agent respond programmatically.

A supervisor (or another internal agent via the cao-mcp-server) calls answer_user_prompt(terminal_id, answer) to unblock the waiting worker — no human needed. Note: answer_user_prompt is available only through the internal cao-mcp-server, not the ops server. External scripts can send input to a waiting terminal via POST /terminals/{id}/input — the same generic endpoint used for all terminal input.

Zero-Human-in-the-Loop

Combine all three pieces for fully autonomous pipelines: Scheduled Flows trigger work on a cron, the Ops MCP Server lets external systems monitor and control sessions, and answer_user_prompt resolves any agent-to-human blocks automatically. The result: agents that run 24/7 without anyone watching.

Check Your Understanding

Which MCP server would a CI/CD pipeline use to launch a CAO session?

← Back to the fundamentals course