Skip to content

Full-stack tutorial

Wire all three libraries into one runnable app: YAML agents served over AG-UI (kaboo-workflows), persisted and replayed in a CopilotKit runtime (kaboo-runtime), and rendered in React with sub-agent cards, questions, approvals and a shared canvas (kaboo-react), backed by Postgres.

This tutorial follows the Kaboo lab shipped in this repo — a mini-Kaboo: a chat beside a dashboard canvas, agents that build dashboards through MCP tools, every workflow topology, and scenarios that replay a conversation with a scripted model or run it against a live one. Clone this repo and work from examples/kaboo-workflows-demo/.

Architecture & ports

flowchart LR
    B["Browser"] -->|":3100"| F["Vite + kaboo-react"]
    F -->|"/api, /mcp → :4000"| BE["NestJS + kaboo-runtime"]
    F -->|"/mock → :4100"| M["Mock services"]
    BE -->|"KabooHttpAgent → :8080 /invocations"| P["Pipeline (kaboo-workflows)"]
    BE <-->|"persist / replay"| DB[("Postgres :5433")]
    BE -->|"MCP Apps"| PA["Partner app :4300"]
    P -->|"MCP tools"| M
Service Tech Port
frontend Vite + React 19 + kaboo-react 3100
backend NestJS + @copilotkit/runtime v2 + kaboo-runtime 4000
pipeline Python + kaboo-workflows (create_agui_app) 8080
mock-services Dashboards, saved queries and a SQLite dataset: MCP tools, REST and socket.io 4100
partner-app A third-party MCP Apps server 4300
postgres postgres:16-alpine (db kaboo_lab) 5433

The Vite dev server proxies /api and /mcp to the backend and /mock to the mock services, so the browser only ever talks to :3100.

Prerequisites

  • Node 22.12 or newer with Yarn 4 — run corepack enable once.
  • uv (Python 3.12+) for the pipeline.
  • Docker, for Postgres.
  • For live mode only: AWS credentials for Bedrock, or an OpenRouter API key. Scripted mode needs no model.

1. Install and configure

cd examples/kaboo-workflows-demo
yarn install
(cd pipeline-service && uv sync)
cp .env.example .env

The defaults work as they are. To run live mode on OpenRouter instead of Bedrock, set:

LAB_LIVE_MODEL=openrouter
OPENROUTER_API_KEY=sk-or-...

2. Start Postgres

yarn db:up

PostgresThreadStore is what makes a refresh mid-run or after a run restore the thread, and what keeps a pending question or approval across a restart.

3. Start the stack

yarn dev

Nx starts the five services. Open http://localhost:3100; the sections below walk through what each piece does.

The pipeline — kaboo-workflows on :8080

app = create_agui_app(HERE / "config.yaml", session_config_key="workflow_config")

config.yaml holds what every workflow shares: the models, the MCP clients for the mock services' tools, and the state_effects that keep the workspace. Each run brings its own workflow on forwardedProps.workflow_config, composed by the backend from pipeline-service/workflows/ — one per topology: plain, delegate, nested, parallel, swarm and graph. In scripted mode the backend adds per-agent scripts and kaboo-workflows' scripted model plays them, so a scenario replays the same conversation without a model. uv run python check_workflows.py checks every workflow: no agent above 20 tools, and no tool granted twice. See kaboo-workflows' getting started.

The backend — kaboo-runtime on :4000

Trimmed from backend/src/main.ts:

const agent = new KabooHttpAgent({ url: LAB.pipelineUrl });
agent.use(new MCPAppsMiddleware({ mcpServers: [partner] }));

const runner = createKabooRunner(new PostgresThreadStore({ dsn: LAB.databaseUrl }), {
  enrich,
  approvals: { onDecision, grants: new InMemoryGrantStore() },
  accessPolicy: { ownerOf: () => LAB.owner },
});

const runtime = new CopilotRuntime({
  agents: { [LAB_AGENT_ID]: agent },
  runner,
  a2ui: { defaultCatalogId: KABOO_WIDGETS_CATALOG_ID },
  openGenerativeUI: true,
});
app.use(createCopilotExpressHandler({ runtime, basePath: "/api/copilotkit" }));

