---
title: Event System
description: How user gestures travel from the renderer back to the agent — EventType vocabulary, ActionEnvelope shape, subscription rules, and the two consumer paths.
---

User actions originate in the renderer (on the live channel), land in the server, and reach the agent through one of two read paths. This page covers the closed vocabulary of events, the canonical envelope, how subscriptions gate what gets delivered, and how an agent or React SDK consumer actually reads them.

For the full wire grammar of the envelope itself, see [Envelopes](/protocol/envelopes/). For the channel topology, see the [Architecture overview](/architecture/overview/).

## Event vocabulary

The protocol recognizes exactly **one** event type. Every user gesture that drives a turn is a `data:submit`. The earlier `data:change` / `lifecycle:*` / `interaction:*` / `error:*` vocabulary was deleted (draft-2026-06-12) — those types never had a producer.

```typescript
type EventType = "data:submit"; // the only member
```

A `data:submit` is schema-validated against the render's `actionSpec`.

## The envelope

Every user gesture arrives as a flat `ActionEnvelope`:

```typescript
interface ActionEnvelope<TPayload = JsonValue> {
  sessionId: string; // bound at subscribe time; server rejects mismatches
  type: EventType;
  payload?: TPayload; // for `data:submit`: { action, data?, tool? }
  clientSeq?: number; // client-monotonic dedup hint
  schemaVersion?: string; // producer's PROTOCOL_SCHEMA_VERSION (advisory)
}
```

The envelope is intentionally flat — no nested `event` / `context` / `meta` blocks. Render-level diagnostics (device info, interface context, user identity) are captured **once** at subscribe time on the server, not per-delivery. See [Envelopes — "Fields intentionally NOT on the envelope"](/protocol/envelopes/#fields-intentionally-not-on-the-envelope) for the rationale.

## Subscription gating

Delivery gating falls out of the contract's `actionSpec`: every declared action emits a `data:submit` envelope the agent reads via `ggui_consume`. The old per-event `EventSubscription` filter (an allowlist of event types at render time) and the `DEFAULT_SUBSCRIPTION` constant were deleted from the protocol (no shims) — there is no wire-level subscribe object on render, and `data:submit` is now the only event type.

The current `ggui_render` input is `{handshakeId, props, themeId?, infra?, override?}` — `props` is required (pass `{}` when the contract declares no propsSpec), and there is no `subscribe` field.

## Two reader paths, one envelope

The same `ActionEnvelope` reaches consumers through two distinct seams. **Don't conflate them.**

| Consumer                               | Path                                      | Shape returned      |
| -------------------------------------- | ----------------------------------------- | ------------------- |
| **Agent** (server-side, LLM-driven)    | Long-polls `ggui_consume` over MCP        | `ConsumeEventEntry` |
| **Renderer SDK** (browser, e.g. React) | Live tail on the WebSocket subscribe seam | `ActionEnvelope`    |

`ggui_consume` is the agent's read path. It's render-keyed, consume-once, and the row shape (`ConsumeEventEntry`) carries a tiny bit of extra context the LLM needs to route the gesture. The WebSocket subscribe seam is what the iframe-runtime uses to deliver interaction events into the rendered component. See [MCP Protocol — Events](/api/mcp-protocol/#events) for both shapes.

### Host relay + the `ai.ggui/userAction` doorbell

In MCP-Apps hosts the gesture reaches the server via the host's `ggui_runtime_submit_action` `tools/call` relay instead of the WS. If the response reports `consumerPresent: false` (no `ggui_consume` long-poll is draining — e.g. after a page reload), the iframe emits a `ui/message` whose text directs the agent to call `ggui_consume({sessionId})`, with an optional structured mirror on `content[0]._meta["ai.ggui/userAction"]` — a pure pointer; the gesture itself is only ever drained via `ggui_consume`.

## The MCP control surface

The agent never talks to the live channel directly — it renders UI, polls events, discovers gadgets, and browses the blueprint marketplace over MCP. The canonical agent-callable tool surface (lifecycle, capability discovery, stream emit, and blueprint marketplace) is enumerated in the [MCP Protocol](/api/mcp-protocol/) reference — that page is the single source of truth, with field-level shapes and return types. Linking out instead of duplicating here keeps this page from drifting as the tool surface evolves.

The `ggui_consume` long-poll in particular is the agent's event read path — its return shape (`ConsumeEventEntry`) is covered in [MCP Protocol — Events](/api/mcp-protocol/#events).

## Outbound traffic on the same channel

`ActionEnvelope` is the inbound half of the live channel. Server-to-renderer traffic on the same WebSocket arrives as `StreamEnvelope` (one delivery per named `streamSpec` channel). Contract violations are not a separate channel: an invalid inbound action is answered with a typed `error` frame (`CONTRACT_VIOLATION`) on the live channel, and a `ggui_render` / `ggui_emit` validation failure rejects the agent's own tool call. The former `_ggui:contract-error` channel and its `ContractErrorPayload` vocabulary were removed (draft-2026-06-11). Both surviving shapes are covered in [Envelopes](/protocol/envelopes/).

## Generation progress events

While the server is generating a fresh UI (no blueprint hit), it emits progress on the reserved `_ggui:lifecycle` channel (a `{type:'data'}` frame) that the renderer surfaces as a loading state. The canonical `GguiLifecyclePayload` vocabulary lives in `packages/protocol/src/types/lifecycle.ts`:

```
handshake_started → handshake_completed → render_started → consume_polling
```

These feed the built-in progress UI inside the rendered iframe (the iframe-runtime) — your end-user sees real-time feedback instead of a frozen iframe.

## React SDK integration

In the web consumer path the live-channel WebSocket — and the progress events above — are owned by the **iframe-runtime inside the sandboxed `<AppRenderer>`**, not by host code. The host doesn't open the socket, set a `wsEndpoint`, or handle `ActionEnvelope`s directly: the `wsUrl` is server-stamped on the render's `ai.ggui/render` slice, the iframe connects + subscribes + resumes on its own, and the progress UI animates inside the iframe.

```tsx
import { AppRenderer } from "@mcp-ui/client";
import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers";

// Mount the render; the iframe-runtime drives the live channel + progress UI.
const { sessions, handleAppMessage } = useMcpAppsChat({ chatEndpoint });
// <AppRenderer ... onMessage={handleAppMessage} />
```

Structured signals the host may want (dispatch telemetry, subscribe failures, version mismatches, auth-required) surface on the `ggui:observe` postMessage channel. See [React SDK](/sdk/react/) for the host surface and [Error Handling → renderer-side faults](/cookbook/error-handling/#renderer-side-faults-stay-inside-the-iframe) for the observability events.

## Where to next

- [Envelopes](/protocol/envelopes/) — canonical live-channel wire grammar
- [MCP Protocol](/api/mcp-protocol/) — agent-side control plane
- [WebSocket Protocol](/api/websocket-protocol/) — renderer-side live channel
- [Architecture overview](/architecture/overview/) — three-channel topology