# chat.local

> Source: https://trigger.dev/docs/ai-chat/chat-local

The AI Agents and Prompts surface ships as part of the **v4.5 release candidate**. Install with `@trigger.dev/sdk@rc` (or pin `4.5.0-rc.0` or later) to use these features — they aren’t yet on the latest stable, and APIs may still change before the 4.5.0 GA. See [supported AI SDK versions](https://trigger.dev/docs/ai-chat/reference#compatibility)
 and the [AI chat changelog](https://trigger.dev/docs/ai-chat/changelog)
 for details.

Use `chat.local` to create typed, run-scoped data that persists across turns and is accessible from anywhere — the run function, tools, nested helpers. Each run gets its own isolated copy, and locals are automatically cleared between runs. Lifecycle hooks and **`run`** also receive **`ctx`** ([`TaskRunContext`](https://trigger.dev/docs/ai-chat/reference#task-context-ctx)
) — the same object as on a standard `task()` — for tags, metadata, and cleanup that needs the full run record. When a subtask is invoked via `ai.toolExecute()` (or the deprecated `ai.tool()`), initialized locals are automatically serialized into the subtask’s metadata and hydrated on first access — no extra code needed. Subtask changes to hydrated locals are local to the subtask and don’t propagate back to the parent.

[​](https://trigger.dev/docs/ai-chat/chat-local#declaring-and-initializing)

Declaring and initializing
---------------------------------------------------------------------------------------------------------

Declare locals at module level with a unique `id`, then initialize them inside a lifecycle hook where you have context (chatId, clientData, etc.):

    import { chat } from "@trigger.dev/sdk/ai";
    import { streamText, tool, stepCountIs } from "ai";
    import { anthropic } from "@ai-sdk/anthropic";
    import { z } from "zod";
    import { db } from "@/lib/db";
    
    // Declare at module level — each local needs a unique id
    const userContext = chat.local<{
      userId: string;
      name: string;
      plan: "free" | "pro";
      messageCount: number;
    }>({ id: "userContext" });
    
    export const myChat = chat.agent({
      id: "my-chat",
      clientDataSchema: z.object({ userId: z.string() }),
      onBoot: async ({ clientData }) => {
        // Initialize with real data from your database
        const user = await db.user.findUnique({
          where: { id: clientData.userId },
        });
        userContext.init({
          userId: clientData.userId,
          name: user.name,
          plan: user.plan,
          messageCount: user.messageCount,
        });
      },
      run: async ({ messages, signal }) => {
        userContext.messageCount++;
    
        return streamText({
          model: anthropic("claude-sonnet-4-5"),
          system: `Helping ${userContext.name} (${userContext.plan} plan).`,
          messages,
          abortSignal: signal,
          stopWhen: stepCountIs(15),
        });
      },
    });
    

Initialize `chat.local` in [`onBoot`](https://trigger.dev/docs/ai-chat/lifecycle-hooks#onboot)
, not `onChatStart`. `onBoot` fires on every fresh worker — including continuation runs (post-cancel, crash, `endRun`, `requestUpgrade`, OOM retry) — whereas `onChatStart` only fires on the chat’s very first message. Initializing in `onChatStart` means `run()` will crash on continuation runs with `chat.local can only be modified after initialization`.

[​](https://trigger.dev/docs/ai-chat/chat-local#accessing-from-tools)

Accessing from tools
---------------------------------------------------------------------------------------------

Locals are accessible from anywhere during task execution — including AI SDK tools:

    const userContext = chat.local<{ plan: "free" | "pro" }>({ id: "userContext" });
    
    const premiumTool = tool({
      description: "Access premium features",
      inputSchema: z.object({ feature: z.string() }),
      execute: async ({ feature }) => {
        if (userContext.plan !== "pro") {
          return { error: "This feature requires a Pro plan." };
        }
        // ... premium logic
      },
    });
    

[​](https://trigger.dev/docs/ai-chat/chat-local#accessing-from-subtasks)

Accessing from subtasks
---------------------------------------------------------------------------------------------------

When you use `ai.toolExecute()` inside AI SDK `tool()` to expose a subtask, chat locals are automatically available read-only:

    import { chat, ai } from "@trigger.dev/sdk/ai";
    import { schemaTask } from "@trigger.dev/sdk";
    import { streamText, tool } from "ai";
    import { anthropic } from "@ai-sdk/anthropic";
    import { z } from "zod";
    
    const userContext = chat.local<{ name: string; plan: "free" | "pro" }>({ id: "userContext" });
    
    export const analyzeDataTask = schemaTask({
      id: "analyze-data",
      schema: z.object({ query: z.string() }),
      run: async ({ query }) => {
        // userContext.name just works — auto-hydrated from parent metadata
        console.log(`Analyzing for ${userContext.name}`);
        // Changes here are local to this subtask and don't propagate back
      },
    });
    
    const analyzeData = tool({
      description: analyzeDataTask.description ?? "",
      inputSchema: analyzeDataTask.schema!,
      execute: ai.toolExecute(analyzeDataTask),
    });
    
    export const myChat = chat.agent({
      id: "my-chat",
      onBoot: async ({ clientData }) => {
        userContext.init({ name: "Alice", plan: "pro" });
      },
      run: async ({ messages, signal }) => {
        return streamText({
          model: anthropic("claude-sonnet-4-5"),
          messages,
          tools: { analyzeData },
          abortSignal: signal,
          stopWhen: stepCountIs(15),
        });
      },
    });
    

Values must be JSON-serializable for subtask access. Non-serializable values (functions, class instances, etc.) will be lost during transfer.

[​](https://trigger.dev/docs/ai-chat/chat-local#dirty-tracking-and-persistence)

Dirty tracking and persistence
-----------------------------------------------------------------------------------------------------------------

The `hasChanged()` method returns `true` if any property was set since the last check, then resets the flag. Use it in lifecycle hooks to only persist when data actually changed:

    onTurnComplete: async ({ chatId }) => {
      if (userContext.hasChanged()) {
        await db.user.update({
          where: { id: userContext.get().userId },
          data: {
            messageCount: userContext.messageCount,
          },
        });
      }
    },
    

[​](https://trigger.dev/docs/ai-chat/chat-local#api)

API
-----------------------------------------------------------

| Method | Description |
| --- | --- |
| `chat.local<T>({ id })` | Create a typed local with a unique id (declare at module level) |
| `local.init(value)` | Initialize with a value (call in hooks or `run`) |
| `local.hasChanged()` | Returns `true` if modified since last check, resets flag |
| `local.get()` | Returns a plain object copy (for serialization) |
| `local.property` | Direct property access (read/write via Proxy) |

Locals use shallow proxying. Nested object mutations like `local.prefs.theme = "dark"` won’t trigger the dirty flag. Instead, replace the whole property: `local.prefs = { ...local.prefs, theme: "dark" }`.

[​](https://trigger.dev/docs/ai-chat/chat-local#see-also)

See also
---------------------------------------------------------------------

*   [Lifecycle hooks](https://trigger.dev/docs/ai-chat/lifecycle-hooks)
     — `onBoot` is the canonical init site for `chat.local`.
*   [Database persistence pattern](https://trigger.dev/docs/ai-chat/patterns/database-persistence)
     — full per-hook breakdown using `chat.local` alongside DB rows.
*   [Code execution sandbox pattern](https://trigger.dev/docs/ai-chat/patterns/code-sandbox)
     — example of using `chat.local` to hold a sandbox handle across turns.
*   [Database connections](https://trigger.dev/docs/database-connections)
     — why the database client and its connection pool belong at module scope, not in `chat.local`.

Was this page helpful?

YesNo

[Previous](https://trigger.dev/docs/ai-chat/sessions)
[TypesTypeScript types for AI Agents, UI messages, and the frontend transport.\
\
Next](https://trigger.dev/docs/ai-chat/types)

⌘I

Assistant

Responses are generated using AI and may contain mistakes.
