Skip to main content

Overview

This example is a fullstack chat agent that teaches you Trigger.dev by drawing. Instead of returning paragraphs, it composes interactive UI: ask “how does a fan-out with retries work?” and it renders an animated node-graph of the flow; ask it to teach you retries and you get a short explainer, a quiz, and a gotcha callout. Every turn ends with clickable next-step chips, so the learning keeps flowing. The agent writes a sentence or two, then calls a renderVisualization tool with a json-render spec that a Next.js chat UI renders live with React Flow and a kit of shadcn/ui teaching components. The model supplies data, not markup, so every card is a few tokens and always looks right. Every fact it states is grounded on the live docs through a documentation MCP server, so it doesn’t invent API surface. Tech stack: Features:
  • Generative UI: a renderVisualization tool takes a json-render spec built from a fixed kit of components: FlowGraph, HeroCard, Quiz, Callout, Compare, Steps, Glossary, StatCard, CodeCard, DiagramCard, PromptCard. Specs are validated against the component catalog and errors are returned to the model, so it corrects the spec and retries.
  • One shared catalog: the same module generates the system-prompt component reference and validates tool calls, so the prompt and the renderer can’t drift apart.
  • Docs grounding via MCP: docs-server tools are merged into the agent each turn, so it looks up Trigger.dev APIs instead of answering from memory. Swap DOCS_MCP_URL to point the demo at any other product’s docs MCP.
  • Untrusted-input quarantine: retrieved docs are the upstream prompt-injection vector, so each tool’s output is wrapped as data-not-instructions and flagged for injection markers before the model ever sees it.
  • Versioned system prompt: defined with prompts.define(), resolvable per-run and overridable from the dashboard without redeploying. Storing it via chat.prompt.set() wires up experimental_telemetry, so every model call appears in the run trace with token, cost and latency metrics.
  • Per-turn model triage: most turns answer on a fast, low-cost model (Claude Haiku); only genuinely hard teaching turns escalate to a stronger model (Claude Sonnet), so the common case stays cheap and quick.
  • A learning flywheel: a suggestNext tool ends every turn with 2–4 next-step chips whose labels are sent verbatim as the next message, so the learner never has to invent the next question.

GitHub repo

View the Ask Trigger chat agent repo

Click here to view the full code for this project in our examples repository on GitHub. You can fork it and use it as a starting point for your own project.

How it works

The agent

The agent is a single chat.agent() call. Trigger.dev handles the chat session, turn loop, streaming and resumability. The system prompt is a versioned AI Prompt: the editable teaching guidance lives in the prompt template, while the json-render component reference is generated from the catalog at run time and injected as a template variable, so it always matches the deployed code. Setting the resolved prompt with chat.prompt.set() lets chat.toStreamTextOptions() supply the system text, model, config and telemetry:
src/trigger/trigger-chat-agent.ts
On AI SDK v5/v6, experimental_telemetry comes from the stored prompt via chat.toStreamTextOptions() — without chat.prompt.set(), model calls don’t appear as spans in the run trace.

Grounding answers on live docs with MCP

Before stating any fact, the agent looks it up through a documentation MCP server rather than answering from memory. The client and its tool set are created lazily and cached at module scope, so a successful handshake happens once per run process and every later turn reuses the same tools. A failure isn’t cached, so the turn degrades gracefully and a later turn retries. The tools are declared on the agent config, so their calls survive Trigger.dev’s cross-turn history re-conversion:
src/trigger/trigger-chat-agent.ts
Retrieved documentation is untrusted input: a poisoned page is the upstream prompt-injection vector, so each docs tool’s output is quarantined before the model reads it. Overriding toModelOutput (not execute) is the layer that decides what text the model sees, and it’s re-applied on cross-turn history re-conversion so the wrapping persists across turns:
src/trigger/trigger-chat-agent.ts
quarantineDocs (in src/lib/quarantine.ts) wraps the content in nonce-fenced “untrusted reference material” markers using the standard spotlighting defense, strips any fence markers the page itself contains, and flags common injection phrases so a review of the run trace surfaces them. Even if an injection slips through, validateSpec is the second net: the only thing the model can render is a spec built from the fixed component catalog, validated server-side before it reaches the client.

