I spend my working weeks inside large financial institutions, watching teams demo "AI agents" that are, on inspection, chat boxes with a system prompt. The interface is a text stream. The user sees tokens. The agent sees tokens back. Nothing else exists. And then someone from the operations side asks the question that kills the demo: "Where does the approval happen?"

Because real work is not a conversation. A wealth onboarding flow is not "tell me about yourself." It is identity verification, then risk assessment, then a routing decision, then an advisor review for the cases that need one, then account opening. Each step has state, output, and a decision. The moment an agent computes a risk score, the user does not want a sentence describing the score. They want the gauge. When it proposes an allocation, they want the donut chart they can click into, not a paragraph that approximates one.

This guide is about AG-UI, the protocol that closes that gap: a typed event stream between an agentic backend and a user-facing frontend. I built a full reference implementation for it (AURUM, a fictional private-bank onboarding demo, public repo linked at the end), with a process engine as the orchestrator instead of a chat loop. That architectural choice turned out to matter more than the protocol itself, and the second half of this guide is about why. The patterns here are framework-agnostic. I used Camunda 8 because I know it well; everything I claim about externalized state applies equally to Temporal, LangGraph with a checkpointer, or a state machine you wrote yourself.

## The problem: tokens are not an interface

Streaming tokens solved a latency problem, not an interface problem. Token streaming makes a slow model feel responsive. It does nothing for the actual shape of the work.

Consider what a chat-native agent cannot show you:

- **Progress through a process.** "Step 3 of 6" is a UI concept. A token stream has no steps, only more tokens.
- **A tool call as a first-class object.** The agent called `verifyIdentity` and got back `{status: "verified", method: "document+biometric"}`. That is a card with a green check, not a sentence the model paraphrases (and occasionally paraphrases wrong).
- **A pause.** A human needs to approve this. In a chat loop, "waiting for a human" means the request is either held open or lost. Neither is a real pause.
- **Structured state.** The onboarding record has 40 fields that both the agent and the UI care about. Serializing it into prose on every turn is lossy in both directions.

Every team I have watched hit this wall solves it the same way: they invent a private event vocabulary between their backend and their frontend. A JSON blob with a `type` field, a switch statement in React. It works, and then it calcifies. The events are undocumented, the frontend and backend drift, and the second client (the mobile app, the ops dashboard) has to reverse-engineer the dialect.

AG-UI is that private vocabulary, standardized. That is the whole pitch, and it is enough.

## What AG-UI is

AG-UI (Agent-User Interaction Protocol) is an open, event-based protocol that standardizes how an agentic backend talks to a user-facing application. It came out of CopilotKit, developed in partnership with LangGraph and CrewAI, and now lives in the open under the `ag-ui-protocol` organization on GitHub. The agent side emits typed events over a stream. The app side renders each event. User input flows back as structured messages. That is the entire model.

| Fact | Detail |
|---|---|
| Origin | CopilotKit, with LangGraph and CrewAI; open-sourced under `ag-ui-protocol` |
| Shape | One-directional event stream agent-to-app, plus a structured input channel app-to-agent |
| Core event vocabulary | ~16 event types in the original spec across five categories: run lifecycle, text messages, tool calls, state management, and special (RAW, CUSTOM). The current spec has grown past that with convenience chunk events and reasoning events |
| State model | Shared typed store: `STATE_SNAPSHOT` for full state, `STATE_DELTA` for RFC 6902 JSON Patch diffs |
| Transport | Transport-agnostic. Server-Sent Events (SSE) is the default and reference transport; plain HTTP and WebSockets also work |
| Human-in-the-loop | First-class: pause, approve, edit, retry mid-run without losing state |
| What it standardizes | The event types, their payload shapes, ordering semantics, and the state-sync mechanism |
| What it leaves open | How you render anything, component schemas for generative UI, authentication, authorization, persistence |
| Integrations | 16+ framework bindings: LangGraph, CrewAI, Microsoft Agent Framework, Google ADK, AWS Strands, Pydantic AI, LlamaIndex, and others |

