Guide
Building Agents with Harness
The Mastra Harness class orchestrates multiple agent modes, shared state, memory, and storage into a single runtime. Think of it as the control layer between your UI and your agents.
What is Harness?
A plain Agent handles one conversation in one mode. The Harness wraps multiple agents and adds what production apps need:
- Modes — switch between "plan," "build," "research" agents mid-conversation
- Shared state — validated with a schema, persisted across sessions
- Thread management — create, switch, list conversation threads
- Tool approvals — human-in-the-loop for dangerous operations
- Permissions — grant/deny tool categories per session
- Subagents — spawn focused agents that fork the parent context
- Observational memory — background consolidation of conversation history
Agent for simple single-turn or chat use cases. Use Harness when you need modes, persistent threads, tool approvals, or subagent delegation.Setup
Install the packages:
pnpm add @mastra/core @mastra/memory @mastra/libsqlCreate your first harness with a single mode:
import { Harness } from '@mastra/core/harness';
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
const storage = new LibSQLStore({
url: process.env.DATABASE_URL ?? 'file:./local.db',
});
const memory = new Memory({
storage,
options: {
lastMessages: 30,
workingMemory: { enabled: true },
},
});
const planAgent = new Agent({
id: 'plan',
name: 'Planner',
instructions: 'You break down tasks into clear steps.',
model: 'openai/gpt-4o-mini',
});
const buildAgent = new Agent({
id: 'build',
name: 'Builder',
instructions: 'You implement code based on a plan.',
model: 'openai/gpt-4o-mini',
});
export const harness = new Harness({
modes: [
{ id: 'plan', agent: planAgent, default: true },
{ id: 'build', agent: buildAgent },
],
storage,
memory,
});Modes
Modes are agent configurations the harness can switch between. Each mode has its own agent with distinct instructions, tools, and model. The user (or your UI) triggers switches.
import { harness } from '@/mastra/harness';
export async function POST(req: Request) {
const { message, threadId, mode } = await req.json();
await harness.init();
await harness.selectOrCreateThread(threadId);
// Switch mode if requested
if (mode) {
await harness.switchMode({ modeId: mode });
}
const response = await harness.sendMessage({ content: message });
return Response.json({ text: response.text });
}The harness preserves conversation context across mode switches — the builder agent sees what the planner discussed.
Tools & Permissions
Register tools globally (available to all modes) or per-mode. The permission system controls which tools run automatically vs. which need user approval.
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
const fileTool = createTool({
id: 'write-file',
description: 'Write content to a file',
inputSchema: z.object({
path: z.string(),
content: z.string(),
}),
outputSchema: z.object({ success: z.boolean() }),
execute: async (inputData) => {
// In production: write to workspace filesystem
console.log(`Writing ${inputData.path}`);
return { success: true };
},
});
export const harness = new Harness({
modes: [
{ id: 'plan', agent: planAgent, default: true },
{ id: 'build', agent: buildAgent },
],
tools: { fileTool },
storage,
memory,
});
// After init, set permission policies:
// await harness.grantSessionCategory({ category: 'read' });
// await harness.setPermissionForTool({ toolId: 'write-file', policy: 'ask' });When a tool has policy: 'ask', the harness pauses execution and emits an approval event. Your UI presents the request, the user approves/denies, and you call respondToToolApproval().
Memory & State
The harness manages two persistence layers: memory (conversation history, semantic recall, working memory) and state (application-level data like current model, user preferences).
import { toJsonSchema } from 'zod-to-json-schema';
const stateSchema = toJsonSchema(z.object({
currentModelId: z.string().default('openai/gpt-4o-mini'),
projectName: z.string().default('untitled'),
lastActivity: z.string().optional(),
}));
export const harness = new Harness({
modes: [
{ id: 'plan', agent: planAgent, default: true },
{ id: 'build', agent: buildAgent },
],
storage,
memory,
stateSchema,
});
// Read/write state:
// const state = await harness.getState();
// await harness.setState({ projectName: 'my-agent' });observationalMemory: { enabled: true } in the memory config. This keeps the context window lean as conversations grow.Human-in-the-Loop
Three interaction points where the harness pauses for human input:
- Tool approvals — agent wants to run a guarded tool, user approves/denies
- Questions — agent uses
ask_usertool, user answers - Plan approvals — agent submits a plan, user approves before execution
import { harness } from '@/mastra/harness';
export async function POST(req: Request) {
const { type, id, decision, answer } = await req.json();
switch (type) {
case 'tool':
await harness.respondToToolApproval({
toolCallId: id,
decision, // 'approve' | 'deny'
});
break;
case 'question':
await harness.respondToQuestion({
questionId: id,
answer,
});
break;
case 'plan':
await harness.respondToPlanApproval({
planId: id,
decision,
});
break;
}
return Response.json({ ok: true });
}Subagents
Spawn focused agents for specific subtasks. A forked subagent clones the parent thread so it has full conversation context — and preserves prompt-cache hits.
const researchSubagent = new Agent({
id: 'research-sub',
name: 'Researcher',
instructions: 'You research topics using web search. Return facts only.',
model: 'openai/gpt-4o-mini',
tools: { searchTool },
});
export const harness = new Harness({
modes: [
{ id: 'plan', agent: planAgent, default: true },
{ id: 'build', agent: buildAgent },
],
subagents: [
{
agent: researchSubagent,
forked: true, // inherits parent conversation context
},
],
tools: { fileTool },
storage,
memory,
});The main agent can delegate to a subagent by calling a tool — or your UI can explicitly dispatch to one. Forked subagent threads are hidden from listThreads() by default to keep the thread list clean.
Full Example
Putting it all together — a harness with two modes, tools with permissions, memory, state, and a subagent:
import { Harness } from '@mastra/core/harness';
import { Agent } from '@mastra/core/agent';
import { createTool } from '@mastra/core/tools';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
import { z } from 'zod';
import { toJsonSchema } from 'zod-to-json-schema';
// Storage
const storage = new LibSQLStore({
url: process.env.DATABASE_URL ?? 'file:./local.db',
});
// Memory with observational memory
const memory = new Memory({
storage,
options: {
lastMessages: 30,
workingMemory: { enabled: true },
semanticRecall: { topK: 5 },
observationalMemory: { enabled: true, consolidateAfter: 25 },
},
});
// Tools
const searchTool = createTool({
id: 'web-search',
description: 'Search the web',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.object({ results: z.string() }),
execute: async (inputData) => {
const res = await fetch('https://api.tavily.com/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: inputData.query,
max_results: 5,
api_key: process.env.TAVILY_API_KEY,
}),
});
const data = await res.json();
return {
results: (data.results ?? [])
.map((r: { title: string; content: string }) =>
`- ${r.title}: ${r.content?.slice(0, 200)}`)
.join('\n'),
};
},
});
const fileTool = createTool({
id: 'write-file',
description: 'Write a file to the project',
inputSchema: z.object({ path: z.string(), content: z.string() }),
outputSchema: z.object({ success: z.boolean() }),
execute: async (inputData) => {
// workspace write in production
return { success: true };
},
});
// Agents
const planner = new Agent({
id: 'plan',
name: 'Planner',
instructions: `You are a planning agent. Break tasks into clear steps.
Never write code directly — delegate to the builder.`,
model: 'openai/gpt-4o-mini',
});
const builder = new Agent({
id: 'build',
name: 'Builder',
instructions: `You implement code. Use write-file to create files.
Follow the plan from the planner. Be precise.`,
model: 'openai/gpt-4o-mini',
});
const researcher = new Agent({
id: 'research-sub',
name: 'Researcher',
instructions: 'Research topics. Return concise facts with sources.',
model: 'openai/gpt-4o-mini',
tools: { searchTool },
});
// State schema
const stateSchema = toJsonSchema(z.object({
currentModelId: z.string().default('openai/gpt-4o-mini'),
projectName: z.string().default('untitled'),
}));
// Harness
export const harness = new Harness({
modes: [
{ id: 'plan', agent: planner, default: true },
{ id: 'build', agent: builder },
],
subagents: [
{ agent: researcher, forked: true },
],
tools: { fileTool },
storage,
memory,
stateSchema,
});import { harness } from '@/mastra/harness';
export async function POST(req: Request) {
const { message, threadId, mode } = await req.json();
await harness.init();
await harness.selectOrCreateThread(threadId ?? 'default');
if (mode) await harness.switchMode({ modeId: mode });
// Grant read tools auto-approval, require ask for writes
await harness.grantSessionCategory({ category: 'read' });
await harness.setPermissionForTool({ toolId: 'write-file', policy: 'ask' });
const response = await harness.sendMessage({ content: message });
return Response.json({
text: response.text,
mode: harness.getCurrentModeId(),
});
}Next steps
- Browse the Mastra Patterns for copy-paste implementations
- Read the Harness API Reference for all methods
- Use Compose to bundle patterns into a deployable project