Generative UI with one shared catalog

A single module defines which components the model may use: layout and text primitives from @json-render/shadcn, plus a kit of custom teaching components. Every component is data-driven: the model fills fields and the component renders, so there’s no model-authored markup to sanitize. The same catalog produces the system-prompt reference and validates tool calls:
src/lib/catalog.ts
The renderVisualization tool accepts a flat json-render spec. Validation failures go back to the model as tool output, and the full spec is read straight off the tool call’s input by the client, so the return value only needs a short ack:
src/trigger/trigger-chat-agent.ts
A second tool, suggestNext, is the flywheel: called last on every turn with 2–4 chips (deeper, sideways, practice or topic). The client renders each chip as a button, and its label is sent verbatim as the next message when clicked, so the learning keeps flowing without the user having to think up the next question.

The Next.js chat UI

The frontend uses useChat with useTriggerChatTransport: the browser subscribes to the session’s durable streams directly, authenticated by two small server actions, so there’s no API route to maintain. renderVisualization tool parts in the message stream render through json-render’s <Renderer> with the shadcn component registry:
src/components/chat.tsx
The registry maps every catalog component to its React implementation: the stock @json-render/shadcn components plus the custom teaching kit (src/lib/registry.tsx). The main visual is FlowGraph (src/components/flow-graph.tsx): a directed node-graph on React Flow and dagre, styled like the Trigger.dev dashboard, with status dots, dashed retry edges, an animated topological reveal, and an optional timed status sequence that plays a run’s state changes live. Conversations are device-local: each has its own URL (/c/<id>) and its transcript is saved in the browser (IndexedDB), so a refresh restores the thread and the sidebar lists past chats. There’s no server database: Trigger.dev keeps the live conversation with the durable Session, so history never leaves the machine.

The session server actions

The browser talks to Trigger.dev directly; two server actions start the session and mint session-scoped tokens:
src/app/actions.ts
These session actions are unauthenticated, so anyone with a chat URL can start a session. That’s fine for a personal or gated deploy, but for real multi-user use add an ownership check in mintChatAccessToken and set an org spend limit in the dashboard (Billing). The agent’s queue.concurrencyLimit throttles throughput, not total spend.

Running it

The example needs ANTHROPIC_API_KEY (and optionally DOCS_MCP_URL) set in the Trigger.dev dashboard on the Environment Variables page, and TRIGGER_PROJECT_REF plus TRIGGER_SECRET_KEY in the local .env for the Next.js server actions:
.env
Run the agent and the app in two terminals, then open http://localhost:3000:
Try asking “How does a fan-out with retries work?” for an interactive FlowGraph, “Teach me retries properly” for an explainer, a quiz and a gotcha callout, or “How does a run survive a redeploy?” to draw the checkpoints. Then follow the next-step chips under each answer to keep going.

Relevant code

  • Agent + tools: src/trigger/trigger-chat-agent.ts: the chat.agent() definition, the versioned prompt, the docs-MCP loading and quarantine wrapping, renderVisualization and suggestNext, and the per-turn model triage
  • Docs quarantine: src/lib/quarantine.ts: spotlighting wrappers, fence-marker stripping, and injection-phrase flagging for untrusted tool output
  • Shared catalog: src/lib/catalog.ts: component definitions, prompt-reference generation, and spec validation
  • Component registry: src/lib/registry.tsx: maps catalog components to their React implementations
  • FlowGraph: src/components/flow-graph.tsx: the React Flow + dagre node-graph with the animated status sequence
  • Chat UI: src/components/chat.tsx: useChat + the Trigger chat transport, message parts, and visualization rendering
  • Server actions: src/app/actions.ts: session creation and token minting

Learn more

AI chat overview

How chat agents, sessions, and the turn loop work.

Frontend

The chat transport, session tokens, and reconnection.

AI Prompts

Versioned prompts with dashboard overrides and generation tracking.

Tools

Declaring tools on your agent and how they persist across turns.