What's New in Mastra (Late 2026): Harness, AgentController GA, Eval Gates and MCP 2.x

TL;DR
Mastra in late 2026: goals, schedules, signals, AgentController GA, eval gates and MCP 2.x, with type-checked TypeScript examples and honest tradeoffs.
Mastra is an open-source TypeScript framework for agents, workflows, memory and evals. It has grown from a set of agent primitives into a runtime for long-running agents: goals, cron schedules, signals, background tasks, and an AgentController that went GA on October 6, 2026. Our verdict: for TypeScript teams it is the most complete batteries-included option available right now. The cost is a fast-moving API (15 minor releases of @mastra/core in six weeks), and several of the newest pieces are still labelled beta.
Last updated: October 17, 2026
Every code example below was type-checked with tsc --noEmit (strict, moduleResolution: bundler) against @mastra/core@1.78.0, @mastra/memory@1.38.0, @mastra/libsql@1.26.2, @mastra/evals@1.10.7 and @mastra/mcp@2.4.1. No API keys were used, so none of them were run against a live model. If you have never set Mastra up, start with our Mastra review and setup guide. It covers create-mastra, Studio, the core primitives and Platform pricing, so this post skips all of that.
The feature timeline#
| Feature | Added in | Status |
|---|---|---|
| Background tasks | @mastra/core 1.29.0 | Released, off by default |
Code mode (createCodeMode) | 1.38.0 | Released |
| Signals | 1.39.0 | Released |
| Goals | 1.43.0 | Released |
| Durable agents | 1.45.0 | Beta |
createCodingAgent() | 1.48.0 | Released |
| Schedules | 1.50.0 | Released |
| Classifier primitive | 1.69.0 | Released |
| Error processors on by default | 1.73.0 | Released, on by default |
@mastra/mcp 2.0 | September 22, 2026 | Released |
| AgentController GA | October 6, 2026 | GA |
Agent scopes, beforeApproval | 1.78.0 | Released |
The release pace is the first thing to plan around. According to the npm registry, @mastra/core 1.64.0 shipped on September 3, 2026 and 1.78.0 on October 15, 2026, which is 15 minor releases in about six weeks. 1.79.0 is already in alpha.
The harness: agents that outlive one request#
Mastra's docs use "harness" for a set of capabilities that manage an agent beyond a single uninterrupted run: durable agents, background tasks, goals, schedules, signals, signal providers (GitHub, Slack and CI events forwarded into threads), AgentController and createCodingAgent(). The important design choice is that you can adopt them one at a time. A plain Agent with memory still works. You add a goal when you want the agent to keep going, a schedule when you want it to wake up on its own, and so on.
%% title: Mastra harness layers
flowchart TD
U[User or client] --> AC[AgentController session]
AC --> A[Agent loop]
S[Schedules] -->|cron fire| SIG[Signals]
EXT[GitHub, Slack, CI] -->|signal providers| SIG
SIG -->|steer or wake| A
A --> G{Goal judge}
G -->|not done| A
G -->|done or maxRuns| END[Stop]
A -->|slow tool| BG[Background task]
BG -->|result to memory| A
A --> D[(Storage: threads, snapshots)]Note the storage box at the bottom: goals, schedules, background tasks and threads all persist there. That point comes back in the tradeoffs section.
Goals#
A goal (added in 1.43.0) is a durable objective scoped to a thread and stored in thread state. On each loop iteration an LLM judge scores the work so far. The loop continues while the objective is unsatisfied and budget remains. When the judge is satisfied, the objective is marked done. When runsUsed reaches maxRuns, it is marked paused. The defaults are maxRuns: 50 and a judge maxSteps of 5.
// goal-agent.ts
import { Agent } from '@mastra/core/agent';
import { Mastra } from '@mastra/core/mastra';
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
const storage = new LibSQLStore({ id: 'app-store', url: 'file:./mastra.db' });
export const migrator = new Agent({
id: 'migrator',
name: 'Migrator',
instructions: 'You migrate test files from Jest to Vitest, one file per step.',
model: 'anthropic/claude-sonnet-5',
memory: new Memory(),
goal: {
judge: 'anthropic/claude-haiku-4-5', // no judge = the goal step is a no-op
maxRuns: 20, // default is 50; the objective is marked "paused" when exhausted
},
});
export const mastra = new Mastra({ agents: { migrator }, storage });
export async function run() {
const agent = mastra.getAgent('migrator');
const thread = { threadId: 'migration-42', resourceId: 'repo-acme-web' };
await agent.setObjective(
'Every *.test.ts file under src/ imports from vitest and none import from @jest/globals.',
thread,
);
const stream = await agent.stream('Start the migration.', { memory: { thread: thread.threadId, resource: thread.resourceId } });
for await (const chunk of stream.fullStream) {
if (chunk.type === 'goal') console.log('judge:', chunk.payload);
}
}
Two details matter. First, with no judge model resolved, the goal step is a no-op. Set goal.judge on the agent, or pass judgeModelId to setObjective(). Second, setObjective() needs a memory-backed thread and a storage instance, and it is a no-op without them. Each evaluation emits a typed goal chunk on the stream, so a UI can show the judge's verdict as it happens. Write objectives the way you would write a test assertion. "Every test file imports from vitest" can be checked. "Improve the tests" cannot.
Schedules#
Schedules (1.50.0) run an agent on a cron cadence. They are persisted, so they survive restarts and redeploys, and you manage them with CRUD methods on mastra.schedules. There are two kinds.
// schedules.ts
import { mastra } from './goal-agent';
export async function createSchedules() {
// Threadless: an isolated agent.generate() run every weekday morning.
await mastra.schedules.create({
agentId: 'migrator',
cron: '0 9 * * 1-5',
timezone: 'Europe/Berlin',
prompt: 'Report which test files still import from @jest/globals.',
});
// Threaded: the fire becomes a signal into an existing conversation.
await mastra.schedules.create({
agentId: 'migrator',
cron: '@daily',
prompt: 'Daily check-in: continue the migration where you left off.',
threadId: 'migration-42',
resourceId: 'repo-acme-web',
ifActive: { behavior: 'discard' }, // a run is already going - skip this fire
ifIdle: { behavior: 'wake' }, // thread is quiet - start a new run
});
return mastra.schedules.list({ agentId: 'migrator' });
}
A schedule without a threadId is a one-off agent.generate() with no conversation attached. That suits reports. A schedule with threadId and resourceId sends a signal into that thread, and ifActive / ifIdle decide what happens depending on whether a run is already in progress. The ifActive behaviors are deliver, persist and discard. The ifIdle behaviors are wake, persist and discard. Cron strings take 5, 6 or 7 parts plus croner nicknames such as @daily, and timezone takes an IANA name. Your storage adapter has to implement the schedules domain.
Signals#
Signals (1.39.0) let you talk to an agent while it is working. Each of the three entry points answers a different question:
sendMessage()is delivered into a running loop, or starts a run if the thread is idle.queueMessage()waits for the active run to finish, then starts a new run.sendSignal()is for system context rather than user text. 1.78.0 addedqueueSignal(), which queues a signal the same way.
%% title: sendMessage vs queueMessage
sequenceDiagram
participant C as Client
participant T as Thread
participant L as Agent loop
C->>T: sendMessage(text)
alt run is active
T->>L: inject at next loop boundary
else thread is idle
T->>L: start a new run
end
C->>T: queueMessage(text)
T-->>T: wait for active run to finish
T->>L: start the next run// steer.ts
import { mastra } from './goal-agent';
export async function steer() {
const agent = mastra.getAgent('migrator');
const target = { threadId: 'migration-42', resourceId: 'repo-acme-web' };
const sub = await agent.subscribeToThread(target);
// Delivered into the running loop at the next boundary, or starts a run if idle.
const steerResult = agent.sendMessage('Skip the e2e/ folder, it is being deleted.', target);
console.log(await steerResult.accepted); // { action: 'deliver' | 'wake' | ... }
// Waits for the active run to finish, then starts a fresh run.
agent.queueMessage('When you are done, open a summary of what changed.', target);
for await (const chunk of sub.stream) {
if (chunk.type === 'finish') break;
}
sub.unsubscribe();
}
The timing rule is worth knowing. Pending input interrupts a model request that has so far produced only reasoning or metadata, and that interrupted request does not count toward maxSteps. Once text or tool calls start streaming, the signal waits for the next loop boundary. In practice, a correction lands quickly while the model is still thinking, and does not cut into a tool call that is already streaming.
Background tasks#
Background tasks (1.29.0) let a slow tool run without blocking the conversation. They are off by default and need storage. You enable them on the Mastra instance, then opt individual tools in:
// support-agent.ts
import { Agent } from '@mastra/core/agent';
import { Mastra } from '@mastra/core/mastra';
import { createTool } from '@mastra/core/tools';
import { LibSQLStore } from '@mastra/libsql';
import { Memory } from '@mastra/memory';
import { z } from 'zod';
export const exportInvoices = createTool({
id: 'exportInvoices',
description: 'Builds a CSV export of invoices. Can take several minutes.',
inputSchema: z.object({ from: z.string(), to: z.string() }),
outputSchema: z.object({ url: z.string() }),
background: {
enabled: true,
defaultDisposition: 'deferred', // run in the background unless the model asks otherwise
timeoutMs: 10 * 60_000,
maxRetries: 2,
},
execute: async ({ from, to }) => ({ url: `https://files.example.com/invoices-${from}-${to}.csv` }),
});
export const supportAgent = new Agent({
id: 'support',
name: 'Support',
instructions: 'Help customers with billing questions.',
model: 'anthropic/claude-sonnet-5',
memory: new Memory(),
tools: { exportInvoices },
});
export const mastra = new Mastra({
agents: { supportAgent },
storage: new LibSQLStore({ id: 'support-store', url: 'file:./support.db' }),
backgroundTasks: {
enabled: true, // off by default
globalConcurrency: 10,
perAgentConcurrency: 3,
backpressure: 'queue',
defaultTimeoutMs: 5 * 60_000,
},
});
The model also gets a _background field on eligible tools and can choose foreground, deferred or awaited for each call. defaultDisposition only sets what happens when the model does not choose. The concurrency settings are the part to tune: set perAgentConcurrency below globalConcurrency so one busy agent cannot take the whole pool.
Durability, in one paragraph#
Durable agents are still labelled beta (added in 1.45.0). Since 1.52.0 you can opt in with durable: true on the agent config instead of calling createDurableAgent(), and since 1.68.0 durable agents no longer persist "running" checkpoints by default. Crash semantics, recovery modes and the Inngest and Temporal runners are covered in depth in our durable agent frameworks comparison, so they are not repeated here.
AgentController GA: sessions for interactive agent apps#
AgentController is the layer for interactive agent apps: chat products, IDE panels, multi-user tools. It first shipped in June under the name "Harness", was renamed, and went GA on October 6, 2026. Each Session owns its conversation state, event bus and run engine. Sessions are get-or-create: calling createSession() with the same resourceId (and optional scope) returns the same live session.
// controller.ts
import { Agent } from '@mastra/core/agent';
import { AgentController } from '@mastra/core/agent-controller';
import { LibSQLStore } from '@mastra/libsql';
import { Memory } from '@mastra/memory';
const codingAgent = new Agent({
id: 'coder',
name: 'Coder',
instructions: 'You are a careful coding assistant.',
model: 'anthropic/claude-sonnet-5',
});
export const controller = new AgentController({
id: 'my-coding-app',
storage: new LibSQLStore({ id: 'controller-store', url: 'file:./controller.db' }),
memory: new Memory(),
agent: codingAgent,
modes: [
{ id: 'plan', name: 'Plan', instructions: 'Read and propose. Do not edit files.', transitionsTo: 'build' },
{ id: 'build', name: 'Build', instructions: 'Implement the approved plan.' },
],
defaultModeId: 'plan',
});
export async function handleRequest(userId: string, worktree: string, text: string) {
await controller.init();
// Get-or-create: same resourceId + scope returns the same live session.
const session = await controller.createSession({ resourceId: `user:${userId}`, scope: worktree });
const off = session.subscribe((event) => {
if (event.type === 'message_update') process.stdout.write('.');
});
await session.sendMessage({ content: text, author: { id: userId } });
await session.mode.switch({ modeId: 'build' }); // same thread, different operating profile
off();
// Read paths that never construct a session:
return controller.queryThreads({ resourceId: `user:${userId}` });
}
A few things to understand before you ship on it:
- Modes share one thread. Plan and build are operating profiles layered over one backing agent, so switching modes does not lose context.
- Not everything persists. Threads persist through storage.
session.state, permission grants, pending approvals and active runs do not survive process recreation. If your host restarts, users keep their history but have to re-approve anything that was pending. - Reads are cheap.
queryThreads(),queryThreadById()andqueryThreadMessages()read straight from storage without creating a session, so a GET endpoint does not start a workspace as a side effect. - Attribution and ownership. 1.77.0 added authenticated message authors (pass
authortosendMessageorqueueMessage) andsession.thread.transferOwnership(). 1.78.0 added abeforeApprovalhook that can reject an approval-gated tool call before the agent pauses, so people are only asked to approve valid calls. - Two kinds of "scope". The
scopeoncreateSession()is an isolation key within a resource (one session per git worktree, for example). The agent scopes added in 1.78.0 are different: strings in the form<type>:<value>, such asorg:acmeorresource:user-123, passed per call, set on the agent, or set by trusted middleware through theMASTRA_SCOPES_KEYrequest-context key.
Mastra's GA post reports that after the switch to batched snapshots and deltas, memory use in long-running Mastra Code processes dropped from 2-20 GB to 300-750 MB. That is Mastra's own measurement, but it is the kind of figure that tells you the session layer was rebuilt with long-lived processes in mind.
Mastra Code and Mastra Factory are the flagship apps built on AgentController. Factory entered beta on September 8, 2026, after an alpha in July. It is open source, connects GitHub, Linear and Slack to configurable boards, and installs with npm create factory@latest. Mastra says it now writes 25-35% of the project's PRs and closes 50-60% of its issues. The charts in the same post show 277 of 1,627 merged PRs (17.0%) and 222 of 778 closed issues (28.5%) attributed to Factory. Both sets of numbers are self-reported, and the headline range is higher than the charted totals.
Multi-agent: supervisors replace networks#
Agent networks (.network()) are deprecated and will be removed in a future major release. Mastra recommends supervisor agents instead: add subagents to the parent's agents property and call the parent with stream() or generate(). Subagents have existed since 1.8.0, so this is a consolidation onto the existing pattern rather than a new one, and there is an official migration guide.
The delegation controls are where the recent work went:
onDelegationStartcan returnproceed,modifiedPromptandmodifiedMaxSteps. Since 1.76.0 it can also returnthreadMetadata, so a delegated sub-agent thread carries a tenant ID and shows up in metadata-filtered queries.delegation.enableResultReferences(1.67.0, off by default) appends a[ref: <id>]line to each successful subagent result, and gives every delegation tool acontextFromRefsinput. A later subagent can then receive an earlier result verbatim instead of the supervisor paraphrasing it.
Existing network code keeps working until the next major. Moving to supervisors means one less API to learn, and supervisors run through the same stream() / generate() path as every other agent, so there is one execution path to trace and debug.
Workflows: schedules, time travel and JSON definitions#
Workflows got three practical additions. The first is declarative schedules: you put schedule on createWorkflow() and there is no separate registration call.
// nightly-digest.ts
import { createStep, createWorkflow } from '@mastra/core/workflows';
import { z } from 'zod';
const collect = createStep({
id: 'collect',
inputSchema: z.object({ repo: z.string() }),
outputSchema: z.object({ repo: z.string(), openIssues: z.number() }),
execute: async ({ inputData }) => ({ repo: inputData.repo, openIssues: 0 }),
});
const summarize = createStep({
id: 'summarize',
inputSchema: z.object({ repo: z.string(), openIssues: z.number() }),
outputSchema: z.object({ summary: z.string() }),
execute: async ({ inputData }) => ({
summary: `${inputData.repo}: ${inputData.openIssues} open issues`,
}),
});
export const nightlyDigest = createWorkflow({
id: 'nightly-digest',
inputSchema: z.object({ repo: z.string() }),
outputSchema: z.object({ summary: z.string() }),
// Declared inline - no separate registration call. inputData is type-checked against inputSchema.
schedule: {
cron: '30 2 * * *',
timezone: 'America/New_York',
inputData: { repo: 'acme/web' },
},
})
.then(collect)
.then(summarize)
.commit();
export async function debugFromStep() {
const run = await nightlyDigest.createRun();
// Re-execute from one step using stored snapshot data (requires storage).
return run.timeTravel({ step: 'summarize', inputData: { repo: 'acme/web', openIssues: 3 } });
}
Scheduled fires run in-process on whichever host runs the scheduler. If MASTRA_WORKERS is set, scheduled workflows switch to the evented engine and need an atomic storage adapter such as @mastra/libsql or @mastra/pg. Release 1.72.0 fixed scheduled workflows hanging on serverless hosts (#18807). If you were on an older version and saw that, upgrade before you blame your cron string.
The second addition is time travel. run.timeTravel({ step, inputData }) re-executes from a chosen step using stored snapshot data or context you supply. In practice you can rerun step 4 of a failed run with corrected input, without paying for steps 1-3 again. It requires storage, and it fails before executing anything if the workflow definition has changed (a renamed step, for example). That fail-safe behavior is the right one.
The third is dynamic workflows (beta): workflow definitions expressed as JSON, persisted in storage, and able to call registered agents, tools and workflows. Release 1.67.0 added the Studio Workflow Builder backend, an editor-owned agent that writes those persisted definitions. Code-defined workflows remain the default. JSON definitions are for products where end users or operators compose flows at runtime.
Alongside these, workers (beta) move workflow step execution, cron scheduling and long-running tool calls out of the request cycle. By default they run in the same process as the API. You can split them into separate processes or containers, with pub/sub through @mastra/redis-streams, @mastra/valkey-streams or @mastra/google-cloud-pubsub.
Evals: gates, verdicts and classifiers#
Evals now give you a single verdict you can fail CI on. runEvals() takes gates (scorers that must average 1.0) and scorers with optional thresholds:
// evals.ts
import { runEvals } from '@mastra/core/evals';
import { checks } from '@mastra/evals/checks';
import { createAnswerRelevancyScorer } from '@mastra/evals/scorers/prebuilt';
import { supportAgent } from './support-agent';
export async function evaluate() {
const result = await runEvals({
target: supportAgent,
data: [
{ input: 'Export my March invoices as CSV' },
{ input: 'Export all invoices for 2025' },
],
// Gates must average 1.0 or the verdict is "failed".
gates: [checks.calledTool('exportInvoices'), checks.noToolErrors()],
// Thresholds: a number means minimum, or pass { min, max }.
scorers: [
{ scorer: createAnswerRelevancyScorer({ model: 'anthropic/claude-haiku-4-5' }), threshold: 0.8 },
{ scorer: checks.includes('csv', { ignoreCase: true }), threshold: { min: 0.5 } },
],
});
if (result.verdict === 'failed') process.exitCode = 1;
return result.verdict; // 'passed' | 'scored' | 'failed' | undefined
}
%% title: runEvals verdict
flowchart LR
D[Dataset items] --> R[Run target agent]
R --> GT[Gates must average 1.0]
R --> SC[Scorers with thresholds]
GT -->|any gate below 1.0| F[failed]
GT -->|all pass| TH{Thresholds met}
SC --> TH
TH -->|no| S[scored]
TH -->|yes| P[passed]The three-state verdict is a useful design. failed means a hard requirement broke, such as the agent not calling the export tool. scored means the hard requirements held but a quality threshold was missed. That is worth a warning, not necessarily a blocked merge. The quick checks in @mastra/evals/checks (calledTool, noToolErrors, includes, toolOrder, maxToolCalls and others) are deterministic and cost nothing to run, which makes them good gates. Since 1.68.0 a scorer can return notScorable() to leave an item out of the averages rather than scoring it zero.
The other new primitive is the Classifier (1.69.0), registered on new Mastra({ classifiers }). It comes with a ClassifierProcessor, classifier workflow steps and createClassifierScorer (1.70.0). In 1.76.0, Classifier.decide() replaced Classifier.evaluate() to match AI SDK naming, and the old name is a deprecated alias.
Classifiers also power ModelSelectionProcessor (1.70.0), which classifies a request and can downgrade it to a cheaper model. The reference page warns that routing can cost more than it saves: provider prompt caches are not shared between models, and a cheaper model can take more steps. It is good to see a framework document the downside of its own feature. Also note that its model option takes a decision model, not a 'provider/model' string.
Memory: what got sturdier#
Observational Memory (added in @mastra/memory 1.1.0, enabled with observationalMemory: true) runs Observer and Reflector background agents that compress a long session into notes. Mastra's Factory post credits it for accurate issue investigations: a coding investigation produces a lot of throwaway text, such as file contents and test output, which gets compressed into a line-by-line log of what matters. Recent releases mostly made it and the other memory types more robust:
- 1.68.0:
messageHistory: { maxTokens }caps history by token budget instead of message count. - 1.72.0:
maxRetriesandfailurePolicy: 'continue'for the Observer and Reflector, so a failed background compression does not fail the user's turn. - 1.75.0: semantic recall works with vector stores that embed text themselves, so they no longer need a client-side
embedder. - 1.76.0: concurrent schema-based working-memory updates deep-merge inside a row-locked transaction on PostgreSQL, so concurrent writes merge instead of racing.
MCP 2.x and the other connection types#
@mastra/mcp 2.0.0 was published on September 22, 2026. It serves only the MCP 2026-07-28 revision: no initialize handshake, no session header, no standalone HTTP+SSE transport and no server-initiated requests. The client side is more forgiving. It probes each server and speaks whichever revision that server offers, so your agents can still reach older MCP servers. The spec changes themselves are covered in MCP 2026-07-28 breaking changes.
The part that matters for Mastra code is how a tool asks the caller for input. Elicitation as a server-initiated request is gone. Tools call suspend() and the server returns input_required. This is the same suspendSchema / resumeSchema shape that agents and workflows already use, so one tool definition works in all three places:
// repo-tools.ts
import { createTool } from '@mastra/core/tools';
import { MCPServer } from '@mastra/mcp';
import { z } from 'zod';
export const deleteBranch = createTool({
id: 'deleteBranch',
description: 'Deletes a git branch after the caller confirms.',
inputSchema: z.object({ branch: z.string() }),
outputSchema: z.object({ deleted: z.boolean() }),
suspendSchema: z.object({ question: z.string() }),
resumeSchema: z.object({ confirm: z.boolean() }),
execute: async ({ branch }, { suspend, resumeData }) => {
if (!resumeData) {
// On an MCP 2.x server this ends the request as input_required.
await suspend?.({ question: `Delete ${branch}? This cannot be undone.` });
return { deleted: false };
}
return { deleted: resumeData.confirm };
},
});
export const server = new MCPServer({
id: 'repo-tools',
name: 'Repo tools',
version: '1.0.0',
tools: { deleteBranch },
});
%% title: MCP 2026-07-28 tool input via suspend
sequenceDiagram
participant C as MCP client
participant S as Mastra MCPServer v2
participant T as Tool
C->>S: tools/call
S->>T: execute without resumeData
T-->>S: suspend(payload)
S-->>C: input_required
C->>S: tools/call with answer
S->>T: execute with resumeData
T-->>S: result
S-->>C: resultThe upgrade rule is short. @mastra/mcp 2.x requires @mastra/core 1.76 or later. If you still need to serve clients that predate the 2026 revision, stay on 1.x. You can also register a 1.x server and a 2.x server side by side during the transition.
MCP is now one of four connection types. A2A (A2AAgent) gained v1.0 remote delegation in 1.68.0, with v0.3 still the default, so you opt in. ACP runs coding-agent processes such as Claude Code, Cline or OpenCode as Mastra tools or subagents. SDK agents (@mastra/claude, @mastra/cursor, @mastra/openai) bring a provider's own agent SDK into Mastra, with the provider SDK keeping control of the agent loop. The practical upshot: if part of your stack is already Claude Code or another vendor's agent, you can orchestrate it from Mastra rather than re-implementing it.
Observability and reliability defaults#
Every agent run, workflow step, tool call and model call produces OpenTelemetry-style spans, with duration, tokens and cost extracted automatically. Releases 1.65 through 1.78 added an advanced trace-query contract that filters on spans, scores, feedback, metadata, tags, duration and tenant scope. 1.75.0 added aggregateTraces() with token and cost measures, and 1.78.0 removed the 31-day limit on trace search ranges. Trace Intelligence, which clusters trace signals into themes, is a private beta on Mastra platform only and needs @mastra/core 1.53.0 or later.
The quieter change is reliability defaults. Since 1.73.0, three error processors are on for every agent with no configuration: ProviderHistoryCompat (repairs tool-call history when you switch providers), PrefillErrorHandler and StreamErrorRetryProcessor. Error-processor retries are capped at 3 per turn by default, and a per-call errorProcessors: [] turns them off. 1.76.0 added UnsupportedFileHandler, so a file the provider rejects no longer breaks every later turn in the thread, and default retries for Gemini's MALFORMED_FUNCTION_CALL finish. If your traces start showing retried model calls after an upgrade, these defaults are a likely reason.
Code mode (1.38.0) is worth a look if your agents make many small tool calls. createCodeMode() returns an execute_typescript tool. The model writes one function that calls your tools as external_* functions inside a Workspace sandbox. You must pass a sandbox explicitly, and LocalSandbox runs with host privileges, so use a remote sandbox for anything untrusted.
Honest tradeoffs#
1. Pace. Weekly minor releases mean you should pin exact versions, read the changelog before each bump, and upgrade @mastra/* packages together. After the June 2026 npm supply-chain incident that hit Mastra packages, lockfile discipline is not optional for any fast-moving dependency.
2. Beta labels. Durable agents, workers and dynamic workflows are beta and can change without a major release. AgentController is GA, but if you enable durability underneath it, that layer is still beta.
3. Storage carries a lot of weight. Goals, schedules, background tasks, time travel and persistent threads all need a configured storage adapter, and setObjective(), for one, is a no-op without it. Choose LibSQL or Postgres early and treat it like any other production database.
4. Surface area. Harness, AgentController, Platform, Factory, four connection types: there is a lot to learn. A plain Agent plus workflows is still a perfectly good place to stop. Adopt the harness pieces when you have the problem they solve.
5. Licensing. Most of the codebase is Apache 2.0. Anything under ee/ directories (for example @mastra/core/auth/ee, @mastra/core/agent-builder/ee, @mastra/editor/ee) is under the Mastra Enterprise License. Check your imports if that matters to your legal team.
6. Runtime floor. Node.js 22.13.0 or later, Bun, Deno or Cloudflare. Built-in deployers cover Vercel, Netlify and Cloudflare, and a Mastra server can be deployed into an ephemeral sandbox for a live preview URL.
When to pick it. Choose Mastra when you want agents, workflows, memory, evals and tracing from one TypeScript dependency with shared types, and when long-running or scheduled agents are on your roadmap. Choose the Vercel AI SDK when you mainly need a model-calling and UI layer and would rather own the orchestration yourself. Mastra already aligns parts of its API with AI SDK naming (Classifier.decide() in 1.76.0), and our AI SDK 7 production agents guide shows how far the AI SDK alone now goes. Choose LangGraph.js when explicit graph control and the LangChain ecosystem matter more than batteries-included defaults. Our Mastra vs LangGraph.js comparison goes through that decision in detail.
FAQ#
Is Mastra AgentController production ready?#
AgentController went GA on October 6, 2026, so it is no longer labelled beta. Two caveats: session state, permission grants and pending approvals do not survive a process restart (threads do), and durable agents, if you use them underneath, are still beta.
Should I migrate off agent networks now?#
Not urgently, but plan for it. .network() is deprecated and will be removed in a future major release. The replacement is a supervisor agent with subagents on its agents property, called through stream() or generate(). Mastra publishes a network-to-supervisor migration guide.
Do I need to upgrade to @mastra/mcp 2.0?#
Only if you want to serve the MCP 2026-07-28 revision. 2.x requires @mastra/core 1.76 or later and serves only the new revision. Stay on 1.x to keep serving older clients, or run 1.x and 2.x servers side by side. The 2.x client still reaches servers on older revisions.
What do goals, schedules and background tasks need to work?#
A storage adapter for all three, and the schedules feature needs one that implements the schedules domain (such as @mastra/libsql). Background tasks are also off by default and must be enabled on the Mastra instance. Goals need a judge model and a memory-backed thread. Without a judge, the goal step is a no-op.
Sources#
- https://mastra.ai/docs/harness/overview
- https://mastra.ai/docs/harness/goals
- https://mastra.ai/docs/harness/schedules
- https://mastra.ai/docs/harness/signals
- https://mastra.ai/docs/harness/background-tasks
- https://mastra.ai/docs/harness/durable-agents
- https://mastra.ai/docs/harness/agent-controller
- https://mastra.ai/blog/announcing-agent-controller-ga
- https://mastra.ai/blog/announcing-mastra-factory-beta
- https://mastra.ai/docs/subagents
- https://mastra.ai/docs/agents/networks
- https://mastra.ai/reference/migrations/network-to-supervisor
- https://mastra.ai/docs/workflows/scheduled-workflows
- https://mastra.ai/docs/workflows/time-travel
- https://mastra.ai/docs/workflows/dynamic-workflows
- https://mastra.ai/docs/deployment/workers
- https://mastra.ai/docs/evals/gates-and-verdicts
- https://mastra.ai/reference/processors/model-selection-processor
- https://mastra.ai/docs/memory/observational-memory
- https://mastra.ai/reference/migrations/mcp-v2
- https://mastra.ai/docs/connections/overview
- https://mastra.ai/docs/agents/code-mode
- https://mastra.ai/docs/observability/overview
- https://mastra.ai/docs/mastra-platform/trace-intelligence
- https://mastra.ai/docs/deployment/overview
- https://mastra.ai/docs/license
- https://github.com/mastra-ai/mastra/blob/main/packages/core/CHANGELOG.md
- https://www.npmjs.com/package/@mastra/core
Continue Reading#
- Mastra and durable TypeScript agents - where Mastra fits, and where it does not.
- The TypeScript AI agent stack in 2026 - how Mastra composes with the AI SDK, the OpenAI Agents SDK and LangGraph.js.
- AI agent frameworks compared - the broader framework map across languages.
- Choosing an Agent Framework in Late 2026: 10 Frameworks Compared by Use Case - how Mastra compares with nine other frameworks by use case.
Get the next deep dive like this in your inbox
One email a week on TypeScript and the rest of the AI dev stack. Free.
Read next on AI coding tools
Mastra: Review and Setup Guide for TypeScript Agent Apps (2026)
Mastra review and setup for TypeScript AI agents - verified create commands, Mastra Studio, workflows and memory, plus pricing and who should skip it.
8 min readDurable AI Agent Frameworks Compared: One Run, Four Stacks
One agent run crashes mid tool call, waits two days for approval, then retries a charge. How LangGraph, Mastra, Pydantic AI and OpenAI Agents SDK handle it.
14 min readMastra vs LangGraph.js: TypeScript Agent Frameworks Compared
Both Mastra and LangGraph.js are serious TypeScript agent frameworks - but they start from opposite philosophies. Here is what that means for your next project.
8 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.





