Skip to main content

Threads and state

Threads and state are how a Harness 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 Harness because they coordinate the shared thread lock and emit events. The active thread binding and thread/message reads live on the Session as harness.session.thread.

Threads
Direct link to Threads

A thread holds one conversation's full message history. The Harness 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
Direct link to Creating and selecting threads

On startup, call selectOrCreateThread() to resume the most recent thread or create a new one:

src/mastra/harness.ts
await harness.init()
const thread = await harness.selectOrCreateThread()

Create a thread explicitly with a title:

const thread = await harness.createThread({ title: 'New conversation' })

Switching threads
Direct link to Switching threads

Switch to an existing thread. The harness aborts any in-progress generation, acquires a lock on the new thread, and emits a thread_changed event:

await harness.switchThread({ threadId: 'thread-abc123' })

Listing threads
Direct link to Listing threads

List threads for the current resource. Forked subagent threads are hidden by default:

const threads = await harness.session.thread.list()

// Include all resources
const allThreads = await harness.session.thread.list({ allResources: true })

Cloning threads
Direct link to Cloning threads

Clone a thread to create a branch of the conversation. The harness copies all messages and switches to the clone:

const cloned = await harness.cloneThread({ title: 'Alternative approach' })

Thread locking
Direct link to Thread locking

Pass a threadLock to the Harness constructor to prevent concurrent access from multiple processes. The lock is acquired before any thread operation and released on switch or delete:

src/mastra/harness.ts
const harness = new Harness({
id: 'my-agent',
threadLock: {
acquire: async threadId => {
/* acquire lock or throw */
},
release: async threadId => {
/* release lock */
},
},
})

State
Direct link to 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
Direct link to Defining a state schema

Pass a stateSchema (Standard JSON Schema) to validate state and extract defaults:

src/mastra/harness.ts
import { Agent } from '@mastra/core/agent'
import { Harness } from '@mastra/core/harness'
import { z } from 'zod'

const agent = new Agent({
name: 'assistant',
instructions: 'Help the user manage a stateful session.',
model: 'openai/gpt-5.5',
})

const harness = new Harness({
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
Direct link to Reading and writing state

State is owned by the Session as harness.session.state:

// Read the current state snapshot
const state = harness.session.state.get()

// Update state — validates against schema and emits state_changed
await harness.session.state.set({ theme: 'light' })

State changes emit a state_changed event with the new state and the set of changed keys.

Resource IDs
Direct link to Resource IDs

Threads are scoped to a resource ID, which groups a conversation's threads by project, user, or workspace. Set it on the Harness constructor; it defaults to the harness id when omitted:

const harness = new Harness({
id: 'my-agent',
resourceId: 'project-xyz',
})

The resource ID is part of a conversation's identity, so you read it from the Session:

const resourceId = harness.session.identity.getResourceId()