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 enableonce. 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-grantallows deletions for the rest of the chat, andapproval-skippedsends a message instead of answering. - Generative UI:
a2ui-inline,ogui-visualandmcp-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 onRunAgentInput), and the pipeline reads it withget_forwarded_props(). Use it for run-scoped credentials and per-run agent config — it never reaches the model as text. kaboo-runtime'senrichadds 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"), andenrichputs each run's YAML inforwardedPropsunder 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 andconfig.yamlholds only what workflows share. In your backend the definitions come from your database. → Multi-file configs & session overlays - State the server vouches for.
enrichcan also returnstate: 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_templateinconfig.yamlso 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 }tocreateKabooRunnerwith the caller's user id, and each thread records it ascreatedByIdin 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.