The design bet is "loose format matching": AG-UI standardizes the flow of agent state and UI intent, not the pixels. A `TOOL_CALL_START` event does not tell your frontend to render a spinner in a card with rounded corners. It tells your frontend a tool call began, with a name and an ID, and the rest is your problem. This is the right call. Every attempt I have seen to standardize the rendering layer itself dies under the weight of its own component library.

## Where it sits in the protocol stack

The agentic protocol landscape sorted itself, over about eighteen months, into four layers by counterparty. I find this the only framing that makes the acronyms stick:

| Protocol | Connects the agent to... | Origin | One-line job |
|---|---|---|---|
| MCP | Tools and context | Anthropic | Give the agent hands: standardized access to systems and data |
| A2A | Other agents | Google | Let agents delegate and coordinate across vendors and runtimes |
| AG-UI | The human | CopilotKit | The last mile: turn agent activity into something a person can see and click |
| AP2 | Money | Google + payments partners | Cryptographically accountable mandates for agent-initiated payments |

![The four-layer agentic protocol stack by counterparty: MCP for tools, A2A for other agents, AG-UI for the human highlighted as the layer this guide covers, and AP2 for money](/diagrams/agui-protocol-stack.svg)

One production system frequently uses three of these at once, and increasingly all four. In AURUM, the agent reaches its tools through the orchestration layer (Camunda's AI Agent Connector plays the MCP role of tool brokerage), and everything the user sees rides on AG-UI. I cover the other layers in depth in the [A2A field guide](/guides/a2a-field-guide/) and the [AP2 field guide](/guides/ap2-field-guide/); this document stays on the human layer.

The layers are genuinely orthogonal. Nothing about AG-UI assumes MCP is underneath it, and nothing about A2A assumes an AG-UI surface exists. That orthogonality is why you can adopt them independently, which is how adoption actually happens inside enterprises: one protocol per budget cycle.

## Event anatomy

AG-UI events are flat JSON objects with a `type` discriminator. Five categories do the work.

#### Run lifecycle

`RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR`, plus `STEP_STARTED` / `STEP_FINISHED` for named phases inside a run. These are the events your progress UI hangs off. A run carries a `threadId` and `runId`; steps carry a `stepName`. In AURUM, the operator console is essentially a renderer for lifecycle events: each BPMN activity maps to a step, so the raw wire reads like a process log because it is one.

#### Text streaming

Three events per message, correlated by `messageId`:

```json
{ "type": "TEXT_MESSAGE_START", "messageId": "msg_042", "role": "assistant" }
{ "type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_042", "delta": "Your risk profile " }
{ "type": "TEXT_MESSAGE_CONTENT", "messageId": "msg_042", "delta": "suggests a balanced allocation." }
{ "type": "TEXT_MESSAGE_END", "messageId": "msg_042" }
```

The start/content/end triple looks bureaucratic next to "just stream the string," but the explicit boundaries are what let a client interleave multiple concurrent messages and know, deterministically, when a message is complete rather than stalled.

#### Tool calls

`TOOL_CALL_START` (name and ID), `TOOL_CALL_ARGS` (streamed argument deltas), `TOOL_CALL_END`, and `TOOL_CALL_RESULT`. This is the category that separates an agent UI from a chat UI. The frontend can render the call as an object with its own lifecycle: pending, running, resolved, with arguments and results inspectable. In AURUM, `parseApplicationForm → {fieldsParsed: 12}` renders as a completed step chip in the customer view and as a raw event in the operator view. Same event, two renderings.

#### State synchronization

`STATE_SNAPSHOT` carries the whole shared state object. `STATE_DELTA` carries an RFC 6902 JSON Patch array:

```json
{ "type": "STATE_DELTA", "delta": [
  { "op": "replace", "path": "/application/riskScore", "value": 72 },
  { "op": "add", "path": "/application/flags/-", "value": "PEP_SCREEN_REQUIRED" }
] }
```

Snapshots on connect, deltas while streaming. The pattern is event-sourcing applied to UI state, and it is the piece most homegrown dialects get wrong by shipping full state on every change and wondering why the stream is 40x larger than it needs to be.

#### Generative UI

Here the spec is deliberately unopinionated: generative UI rides on tool-call events rendered as components, or on `CUSTOM` events. My demo dialect names it explicitly, which the spec would classify as a custom event:

```json
{ "type": "GENERATIVE_UI", "pid": "2251799813737463", "ts": 1719849302144,
  "component": "PortfolioAllocationChart",
  "props": { "equities": 60, "fixedIncome": 30, "alternatives": 10 } }
```

The frontend holds a registry mapping `component` names to real React components (`IdentityResultCard`, `PortfolioAllocationChart`, `ProjectedGrowthChart`, `AccountOpenedCard`). Unknown component name? Render a fallback card with the raw props. That one defensive decision saved the demo twice during development, and it is the difference between a dialect that degrades and one that white-screens.

The ordering rules are simple and load-bearing: process events in arrival order, and treat events sharing an ID (`messageId`, `toolCallId`) as one logical stream. Everything else in a client follows from those two rules.

## The pattern I proved: process-orchestrated generative UI

Here is the part I care about most, because it is the part the protocol docs cannot tell you.

AG-UI standardizes the wire. It says nothing about who owns the state behind the wire. In every CopilotKit-style quickstart, the answer is the agent loop itself: a chat runtime holds the conversation, calls the model, emits events. That works until the process crashes, or a human needs three days to decide, or an auditor asks what happened on March 14th.

In AURUM I inverted it. The orchestrator is a process engine (Camunda 8, BPMN), and the AG-UI stream is a projection of process state. The backend is a Node BFF with an SSE hub; Zeebe workers and a task poller translate engine events into AG-UI events. The agent (Camunda's AI Agent Connector running an ad-hoc sub-process, with Claude on Bedrock and Gemini as swappable models) picks per turn from 13 heterogeneous tools: service tasks, a FEEL script, an HTTP connector for live FX rates, a human hand-off. The flow is intake, identity, risk, a DMN decision table for routing, then either straight-through account opening or an advisor review.

When the orchestrator owns the state, the UI stream inherits three properties that chat-native architectures cannot have:

**Resumable.** If the BFF or the agent crashes mid-flow, the process instance is still sitting in the engine at the exact activity it reached. Restart the backend, replay a `STATE_SNAPSHOT` from process variables, and the stream continues. State lives in the engine, not in the LLM's context window and not in a Node process's heap.

**Pausable.** The DMN table routes cases above $1.5M to a human user task. At that moment the process token stops. Not "the agent polls in a loop," not "we hold an HTTP request open with a long timeout." The token is parked, durably, for a minute or a month. The AG-UI stream emits the `USER_TASK` event, the customer sees an honest "your advisor is reviewing this" card, the advisor sees an approval card in their queue. When the advisor decides, the token moves and the stream resumes. HITL is a first-class construct of the orchestrator, not a hack bolted onto a chat loop. Critically, the AI did not decide to wait. The process did. You do not want "pause for compliance review" to be a behavior you prompt for.

**Auditable.** Every state transition, every tool invocation, every routing decision is recorded by the engine as a side effect of execution. In Camunda that trail is visible in Operate with zero custom logging code. When the auditor asks why James Rodriguez's application went to manual review and Sarah Chen's did not, the answer is a DMN table with hit-policy semantics and a timestamped token history, not a grep through model transcripts.

> Review comment: if your audit story depends on a developer remembering to add a log line next to every tool call, you do not have an audit story. You have a diary.

None of this is Camunda-specific. The property comes from externalizing state into something durable that is not the model context and not the web tier. Temporal gives you the same resumability through event-sourced workflow histories. LangGraph gives you a weaker but real version through checkpointers. A Postgres-backed state machine you wrote yourself qualifies. The design rule is: **the AG-UI stream should be a view over durable state, never the system of record.** Get that right and the protocol choice on the wire is almost incidental. Get it wrong and no protocol will save you.

## One stream, many surfaces

The event stream is typed and semantic, so nothing about it assumes one reader. AURUM runs three personas off the same wire:

| Surface | Route | What it renders from the stream |
|---|---|---|
| Customer | `#/customer` | Concierge text, generative cards streaming in one by one, honest "waiting for review" states |
| Advisor | `#/advisor` | The approval queue: `USER_TASK` events become actionable cards; decisions flow back into the process |
| Operator | `#/operator/{pid}` | The raw event log, unrendered: every lifecycle event, tool call, and state delta as it arrives |

![One agentic backend emitting a typed AG-UI event stream over SSE, with TEXT_MESSAGE, TOOL_CALL, STATE_DELTA, and generative UI events flowing on the wire and fanning out to customer, advisor, and operator views](/diagrams/agui-event-stream.svg)

The operator view exists as the "don't take my word for it" view. Open it next to a customer session and you watch the raw wire produce the polished UI in real time. In customer demos this lands harder than any architecture slide, because it proves the cards are not choreographed frontend theater. They are renderings of the same events the ops console shows raw.

The deeper point: in most enterprises, the customer UI, the back-office queue, and the ops dashboard are three teams, three codebases, and three integration projects. A typed event stream collapses that to one contract with three renderers. The advisor desk views (`#/desk`, `#/pipeline`) came almost for free once the vocabulary existed, and "almost for free" is not a phrase I use loosely.

## Implementation honesty: what ~150 lines buys you

The AURUM frontend has no agent framework. No CopilotKit, no ChatKit. The AG-UI client is a small `EventSource` wrapper plus a `useAGUI` React hook, about 150 lines total. I did this deliberately, to find the real floor of what a client needs. Here is the honest accounting.

**What the ~150 lines actually handle:** opening the SSE connection per process instance, parsing each event, dispatching on `type` into React state (append text delta, upsert tool call by ID, push a generative card, set the user-task flag), and leaning on `EventSource`'s built-in auto-reconnect. That is genuinely all a demo client needs, and it is why I tell teams the client-side cost of AG-UI adoption is low. The expensive part of agent UX is the backend semantics, not the socket handling.

**What they do not handle, and what I would harden for production:**

- **Reconnection with continuity.** `EventSource` reconnects, but my server does not implement `Last-Event-ID`, so events emitted during a disconnect are simply gone. The fix is standard: assign monotonic event IDs, keep a short replay buffer (or re-derive from the engine, which is the real system of record), and replay from the client's last ID on reconnect. Falling back to a fresh `STATE_SNAPSHOT` when the buffer has aged out covers the rest.
- **Backpressure.** SSE has none. A slow client means the server buffers. With one demo browser this is invisible; with thousands of concurrent streams it is a memory problem. Production answers: per-connection queue caps with snapshot-and-reset on overflow, or coalescing state deltas (JSON Patch composes, so N deltas can collapse into one).
- **Ordering across sources.** Within one SSE connection, ordering is guaranteed. But my BFF fans in events from multiple workers and a task poller. Two workers emitting near-simultaneously can interleave in an order that differs from engine order. The demo ignores this; production should sequence at the hub using engine-side ordering (position or timestamp from the process instance) before writing to the socket.
- **Idempotent rendering.** Replay plus at-least-once delivery means duplicates. Upserting by `messageId`/`toolCallId` gets you most of the way; my generative card handler appends, so a replayed `GENERATIVE_UI` event renders twice. Cards need stable IDs too.
- **Stream authentication.** The demo's personas are hash routes. There is no auth on the SSE endpoint at all. Production needs a short-lived token bound to the process instance and persona, checked at subscribe time, and re-checked on reconnect.

One more piece of honesty about the demo itself: it runs without any API keys. Without `ANTHROPIC_API_KEY`, the agent workers replay a deterministic scripted scenario ("shadow mode"), so the full AG-UI stream, the HITL pause, and the engine choreography all work before you spend a cent on tokens. I built that for demo reliability, but it earned its keep as a test harness: the entire event pipeline is exercised with zero model variance, which is exactly what you want in CI. If you adopt one idea from this section, adopt that one.

> Review comment: a scripted "shadow mode" is not a lesser demo. It is a fixture for your event contract, and it will catch more frontend regressions than any prompt eval will.

## What AG-UI doesn't solve

Protocols earn trust by being clear about their edges. Three gaps you will own yourself:

**Authorization of actions shown in the UI.** AG-UI can stream an approval card to an advisor. It has no opinion on whether that advisor is allowed to approve. The event is a rendering instruction, not a permission grant, and a frontend that treats "the card appeared" as authorization has an IDOR-shaped hole. The check must live where the action executes. In AURUM that is the engine: the user task completion API enforces who can complete it, and the UI is just a pretty face on that fact. Wherever your actions execute, enforce there, and treat every inbound "user decided X" message as untrusted input.

**Schema governance of generative components.** The `component` + `props` contract between backend emitters and the frontend registry is exactly as strong as your discipline. Nothing in AG-UI validates that `PortfolioAllocationChart` still accepts `alternatives` after last week's refactor. You need what you would need for any API: shared schema definitions (I would use zod or JSON Schema, validated on emit and on render), a fallback renderer for unknown components, and treating component props as a versioned public API, because that is what they are.

**Versioning events across deploys.** Streams are long-lived; deploys are not. During a rolling deploy, a client built against yesterday's event shapes can receive today's events, and a process instance started last week can resume streaming into a redesigned frontend. The spec gives you `RAW` and `CUSTOM` escape hatches but no versioning story. Minimum viable answer: additive-only changes to event payloads, a version field in your custom events, and a policy for draining long-lived streams before breaking changes. Process-orchestrated setups feel this more, not less, because a parked user task can happily wait through six deploys.

I would add a soft fourth: AG-UI does not make your agent good. A typed stream of a bad process is a well-formatted bad process. The protocol moved my problem from "how do I get structure to the frontend" to "what structure does this workflow actually need," which is the right problem, but it is still work.

## The one-paragraph version

AG-UI is an open, typed event protocol between an agentic backend and a user-facing frontend: run lifecycle, streamed text, tool calls with results, JSON Patch state deltas, and an open lane for generative UI components, usually over SSE. It occupies the agent-to-human layer of the stack, alongside MCP (tools), A2A (other agents), and AP2 (payments), and a capable client is small (mine is about 150 lines, no framework). The protocol standardizes the wire and deliberately not the rendering, the authorization, or the schema governance, so those remain your job. The highest-leverage decision is not on the wire at all: make the stream a projection of durable, externalized state (a process engine, Temporal, a checkpointed graph), and the UI becomes resumable after crashes, pausable for human approval as a first-class construct, and auditable as a side effect of execution. I proved the pattern with AURUM, a fictional private-bank onboarding flow where one event stream feeds a customer view, an advisor approval queue, and a raw operator console, and where the $1.5M approval pause is a parked process token, not a prompt.

## References

- AG-UI protocol documentation: [https://docs.ag-ui.com](https://docs.ag-ui.com/introduction)
- AG-UI protocol on GitHub: [https://github.com/ag-ui-protocol/ag-ui](https://github.com/ag-ui-protocol/ag-ui)
- AURUM demo repo (runnable, shadow mode needs no API keys): [https://github.com/letmereviewyourcode/camunda-agui-wealth-demo](https://github.com/letmereviewyourcode/camunda-agui-wealth-demo)
- The EP02 demo video and discussion on LinkedIn: [https://www.linkedin.com/posts/zishanalikhan_agenticai-agui-camunda-ugcPost-7480344185674313728-ffd8](https://www.linkedin.com/posts/zishanalikhan_agenticai-agui-camunda-ugcPost-7480344185674313728-ffd8)
- Companion guides in this series: [A2A field guide](/guides/a2a-field-guide/) · [AP2 field guide](/guides/ap2-field-guide/)
- RFC 6902 (JSON Patch), the mechanism behind `STATE_DELTA`: [https://datatracker.ietf.org/doc/html/rfc6902](https://datatracker.ietf.org/doc/html/rfc6902)