UIMessage subtype with chat.withUIMessage, fix a typed clientData schema with chat.withClientData, chain builder-level hooks, and align types on the client.
Custom UIMessage with chat.withUIMessage
chat.agent() types the wire payload with the base AI SDK UIMessage. That is enough for many apps.
When you add custom data-* parts (via chat.stream / writer) or a typed tool map (e.g. InferUITools<typeof tools>), you want a narrower UIMessage generic so that:
onTurnStart,onTurnComplete, and similar hooks expose correctly typeduiMessages- Stream options like
sendReasoningalign with your message shape - The frontend can treat
useChatmessages as the same subtype end-to-end
chat.withUIMessage<YourUIMessage>(config?) returns a ChatBuilder where .agent(...) accepts the same options as chat.agent() but fixes YourUIMessage as the UI message type for that chat agent.
Defining a UIMessage subtype
Build the type from AI SDK helpers and your tools object:
tool() with execute: ai.toolExecute(schemaTask) where needed — see Task-backed AI tools.
Backend: chat.withUIMessage(...).agent(...)
Call withUIMessage once, then chain .agent({ ... }) instead of chat.agent({ ... }). You can also chain .withClientData() and hook methods before .agent():
Default stream options
The optionalstreamOptions object becomes the default uiMessageStreamOptions for toUIMessageStream().
If you also set uiMessageStreamOptions on the inner .agent({ ... }), the two objects are shallow-merged — keys on the agent win on conflicts. Per-turn overrides via chat.setUIMessageStreamOptions() still apply on top.
Frontend: InferChatUIMessage
Import the helper type and pass it to useChat so messages and render logic match the backend:
InferChatUIMessage from @trigger.dev/sdk/ai in non-React modules.
Typed client data with chat.withClientData
chat.withClientData({ schema }) returns a ChatBuilder that fixes the client data schema. Managed-agent hooks and run receive typed clientData without needing clientDataSchema in .agent() options. A .customAgent() run receives the parsed schema output in payload.metadata, and chat.createSession() yields it as turn.clientData.
.agent() and .customAgent(). Custom agents validate the initial payload and later chat.messages frames. Invalid frames are not passed to user code. Async reads emit an error chunk followed by turn-complete; chat.messages.on() reports through onClientDataValidationError and the task log so it does not end an active response. Without a schema, metadata is passed through unchanged.
withClientData also takes reportErrorAt and onValidationError alongside schema. See chat.withClientData for both, and Validating client data for the custom-agent walkthrough.
ChatBuilder
Bothchat.withUIMessage() and chat.withClientData() return a ChatBuilder — a chainable object that accumulates configuration before creating the agent with .agent().
Builder methods can be chained in any order:
Builder-level hooks
All lifecycle hooks can be set on the builder:onPreload, onChatStart, onTurnStart, onBeforeTurnComplete, onTurnComplete, onCompacted, onChatSuspend, onChatResume.
Builder hooks and task-level hooks coexist. When both are defined for the same event, the builder hook runs first, then the task hook:
When plain chat.agent() is enough
If you do not rely on custom UIMessage generics (only default text, reasoning, and built-in tool UI types), chat.agent() alone is fine — no need for withUIMessage.

