Skip to content

Conversation

@jakevollkommer
Copy link

Closes #2634

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Summary

Adds a custom transport implementation for AI SDK's useChat hook that integrates with Trigger.dev background tasks. This enables long-running AI conversations by triggering tasks, subscribing to realtime run updates via Electric SQL, and streaming AI responses via Server-Sent Events (SSE).

Changes

  • New Hook: useTriggerChat - A drop-in replacement for AI SDK's useChat that works with Trigger.dev tasks
  • Transport Class: TriggerChatTransport - Custom transport implementation following AI SDK's transport pattern
  • Dependencies: Added ai (^5.0.82), @ai-sdk/react (^2.0.14), and eventsource-parser (^3.0.0)

Technical Details

Architecture

Developer Requirements

Important: Developers must create their own server action to trigger tasks. Since useTriggerChat is a client-side hook, it cannot directly call tasks.trigger() (which requires server-side execution). The triggerTask option expects a server action that:

  1. Accepts the task identifier and payload
  2. Calls tasks.trigger() on the server
  3. Returns { success: true, runId, publicAccessToken }

Error Handling

  • Gracefully handles stream disconnections and abort signals
  • Warns on unparseable SSE chunks without breaking the stream
  • Only closes controller when run finishes (not when individual streams end)

Testing

Manually tested with a local project using pnpm patch to verify:

  • ✅ Task triggering and run creation
  • ✅ Realtime run status updates
  • ✅ SSE streaming of AI responses
  • ✅ Multiple concurrent streams
  • ✅ Graceful handling of stream completion
  • ✅ TypeScript compilation

Usage Example

1. Define your Trigger.dev task (e.g. src/trigger/chat.ts):

import { metadata, task } from "@trigger.dev/sdk/v3";
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

export const chatTask = task({
  id: "chat",
  run: async ({ messages }: { messages: Array<{ role: string; content: string }> }) => {
    const result = streamText({
      model: openai("gpt-5"),
      messages,
    });

    // CRITICAL: Stream the result to the client using metadata.stream()
    // The stream key MUST match the streamKey option in useTriggerChat (default: "chat")
    await metadata.stream("chat", result.toUIMessageStream());

    const text = await result.text;
    return { text };
  },
});

2. Create a server action (e.g. src/actions.ts):

"use server";

import { tasks } from "@trigger.dev/sdk/v3";

export async function triggerChatTask(task: string, payload: unknown) {
  const handle = await tasks.trigger(task, payload);
  return {
    success: true,
    runId: handle.id,
    publicAccessToken: handle.publicAccessToken,
  };
}

3. Use the hook in your component (e.g. src/components/Chat.tsx):

"use client";

import { useTriggerChat } from "@trigger.dev/react-hooks";
import { chatTask } from "../trigger/chat";
import { triggerChatTask } from "../actions";

export function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useTriggerChat({
    triggerTask: triggerChatTask,
  });

  return (
    <form onSubmit={handleSubmit}>
      <div>
        {messages.map((msg) => (
          <div key={msg.id}>
            <strong>{msg.role}:</strong> {msg.content}
          </div>
        ))}
      </div>
      <input value={input} onChange={handleInputChange} />
      <button type="submit">Send</button>
    </form>
  );
}

Changelog

Added useTriggerChat hook to @trigger.dev/react-hooks that provides AI SDK useChat integration with Trigger.dev background tasks. Enables long-running AI conversations with realtime streaming via custom transport implementation.

New exports:

  • useTriggerChat - Hook for AI chat integration
  • TriggerChatTransport - Custom transport class
  • TriggerChatTransportOptions - Transport configuration type
  • TriggerChatTaskPayload - Task payload type

New dependencies:

  • ai@^5.0.82
  • @ai-sdk/react@^2.0.14
  • eventsource-parser@^3.0.0

Resources


Screenshots

N/A - This is a developer-facing hook with no UI

💯

…ok with a custom transport for trigger.tdev tasks
@changeset-bot
Copy link

changeset-bot bot commented Oct 29, 2025

🦋 Changeset detected

Latest commit: c4f800f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@trigger.dev/react-hooks Minor
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
@trigger.dev/build Minor
@trigger.dev/core Minor
@trigger.dev/python Minor
@trigger.dev/redis-worker Minor
@trigger.dev/rsc Minor
@trigger.dev/schema-to-json Minor
@trigger.dev/sdk Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
trigger.dev Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/zod-worker Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

