Overview
This example is a fullstack voice chatbot built on a Trigger.dev chat agent: you speak, and it answers out loud. It synthesises the reply in chunks as the model writes it, rather than waiting for the whole answer, and the browser talks straight to Trigger.dev’s durable streams.chat.agent() task that holds the conversation across turns. Two browser audio streams bracket it: ElevenLabs Scribe turns the microphone into text, and ElevenLabs Flash turns each finished sentence of the reply back into audio. Head Start runs the first turn in the warm Next.js process while the agent boots in parallel, which cuts the latency on the first reply.
Tech stack:
- Trigger.dev AI chat for the durable conversation loop, streaming and checkpoint/resume between turns
- Head Start to run turn 1 in the web server while the agent run boots — measured ~57% off first-turn time-to-first-token
- AI SDK with Anthropic Claude Haiku for the model, and
useChaton the frontend - ElevenLabs Scribe (realtime speech-to-text) with server-side voice-activity detection for the mic
- ElevenLabs Flash (streaming text-to-speech) played sample-accurately through the Web Audio API
- Next.js app using
useTriggerChatTransport— the browser talks directly to Trigger.dev, no chat API route to maintain
- Tap the mic once, then talk: no button per turn — ElevenLabs’ voice-activity detection decides when you’ve finished a sentence and sends it.
- Replies spoken as the model writes them: each sentence is sent for synthesis as soon as it’s complete, so playback doesn’t wait for the whole answer.
- Interrupt a reply you don’t need with a button; the conversation keeps its place.
- Multi-turn memory within a sliding window, so follow-up questions make sense while input tokens (and turn latency) stay flat.
- One config file for the voice, the model, the personality, and the latency trade-offs.
- Keys stay on the server: server actions mint short-lived, session-scoped Trigger.dev tokens and single-use ElevenLabs tokens, so no API key ever reaches the browser.
GitHub repo
View the ElevenLabs Voice 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 singlechat.agent() task. Trigger.dev runs the durable conversation loop, so when the user goes quiet the run suspends and checkpoints, and the next thing they say resumes it. A short idleTimeoutInSeconds keeps it warm for a couple of minutes first, so a quick spoken follow-up doesn’t pay a cold continuation boot. prepareMessages keeps only the last few turns each turn, so input tokens and turn latency stay flat however long the conversation runs:
trigger/chat.ts
Because the trimmed prefix changes every turn, byte-exact prompt caching can never hit; the
sliding window and prompt caching are mutually exclusive. Raise
HISTORY_TURNS if you need
memory more than you need flat latency.Head Start for a fast first reply
A fresh agent run takes a moment to boot, which shows up as a slow first reply. That is the worst place for latency in a voice UI. Head Start runs turn 1’s model call in the warm Next.js process while the agent run boots in parallel, then hands the conversation to the agent for every turn after. It shares the same model, system prompt and token cap as the agent, so the voice doesn’t change character mid-handover:lib/chat-handler.ts
app/api/chat/route.ts is just export const POST = chatHandler), and the frontend transport points at it with headStart: "/api/chat".
Streaming voice I/O
Both audio sockets are opened by the browser and bracket the agent.use-scribe.ts streams the microphone to ElevenLabs Scribe, whose server-side voice-activity detection decides where your speech ends and commits a transcript, which is what lets you tap the mic once and talk. use-eleven-tts.ts takes each finished sentence of the reply and streams it to ElevenLabs Flash, playing the raw PCM through the Web Audio API scheduled sample-accurately, so speaking starts before the whole reply is written.
Keys stay on the server
The browser talks to Trigger.dev and ElevenLabs directly, so it needs short-lived tokens rather than API keys. Server actions mint them: a session-scoped Trigger.dev token that the transport refreshes on expiry, and 15-minute single-use ElevenLabs tokens for each voice socket. The secret keys stay on the server:app/actions.ts
useChat and useTriggerChatTransport, pointing the transport at the Head Start route:
app/components/voice-chat.tsx
One config file
Every model id, voice id and tuning constant lives inlib/voice-config.ts. The file stays import-light because it’s shared with the Head Start route bundle, and Head Start only pays off while that bundle stays small. The knobs that matter most:
The
SYSTEM_PROMPT in the same file keeps replies short (every reply is read aloud, so a long one is seconds of dead air) and formats punctuation and numbers for ElevenLabs Flash, which reads ellipses, dashes and raw figures unpredictably.
Running it
You’ll need accounts on Trigger.dev, Anthropic and ElevenLabs, a Chromium browser for the mic, and four values in.env.local:
.env.local
The Anthropic key is named
VOICE_ANTHROPIC_API_KEY, not ANTHROPIC_API_KEY, because the
standard name gets picked up by other tooling that can silently bill against it. The project reads
its own prefixed name via lib/model.ts.Relevant code
- The agent: trigger/chat.ts: the
chat.agent()definition, the sliding-windowprepareMessages, and the streamed reply - Head Start: lib/chat-handler.ts: turn 1 in the warm web process, sharing the agent’s model and prompt
- Config: lib/voice-config.ts: every model id, voice id, prompt and tuning constant in one place
- Server actions: app/actions.ts: Trigger.dev session + token minting, and single-use ElevenLabs tokens
- Speech-to-text: app/lib/use-scribe.ts: microphone to text via ElevenLabs Scribe with server-side VAD
- Text-to-speech: app/lib/use-eleven-tts.ts: reply text to audio, scheduled through Web Audio
- The UI: app/components/voice-chat.tsx: wires both audio streams to the agent via
useChat+ the Trigger chat transport
Learn more
AI chat overview
How chat agents, sessions, and the turn loop work.
Fast starts
Head Start: run turn 1 in your web server while the agent boots.
Frontend
The chat transport, session tokens, and reconnection.
Sessions
Durable sessions, idle timeouts, and checkpoint/resume between turns.

