Skip to content

Commit c071c72

Browse files
committed
Make clientData typesafe and pass to all chat.task hooks
1 parent 4acf58a commit c071c72

File tree

7 files changed

+150
-56
lines changed

7 files changed

+150
-56
lines changed

docs/guides/ai-chat.mdx

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export const myChat = chat.task({
256256
```
257257

258258
<Tip>
259-
`clientData` contains custom data from the frontend — either the `metadata` option on the transport constructor (sent with every message) or the `metadata` option on `sendMessage()` (per-message). See [Client data and metadata](#client-data-and-metadata).
259+
`clientData` contains custom data from the frontend — either the `clientData` option on the transport constructor (sent with every message) or the `metadata` option on `sendMessage()` (per-message). See [Client data and metadata](#client-data-and-metadata).
260260
</Tip>
261261

262262
### onTurnStart
@@ -501,13 +501,17 @@ Putting it all together — a complete chat app with server-side persistence, se
501501
import { chat } from "@trigger.dev/sdk/ai";
502502
import { streamText } from "ai";
503503
import { openai } from "@ai-sdk/openai";
504+
import { z } from "zod";
504505
import { db } from "@/lib/db";
505506

506507
export const myChat = chat.task({
507508
id: "my-chat",
508-
onChatStart: async ({ chatId }) => {
509+
clientDataSchema: z.object({
510+
userId: z.string(),
511+
}),
512+
onChatStart: async ({ chatId, clientData }) => {
509513
await db.chat.create({
510-
data: { id: chatId, title: "New chat", messages: [] },
514+
data: { id: chatId, userId: clientData.userId, title: "New chat", messages: [] },
511515
});
512516
},
513517
onTurnStart: async ({ chatId, uiMessages, runId, chatAccessToken }) => {
@@ -593,6 +597,7 @@ export function Chat({ chatId, initialMessages, initialSessions }) {
593597
const transport = useTriggerChatTransport<typeof myChat>({
594598
task: "my-chat",
595599
accessToken: getChatToken,
600+
clientData: { userId: currentUser.id }, // Type-checked against clientDataSchema
596601
sessions: initialSessions,
597602
onSessionChange: (id, session) => {
598603
if (!session) deleteSession(id);
@@ -676,21 +681,21 @@ export const myChat = chat.task({
676681

677682
## Client data and metadata
678683

679-
### Transport-level metadata
684+
### Transport-level client data
680685

681-
Set default metadata on the transport that's included in every request:
686+
Set default client data on the transport that's included in every request. When the task uses `clientDataSchema`, this is type-checked to match:
682687

683688
```ts
684-
const transport = useTriggerChatTransport({
689+
const transport = useTriggerChatTransport<typeof myChat>({
685690
task: "my-chat",
686691
accessToken: getChatToken,
687-
metadata: { userId: currentUser.id },
692+
clientData: { userId: currentUser.id },
688693
});
689694
```
690695

691696
### Per-message metadata
692697

693-
Pass metadata with individual messages. Per-message values are merged with transport-level metadata (per-message wins on conflicts):
698+
Pass metadata with individual messages via `sendMessage`. Per-message values are merged with transport-level client data (per-message wins on conflicts):
694699

695700
```ts
696701
sendMessage(
@@ -699,30 +704,52 @@ sendMessage(
699704
);
700705
```
701706

702-
### Accessing client data in the task
707+
### Typed client data with `clientDataSchema`
703708

704-
Both transport-level and per-message metadata are available as `clientData` in the `run` function and in `onChatStart`:
709+
Instead of manually parsing `clientData` with Zod in every hook, pass a `clientDataSchema` to `chat.task`. The schema validates the data once per turn, and `clientData` is typed in all hooks and `run`:
705710

706711
```ts
712+
import { chat } from "@trigger.dev/sdk/ai";
713+
import { streamText } from "ai";
714+
import { openai } from "@ai-sdk/openai";
707715
import { z } from "zod";
708716

709717
export const myChat = chat.task({
710718
id: "my-chat",
719+
clientDataSchema: z.object({
720+
model: z.string().optional(),
721+
userId: z.string(),
722+
}),
723+
onChatStart: async ({ chatId, clientData }) => {
724+
// clientData is typed as { model?: string; userId: string }
725+
await db.chat.create({
726+
data: { id: chatId, userId: clientData.userId },
727+
});
728+
},
711729
run: async ({ messages, clientData, signal }) => {
712-
const { model, userId } = z.object({
713-
model: z.string().optional(),
714-
userId: z.string(),
715-
}).parse(clientData);
716-
730+
// Same typed clientData — no manual parsing needed
717731
return streamText({
718-
model: openai(model ?? "gpt-4o"),
732+
model: openai(clientData?.model ?? "gpt-4o"),
719733
messages,
720734
abortSignal: signal,
721735
});
722736
},
723737
});
724738
```
725739

740+
The schema also types the `clientData` option on the frontend transport:
741+
742+
```ts
743+
// TypeScript enforces that clientData matches the schema
744+
const transport = useTriggerChatTransport<typeof myChat>({
745+
task: "my-chat",
746+
accessToken: getChatToken,
747+
clientData: { userId: currentUser.id },
748+
});
749+
```
750+
751+
Supports Zod, ArkType, Valibot, and other schema libraries supported by the SDK.
752+
726753
## Runtime configuration
727754

728755
### chat.setTurnTimeout()
@@ -763,7 +790,7 @@ run: async ({ messages, signal }) => {
763790
| `streamKey` | `string` | `"chat"` | Stream key (only change if using custom key) |
764791
| `headers` | `Record<string, string>` || Extra headers for API requests |
765792
| `streamTimeoutSeconds` | `number` | `120` | How long to wait for stream data |
766-
| `metadata` | `Record<string, unknown>` || Default metadata for every request |
793+
| `clientData` | Typed by `clientDataSchema` || Default client data for every request |
767794
| `sessions` | `Record<string, {...}>` || Restore sessions from storage |
768795
| `onSessionChange` | `(chatId, session \| null) => void` || Fires when session state changes |
769796
| `triggerOptions` | `{...}` || Options for the initial task trigger (see below) |
@@ -837,6 +864,7 @@ const transport = useTriggerChatTransport({
837864
|--------|------|---------|-------------|
838865
| `id` | `string` | required | Task identifier |
839866
| `run` | `(payload: ChatTaskRunPayload) => Promise<unknown>` | required | Handler for each turn |
867+
| `clientDataSchema` | `TaskSchema` || Schema for validating and typing `clientData` |
840868
| `onChatStart` | `(event: ChatStartEvent) => Promise<void> \| void` || Fires on turn 0 before `run()` |
841869
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` || Fires every turn before `run()` |
842870
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` || Fires after each turn completes |
@@ -854,7 +882,7 @@ Plus all standard [TaskOptions](/tasks/overview) — `retry`, `queue`, `machine`
854882
| `chatId` | `string` | Unique chat session ID |
855883
| `trigger` | `"submit-message" \| "regenerate-message"` | What triggered the request |
856884
| `messageId` | `string \| undefined` | Message ID (for regenerate) |
857-
| `clientData` | `unknown` | Custom data from frontend metadata |
885+
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend (typed when schema is provided) |
858886
| `signal` | `AbortSignal` | Combined stop + cancel signal |
859887
| `cancelSignal` | `AbortSignal` | Cancel-only signal |
860888
| `stopSignal` | `AbortSignal` | Stop-only signal (per-turn) |

packages/core/src/v3/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export {
8080
getSchemaParseFn,
8181
type AnySchemaParseFn,
8282
type SchemaParseFn,
83+
type inferSchemaOut,
8384
isSchemaZodEsque,
8485
isSchemaValibotEsque,
8586
isSchemaArkTypeEsque,

0 commit comments

Comments
 (0)