"check-exports": "attw --pack ."
},
"dependencies": {
"@ai-sdk/react": "^2.0.14",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we don't have to include this dependency unless devs are using the useTriggerChat hook

"@ai-sdk/react": "^2.0.14",
"@electric-sql/client": "1.0.14",
"@trigger.dev/core": "workspace:^4.0.5",
"ai": "^5.0.82",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above

Comment on lines +30 to +32
* @remarks
* **CRITICAL:** Your Trigger.dev task MUST call `metadata.stream()` with the AI SDK stream.
* The stream key used in `metadata.stream()` must match the `streamKey` option (default: "chat").
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there may be a better place to put this piece of documentation

the tricky bit here is there are 2 external files required to enable this hook

  1. the trigger.dev task definition itself, which must call a stream() method from ai and forward the stream to metadata.stream()
  2. the server action which invoked the trigger.dev task server-side

Comment on lines +34 to +53
* @example Trigger.dev task that streams AI responses:
* ```ts
* import { metadata, task } from "@trigger.dev/sdk/v3";
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
*
* export const chatTask = task({
* id: "chat",
* run: async ({ messages }) => {
* const result = streamText({
* model: openai("gpt-4"),
* messages,
* });
*
* // CRITICAL: Stream to client using metadata.stream()
* await metadata.stream("chat", result.toUIMessageStream());
*
* return { text: await result.text };
* },
* });
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the above, it's a bit awkward adding this example here, because the task parameter actually just needs the string which is the id of the task ("chat" in the above example)

still unsure if this is the best way to document this

Comment on lines +331 to +349
function parseMetadata(
metadata: Record<string, unknown> | string | undefined
): ParsedMetadata | undefined {
if (!metadata) return undefined;

if (typeof metadata === "string") {
try {
return JSON.parse(metadata) as ParsedMetadata;
} catch {
return undefined;
}
}

if (typeof metadata === "object") {
return metadata as ParsedMetadata;
}

return undefined;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normally I would use zod for this, but I think this matches patterns in other hooks

Comment on lines +481 to +547
* @remarks
* **CRITICAL SETUP REQUIREMENTS:**
*
* 1. Your Trigger.dev task MUST call `metadata.stream()` to stream responses:
* ```ts
* await metadata.stream("chat", result.toUIMessageStream());
* ```
*
* 2. You must provide a server action that calls `tasks.trigger()`:
* ```ts
* "use server";
* export async function triggerChat(task: string, payload: unknown) {
* const handle = await tasks.trigger(task, payload);
* return { success: true, runId: handle.id, publicAccessToken: handle.publicAccessToken };
* }
* ```
*
* @example Complete setup with three files:
*
* **1. Trigger.dev task (src/trigger/chat.ts):**
* ```ts
* import { metadata, task } from "@trigger.dev/sdk/v3";
* import { streamText } from "ai";
* import { openai } from "@ai-sdk/openai";
*
* export const chatTask = task({
* id: "chat",
* run: async ({ messages }) => {
* const result = streamText({ model: openai("gpt-4"), messages });
* // CRITICAL: Stream to client
* await metadata.stream("chat", result.toUIMessageStream());
* return { text: await result.text };
* },
* });
* ```
*
* **2. Server action (src/actions.ts):**
* ```ts
* "use server";
* import { tasks } from "@trigger.dev/sdk/v3";
*
* export async function triggerChat(task: string, payload: unknown) {
* const handle = await tasks.trigger(task, payload);
* return { success: true, runId: handle.id, publicAccessToken: handle.publicAccessToken };
* }
* ```
*
* **3. Client component (src/components/Chat.tsx):**
* ```ts
* "use client";
* import { useTriggerChat } from "@trigger.dev/react-hooks";
* import { triggerChat } from "../actions";
*
* export function Chat() {
* const { messages, input, handleInputChange, handleSubmit } = useTriggerChat({
* transportOptions: { triggerTask: triggerChat }
* });
*
* return (
* <form onSubmit={handleSubmit}>
* {messages.map(m => <div key={m.id}>{m.role}: {m.content}</div>)}
* <input value={input} onChange={handleInputChange} />
* <button type="submit">Send</button>
* </form>
* );
* }
* ```
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the above comments apply here as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: AI SDK ChatTransport

1 participant