The runner records every AG-UI event in Postgres, compacts each finished run, and replays the thread when the browser reconnects. enrich composes the run's workflow into forwardedProps. onDecision hands decisions on tools that enforce their own approvals to the service that owns them, and the grant store remembers "Allow for this chat" and "Always allow". State keys have owners: kaboo_sessions, the agents' memory, never reaches the browser, and kaboo_workspace reaches it read-only. MCPAppsMiddleware lets the partner's own app render in the chat. See kaboo-runtime's concepts.

The frontend — kaboo-react on :3100

Trimmed from frontend/src/App.tsx and frontend/src/chat/:

<KabooProvider runtimeUrl="/api/copilotkit" agent={LAB_AGENT_ID} threadId={threadId} a2ui={A2UI} openGenerativeUI={openGenerativeUI}>
  <BrowserTools />
  <KabooChat />
  <Canvas threadId={threadId} />
</KabooProvider>
function BrowserTools() {
  useAskUserTool();
  useRequestTool({ name: "request_files", description, parameters, render: FilesPanel });
  return null;
}

KabooChat renders the conversation with a card per sub-agent — its text, tools and result — and the questions and approvals waiting on the person. The browser tools are the ones an agent's client_tools let it call. The canvas reads the workspace with useAgentState, shows which sub-agents are working with useAgentPresence, and outlines widgets as they are written with useLiveToolCalls. See kaboo-react's concepts.

4. Try it

Pick a scenario in the header; its turns appear as chips at the bottom of the chat. Scripted scenarios need no model.

  • Sub-agents: delegate-dashboard — the analyst explores and saves queries, then the visualizer builds the dashboard from their ids. Each streams in its own card while the canvas follows them.
  • Questions: ask-subagent — the analyst asks a multiple-choice question (Other is always there). The card comes up in the main thread, and the answer goes back into the analyst's card.
  • Approvals: approval-subagent — approve once, or deny with a reason, a call two delegations down. approval-grant allows deletions for the rest of the chat, and approval-skipped sends a message instead of answering.
  • Generative UI: a2ui-inline, ogui-visual and mcp-partner — a KPI and a chart from the widget catalogue, a visual generated in a sandbox, and a partner's own app.
  • Live: switch Mode to Live and ask anything about the sample data.
  • Replay: refresh the page mid-run or after it — the thread restores from Postgres.

From the command line, yarn lab all runs and checks every scenario, and yarn e2e runs the browser journeys.

How each library fits

Library Role in the lab
kaboo-workflows config.yaml and workflows/ define models, agents, MCP tools, browser tools and topologies; create_agui_app serves each run's workflow over AG-UI SSE.
kaboo-runtime The CopilotKit AgentRunner: records, compacts and replays each thread in Postgres, adds server-side props, remembers approvals and owns state keys.
kaboo-react Renders sub-agent cards, questions and approvals in the chat, and gives the canvas the workspace and the agents' presence.

Going to production: the host integration seams

A real backend (multi-user, files behind auth, many agent definitions) leans on these seams. The lab already uses the first two:

  • Per-run context — forwardedProps. Your Node backend attaches a JSON object to each run (CopilotKit forwards it on RunAgentInput), and the pipeline reads it with get_forwarded_props(). Use it for run-scoped credentials and per-run agent config — it never reaches the model as text. kaboo-runtime's enrich adds to it on the server, where the browser can't change it. → kaboo-workflows concepts
  • Many workflows, one pipeline. The lab's pipeline serves create_agui_app(config, session_config_key="workflow_config"), and enrich puts each run's YAML in forwardedProps under that key. The pipeline builds that run's agents and releases them with the run, so editing a definition takes effect on the next turn and config.yaml holds only what workflows share. In your backend the definitions come from your database. → Multi-file configs & session overlays
  • State the server vouches for. enrich can also return state: keys the server sets for the run, which a browser's copy can't overwrite. → Server-side props
  • Authorized attachments. When user files live behind your API's auth, set attachments.base_url / authorization: forwarded_props:<key> / content_url_template in config.yaml so agents can fetch them on every turn — the token is only ever sent to your own origin. → Attachments & Multimodal
  • Thread ownership. The lab records one fixed owner. Pass accessPolicy: { ownerOf } to createKabooRunner with the caller's user id, and each thread records it as createdById in the thread list your backend filters per caller. → Access control

New to the pieces? Start with Start here to pick a single-library path first.