# Threads and state Threads and state are how an AgentController conversation survives beyond a single exchange. A **thread** is the persistent record of a conversation — its messages and metadata, saved to storage so a user can close the app and resume the same conversation later, or switch between several conversations. **State** is structured data attached to the conversation — values like model preferences, feature flags, or progress — that agents and your UI read and write as the conversation runs. The two work together: the thread is the message history, and state is the shared scratchpad alongside it. Both persist across mode switches, model changes, and restarts, so nothing is lost when a user switches from plan mode to build mode or reopens the app the next day. Thread _lifecycle_ transitions — create, switch, clone, delete — live on the AgentController because they coordinate the shared thread lock and emit events. The active thread binding and thread/message _reads_ live on the [`Session`](https://mastra.ai/docs/agent-controller/session) as `agentController.session.thread`. ## Threads A thread holds one conversation's full message history. The AgentController binds the Session to one active thread at a time; messages you send and the agent's replies are appended to that thread and saved to storage. Threads let users resume a past conversation, keep several conversations side by side, or branch one into alternatives. ### Creating and selecting threads On startup, call `selectOrCreateThread()` to resume the most recent thread or create a new one: ```typescript await agentController.init() const thread = await agentController.selectOrCreateThread() ``` Create a thread explicitly with a title: ```typescript const thread = await agentController.createThread({ title: 'New conversation' }) ``` ### Switching threads Switch to an existing thread. The agentController aborts any in-progress generation, acquires a lock on the new thread, and emits a `thread_changed` event: ```typescript await agentController.switchThread({ threadId: 'thread-abc123' }) ``` ### Listing threads List threads for the current resource. Forked subagent threads are hidden by default: ```typescript const threads = await agentController.session.thread.list() // Include all resources const allThreads = await agentController.session.thread.list({ allResources: true }) ``` ### Cloning threads Clone a thread to create a branch of the conversation. The agentController copies all messages and switches to the clone: ```typescript const cloned = await agentController.cloneThread({ title: 'Alternative approach' }) ``` ### Thread locking Pass a `threadLock` to the AgentController constructor to prevent concurrent access from multiple processes. The lock is acquired before any thread operation and released on switch or delete: ```typescript const agentController = new AgentController({ id: 'my-agent', threadLock: { acquire: async threadId => { /* acquire lock or throw */ }, release: async threadId => { /* release lock */ }, }, }) ``` ## State Where a thread stores the conversation's messages, state stores structured values that describe the conversation but aren't messages — model preferences, feature flags, UI settings, or progress markers. Agents can read and update state during a run, and your UI can react to changes, so state is how the agent and the interface stay in sync on shared facts. You define its shape with a schema, and every update is validated against that schema before it's applied. ### Defining a state schema Pass a `stateSchema` (Standard JSON Schema) to validate state and extract defaults: ```typescript import { Agent } from '@mastra/core/agent' import { AgentController } from '@mastra/core/agent-controller' import { z } from 'zod' const agent = new Agent({ name: 'assistant', instructions: 'Help the user manage a stateful session.', model: 'openai/gpt-5.5', }) const agentController = new AgentController({ id: 'stateful-agent', agent, modes: [{ id: 'default', name: 'Default', metadata: { default: true } }], stateSchema: z.object({ currentModelId: z.string().optional(), theme: z.enum(['light', 'dark']).default('dark'), }), }) ``` ### Reading and writing state State is owned by the [`Session`](https://mastra.ai/docs/agent-controller/session) as `agentController.session.state`: ```typescript // Read the current state snapshot const state = agentController.session.state.get() // Update state — validates against schema and emits state_changed await agentController.session.state.set({ theme: 'light' }) ``` State changes emit a `state_changed` event with the new state and the set of changed keys. ## Resource IDs Threads are scoped to a resource ID, which groups a conversation's threads by project, user, or workspace. Set it on the AgentController constructor; it defaults to the agentController `id` when omitted: ```typescript const agentController = new AgentController({ id: 'my-agent', resourceId: 'project-xyz', }) ``` The resource ID is part of a conversation's identity, so you read it from the Session: ```typescript const resourceId = agentController.session.identity.getResourceId() ``` The session also has a stable `id` and `ownerId` (read with `session.identity.getId()` and `session.identity.getOwnerId()`). Unlike the resource ID, these don't change when you switch resources — see [Session identity](https://mastra.ai/docs/agent-controller/session) for details. ## Related - [AgentController overview](https://mastra.ai/docs/agent-controller/overview) - [Session](https://mastra.ai/docs/agent-controller/session) - [Modes](https://mastra.ai/docs/agent-controller/modes) - [API reference](https://mastra.ai/reference/agent-controller/agent-controller-class)