Highlights
Heartbeats (scheduled agents on cron)
New mastra.heartbeats APIs let you schedule agents to run on a recurring cron, either delivering into an existing thread as a normal signal or running threadless; schedules persist across restarts and are available in @mastra/core, @mastra/server (/api/heartbeats), and @mastra/client-js.
File-based agents & subagents (FS convention + bundler auto-registration)
You can now define agents by dropping directories under src/mastra/agents/<name>/ (with config.ts/instructions.md, tools/, skills/, optional memory.ts and workspace/ seeding), and the deployer/build pipeline will bundle + register them automatically; supports one-level subagents/<childId>/ that become delegatable tools.
Human-in-the-loop reliability: storage-backed suspended run discovery
New agent.listSuspendedRuns() reads pending approvals/suspends from storage (not memory), enabling approval UIs to recover after refresh/restart and across multiple server instances; sendToolApproval() now falls back to this discovery when no in-memory active run is found.
Observational Memory upgrades: Extractor API + OM-managed working memory
@mastra/memory adds a public Observational Memory Extractor API (inline XML extraction + structured follow-ups with built-in extractors like task/suggested response/thread title) and an option for the Observer to automatically manage working memory via observationalMemory.observation.manageWorkingMemory.
Durable/Inngest execution parity & stronger auth context propagation
InngestAgent is brought to parity with DurableAgent (execution options, abort handling, idle-aware resume, generate()/resumeGenerate()), Inngest observe() now replays buffered events for late subscribers, and Inngest workflows now support the FGA actor signal end-to-end across durable/nested boundaries.
Breaking Changes
@mastra/vercel: sandbox exports renamed—VercelSandboxnow means MicroVM-backed Vercel Sandbox; the serverless implementation is nowVercelServerlessSandbox(andVercelMicroVMSandboxis replaced byVercelSandbox).
Changelog
@mastra/core@1.48.0
Minor Changes
-
Renamed the AgentController interval API.
heartbeatHandlersis nowintervalHandlers, theHeartbeatHandlertype is nowIntervalHandler, and theremoveHeartbeat()/stopHeartbeats()methods are nowremoveInterval()/stopIntervals(). This better reflects that these are fixed-interval background tasks, not liveness pings, and is distinct from the unrelatedmastra.heartbeatsscheduled-agent feature. (#18665)Before
const { controller } = await createMastraCode({ heartbeatHandlers: [{ id: 'sync', intervalMs: 60_000, handler: async () => {} }], }); await controller.removeHeartbeat({ id: 'sync' }); await controller.stopHeartbeats();
After
const { controller } = await createMastraCode({ intervalHandlers: [{ id: 'sync', intervalMs: 60_000, handler: async () => {} }], }); await controller.removeInterval({ id: 'sync' }); await controller.stopIntervals();
-
Added optional
maxStepstoScorerJudgeConfigandGoalConfig, letting callers control the internal judge agent's agentic-loop iteration limit instead of relying on the implicit default of 5. Also fixed judge agent not receiving Mastra registration, which caused the observer's API key resolution to fail silently. (#18544) -
Added optional
maxStepstoScorerJudgeConfig, letting callers control the internal judge agent's agentic-loop iteration limit instead of relying on the implicit default of 5. (#18544) -
Added
createCodingAgentfactory and a reusablebuildBasePromptso other projects can build a coding agent on top of the same defaults MastraCode uses. (#18695)The factory wires sensible, portable defaults that you can override per field:
- Workspace — a local filesystem + sandbox rooted at
process.cwd()(setbasePath, pass your ownworkspace, or passworkspace: undefinedto opt out entirely). - Task signals —
TaskSignalProviderso a task list persists across turns. - Error handling — retries on
ECONNRESETand bad-request errors, plus prefill and provider-history compatibility processors. - Goal judging — the default goal judge prompt.
buildBasePromptis parameterized withproductName,coAuthorName(both default to "Mastra Code"), andcoAuthorEmail(defaults to "noreply@mastra.ai"), so you can brand the system prompt and commit trailer without forking it.import { createCodingAgent } from '@mastra/core/coding-agent'; const agent = createCodingAgent({ id: 'my-coding-agent', name: 'My Coding Agent', model: 'openai/gpt-5', instructions: 'You help with my project.', tools: {}, basePath: '/path/to/repo', });
- Workspace — a local filesystem + sandbox rooted at
-
Added heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. (#18184)
A heartbeat fires a prompt to an agent on a schedule. When it has a thread, the run is delivered into that thread as a normal agent signal, so anything watching the thread sees it like any other message; without a thread, the agent just runs in isolation. Each heartbeat has its own id and an optional
name, so one agent or thread can have several heartbeats with different schedules and prompts. The id is generated for you, or you can pass your ownidtocreatefor a stable handle (it's normalized tohb_<slug>). Heartbeats are persisted, so they keep firing across process restarts with no extra setup.const hb = await mastra.heartbeats.create({ agentId: 'chef', name: 'morning-checkin', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', ifActive: { behavior: 'discard' }, // skip if the user is mid-conversation ifIdle: { behavior: 'wake' }, // wake the agent if the thread is idle }); // Threadless: run the agent on a cron with no conversation. await mastra.heartbeats.create({ agentId: 'chef', cron: '0 * * * *', prompt: 'Run the hourly summary', }); await mastra.heartbeats.list({ agentId: 'chef' }); await mastra.heartbeats.get(hb.id); await mastra.heartbeats.update(hb.id, { prompt: 'check in gently' }); await mastra.heartbeats.pause(hb.id); await mastra.heartbeats.resume(hb.id); await mastra.heartbeats.run(hb.id); // fire once now await mastra.heartbeats.delete(hb.id);
The same CRUD is available over HTTP through
@mastra/server(under/api/heartbeats) and as top-level methods on the@mastra/client-jsclient (client.createHeartbeat,client.getHeartbeat,client.listHeartbeats, etc.).Lifecycle hooks
React to heartbeat runs via
heartbeaton theMastraconstructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firingagentIdso you can branch on it.prepareresolves fire-time parameters (for example, creating a fresh thread per fire), andonFinish/onError/onAbortmirroragent.stream.new Mastra({ // ... heartbeat: { // Return overrides, `null` to skip this fire, or `undefined` to use defaults. prepare: async ({ agentId, heartbeat }) => { if (agentId === 'chef' && heartbeat.name === 'daily-digest') { return { threadId: await createDailyThread(), resourceId: 'slack:U095PUH0FKL' }; } }, onFinish: ({ agentId, outcome, result, heartbeat }) => { metrics.record({ agentId, heartbeat: heartbeat.name, outcome }); }, onError: ({ agentId, error, phase, heartbeat }) => { alerts.send(`heartbeat ${agentId}/${heartbeat.name} failed in ${phase}: ${error.message}`); }, }, });
Signal shaping
A heartbeat fire surfaces to the agent as a signal. By default it uses the
notificationtype and renders as<heartbeat>…</heartbeat>; overridesignalTypeandtagNameto change either.ifActiveandifIdlemirror theagent.sendSignaloptions shape ({ behavior, attributes }, plusstreamOptionsonifIdle) and stay JSON-serializable so they persist with the schedule.ifIdle.streamOptionscurrently acceptsrequestContext, which is rehydrated onto the woken run. Top-levelattributesare rendered on the signal tag, and top-levelproviderOptionsare merged into the signal payload on every fire.await mastra.heartbeats.create({ agentId: 'chef', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', tagName: 'check-in', // renders as <check-in>…</check-in> attributes: { source: 'cron' }, providerOptions: { openai: { store: false } }, ifIdle: { behavior: 'wake', streamOptions: { requestContext: { locale: 'en-US' } }, }, });
-
add OM-managed working memory (#18654)
Adds
observationalMemory.observation.manageWorkingMemoryso the Observer can update working memory automatically instead of requiring the main agent to call the working memory tool.new Memory({ options: { workingMemory: { enabled: true }, observationalMemory: { enabled: true, observation: { manageWorkingMemory: true }, }, }, });
This option adds
WorkingMemoryExtractor, defaultsworkingMemory.agentManagedtofalse, and defaultsworkingMemory.useStateSignalstotruewhen working memory is enabled. SetworkingMemory.agentManaged: trueto keep the main agent's working memory tool and instructions enabled. -
Added file-based agents: define an agent by file convention under
src/mastra/agents/<name>/alongside agents created withnew Agent(). (#18609)A directory becomes an agent when it has a
config.tsorinstructions.md. The directory name is the agent name.instructions.mdsupplies the instructions,tools/*.tssupply tools,skills/supplies skills (acreateSkill()module, a packagedSKILL.mddirectory, or a flat<skill>.md), and amemory.tsdefault export supplies the agent'smemory(config.memorywins if both are set). Each file-based agent also gets a workspace by default (contained filesystem + shell sandbox rooted at a per-agentworkspace/dir); customize it with aworkspace.tsdefault export orconfig.workspace. Both styles register into the same Mastra instance and show up together in Studio, the server, and the bundler.Before
import { Agent } from '@mastra/core/agent'; export const weather = new Agent({ id: 'weather', name: 'weather', instructions: 'You are a weather assistant.', model: 'openai/gpt-4o', });
After (file-based, optional)
// src/mastra/agents/weather/config.ts import { agentConfig } from '@mastra/core/agent'; export default agentConfig({ model: 'openai/gpt-4o', // instructions taken from instructions.md, tools from tools/*.ts }); // src/mastra/agents/weather/memory.ts import { Memory } from '@mastra/memory'; export default new Memory(); // wired in as the agent's memory
A file-based agent can also declare subagents under
agents/<name>/subagents/<childId>/, using the same directory layout as an agent (config.ts,instructions.md,tools/,skills/,workspace.ts/workspace/). Each subagent is assembled independently and wired into the parent'sagentsmap, so the loop exposes it as a delegation tool named after the directory. A subagent'sconfig.tsmust set a non-emptydescription(build error otherwise), subagents inherit nothing from the parent, and they are one level deep (a nestedsubagents/directory is ignored with a warning). A subagent id colliding with a parent tool key or another subagent id is a build error; an id also present inconfig.agentskeeps theconfig.agentsentry with a warning.Code-registered agents win on name collisions, and a
config.tsthat exportsnew Agent()is used as-is (its siblinginstructions.md,tools/, andsubagents/are ignored with a warning), so existing projects are unaffected.The core API surface is
agentConfig()plus theassembleAgentFromFsEntry()/Mastra.__registerFsAgents()helpers that turn a discovered directory into a registered agent. Directory discovery itself is performed by the build pipeline; importing themastrainstance directly as a library does not scanagents/<name>/directories, so register those agents in code if you need them outside the build pipeline. -
support inline JSON prompt injection (#18652)
Added
structuredOutput.jsonPromptInjection: 'inline'to
append JSON schema instructions to the latest user message
instead of the system prompt. This helps keep the system
prompt stable on providers that cache prompt prefixes.await agent.generate('Summarize this text', { structuredOutput: { schema, jsonPromptInjection: 'inline', }, });
-
Added storage-backed discovery of suspended agent runs, so human-in-the-loop approval UIs can recover a pending run after a page refresh or server restart. (#17898)
agent.listSuspendedRuns()lists runs waiting on a tool-call approval or on a tool that calledsuspend(). Unlike the in-memorygetActiveThreadRunId(), it reads from storage, so it works after a restart and across multiple server instances:const { runs, total } = await agent.listSuspendedRuns({ threadId, resourceId }); if (runs[0]) { // runs[0].toolCalls -> [{ toolCallId, toolName, args, requiresApproval }] await agent.approveToolCall({ runId: runs[0].runId, toolCallId: runs[0].toolCalls[0].toolCallId }); }
Supports
threadId/resourceId/date filters and pagination, mirroringlistWorkflowRuns(). The same surface is exposed over HTTP asGET /agents/:agentId/suspended-runsand on the client SDK asagent.listSuspendedRuns(); server-enforced request-context values take precedence over client query parameters, so clients cannot list runs outside their scope.sendToolApproval()now falls back to this storage-backed discovery when no active run is found in memory for the thread, so approvals keep working after a restart. If several suspended runs match, it throws an error asking for atoolCallIdto disambiguate.Why: approval UIs previously had no public way to recover a suspended run after a refresh or restart, forcing apps to parse internal workflow snapshots.
Patch Changes
-
Update provider registry and model documentation with latest models and providers (
b9a2961) -
Fixed thread metadata being lost when a processor or working memory writes to it during an agent run. The thread is re-saved when the run finishes, and it was using a stale in-memory snapshot that overwrote any metadata written mid-run via updateThread. The agent now re-reads the latest persisted thread before that save, so mid-run metadata is preserved. Affects all storage backends (Postgres, LibSQL, and others). Fixes #16216. (#18152)
-
Fixed find_files and grep tools to always exclude .git directory contents — the .git directory is now skipped at the traversal boundary in both tools since its internals are never useful and waste tokens. Also fixed pipe-separated exclude patterns in find_files (e.g. ".git|node_modules") to work correctly, matching tree's -I flag behavior. (#18548)
-
DurableAgentnow matchesAgentbehavior in three places where the durable loop previously diverged: (#18677)isTaskCompletescorers receiverequestContextascustomContext, so the same scorer code works on both agents. Only JSON-serializable entries fromrequestContextare forwarded; non-serializable values are dropped. Do not store secrets inRequestContextif you persist durable agent snapshots.- Provider-defined tools (e.g. OpenAI
web_search) resolve and execute when invoked by the model, instead of surfacing asToolNotFoundError. - Each iteration of a multi-step durable run produces a distinct assistant
messageId, matching the non-durable loop and unblocking downstream consumers (signal drains, audit logs, replay) that key off message identity.
-
add agent reference to processor execution context (#18651)
-
Fix in-memory workflow storage
getWorkflowRunByIdreturningnullwhenworkflowNameis omitted.workflowNameis optional in the storage contract and the pg/libsql adapters match byrunIdalone when it is not provided, but the in-memory store always comparedworkflow_name === workflowName, which never matched for an undefined name. It now matches byrunId, only filters byworkflowNamewhen provided, and returns the most recent run for parity with the persistent adapters. Closes #18585. (#18586) -
Fix in-memory observability
listTracesignoring thestartExclusiveandendExclusiveflags onstartedAt/endedAtfilters. Exclusive date-range bounds now drop a trace that sits exactly on the boundary, matching the pg/libsql adapters (and the in-memory log/metric filters). Closes #18635. (#18675) -
Fix in-memory scores store
listScoresByScorerIdreturning scores in insertion order instead of newest first. The pg and libsql adapters order bycreatedAt DESC, and the siblinglistScoresBySpanalready does, so the in-memory store now sorts the same way before paginating. Closes #18618. (#18619) -
add observational memory extractors (#18653)
Introduces a public Extractor API for Observational Memory
with inline XML extraction and structured follow-up modes.
Includes built-in extractors for current task, suggested
response, and thread title. Persists extracted values into
thread OM metadata with key-level merging and carry-forward
into future observer/reflector prompts. -
Fix a polynomial ReDoS in the model gateway error matcher. The
Missing .+ environment variablepattern used to classify expected missing-auth errors could backtrack catastrophically on adversarial error messages; it now usesMissing [^ ]+ environment variable, which matches the same real messages without the ambiguous overlap. (#18680) -
Bring
InngestAgent(Inngest-backed durable agent) to parity withDurableAgentfor per-call execution options, abort handling, idle-aware resume, andgenerate(). (#18615)InngestAgent.stream()andresume()now accept the same execution-option surface asDurableAgent, includingstopWhen,activeTools,structuredOutput,versions,system,disableBackgroundTasks,tracingOptions,actor,transform,prepareStep,isTaskComplete,delegation, function-formrequireToolApproval, and the lifecycle callbacksonAbort/onIterationComplete. Closure-shaped options (prepareStep,transform, function-formisTaskComplete/requireToolApproval,stopWhencallbacks) continue to work in-process; they degrade after a worker hop the same way they do for in-memoryDurableAgent.const result = await inngestAgent.stream(messages, { runId: 'run-1', abortSignal: controller.signal, stopWhen: stepCountIs(5), onIterationComplete: ({ iteration }) => console.log('done', iteration), }); // Cancel a live run from the caller result.abort(); // Resume and drive the run to completion in a single call await inngestAgent.resume({ runId: 'run-1', resumeData, untilIdle: true }); // Durable equivalents of Agent.generate / resumeGenerate const out = await inngestAgent.generate(messages, { runId: 'run-2' }); const resumed = await inngestAgent.resumeGenerate({ runId: 'run-2', resumeData });
@mastra/corere-exportsglobalRunRegistryandrunResumeDurableStreamUntilIdlefrom@mastra/core/agent/durableso durable-agent integrations can share the same registry and idle-wrapper plumbing. -
fix: prevent partial gateway sync from corrupting provider registry (#18545)
-
Amazon Bedrock models now appear under their own
amazon-bedrock/<model>provider in the model picker instead of themastracode/amazon-bedrock/<model>namespace. Bedrock is resolved through a dedicated Amazon Bedrock gateway that authenticates with the AWS credential chain (SigV4) and surfaces models from the public models.dev catalog. Saved model selections using the previousmastracode/amazon-bedrock/...IDs are still resolved at runtime, so existing config keeps working. (#17937) -
Fixed gs:// and s3:// file/image references being downloaded and corrupted into data: URIs during durable agent execution. The durable LLM step now forwards the model's supportedUrls (matching standard execution), so URLs a provider fetches natively (e.g. Vertex gs://) pass through as references instead of failing with "Failed to download asset" or being base64-wrapped. (#18649)
-
Fixed background task execution metadata updates so they no longer rewrite the model-visible tool invocation state. (#18556)
-
Fixed 'Type instantiation is excessively deep' (TS2589) errors that occurred when defining workflows with Zod schemas. Workflow and step type inference is now significantly faster and no longer causes TypeScript to crash or report depth errors. (#18608)
-
Reduce repeated schema work during sub-agent tool conversion for more stable memory usage. (#18566)
-
Scripts using Mastra no longer hang after completing their work. The scheduler timer that polls for due schedules previously kept the Node.js event loop alive, preventing process exit even when all work was done. The timer now allows the process to exit naturally. (#18713)
-
Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. (#18655)
-
Fixed custom model gateways being overridden by default gateways. GatewayManager now deduplicates gateways by ID (first-wins) so custom gateways take precedence over defaults. Narrowed the auth-availability check to only swallow expected missing-credential errors instead of all errors, so real gateway failures surface during debugging. (#18602)
-
Fixed channel broadcasting so agent runs on a channel-backed thread post back to the channel even when they did not start from an inbound platform message. Previously only runs triggered by an incoming Slack/Discord/etc. message would render to the channel; heartbeat, Studio, and custom UI runs were silently dropped. The channels output processor now reconstructs the channel destination from the thread itself, so any run on a channel-backed thread delivers its output. (#18630)
@mastra/ai-sdk@1.6.1
Patch Changes
- Fixed
chatRouteandhandleChatStreamignoring agent instructions and tools edited through the Agent Editor. When an editor is configured, the chat endpoint now applies the agent's stored overrides just like Studio does, instead of running the bare code-defined agent. Previously, instructions edited in the editor were silently dropped and the agent answered as if it had none. (#18592)
@mastra/client-js@1.29.0
Minor Changes
-
Added heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. (#18184)
A heartbeat fires a prompt to an agent on a schedule. When it has a thread, the run is delivered into that thread as a normal agent signal, so anything watching the thread sees it like any other message; without a thread, the agent just runs in isolation. Each heartbeat has its own id and an optional
name, so one agent or thread can have several heartbeats with different schedules and prompts. The id is generated for you, or you can pass your ownidtocreatefor a stable handle (it's normalized tohb_<slug>). Heartbeats are persisted, so they keep firing across process restarts with no extra setup.const hb = await mastra.heartbeats.create({ agentId: 'chef', name: 'morning-checkin', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', ifActive: { behavior: 'discard' }, // skip if the user is mid-conversation ifIdle: { behavior: 'wake' }, // wake the agent if the thread is idle }); // Threadless: run the agent on a cron with no conversation. await mastra.heartbeats.create({ agentId: 'chef', cron: '0 * * * *', prompt: 'Run the hourly summary', }); await mastra.heartbeats.list({ agentId: 'chef' }); await mastra.heartbeats.get(hb.id); await mastra.heartbeats.update(hb.id, { prompt: 'check in gently' }); await mastra.heartbeats.pause(hb.id); await mastra.heartbeats.resume(hb.id); await mastra.heartbeats.run(hb.id); // fire once now await mastra.heartbeats.delete(hb.id);
The same CRUD is available over HTTP through
@mastra/server(under/api/heartbeats) and as top-level methods on the@mastra/client-jsclient (client.createHeartbeat,client.getHeartbeat,client.listHeartbeats, etc.).Lifecycle hooks
React to heartbeat runs via
heartbeaton theMastraconstructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firingagentIdso you can branch on it.prepareresolves fire-time parameters (for example, creating a fresh thread per fire), andonFinish/onError/onAbortmirroragent.stream.new Mastra({ // ... heartbeat: { // Return overrides, `null` to skip this fire, or `undefined` to use defaults. prepare: async ({ agentId, heartbeat }) => { if (agentId === 'chef' && heartbeat.name === 'daily-digest') { return { threadId: await createDailyThread(), resourceId: 'slack:U095PUH0FKL' }; } }, onFinish: ({ agentId, outcome, result, heartbeat }) => { metrics.record({ agentId, heartbeat: heartbeat.name, outcome }); }, onError: ({ agentId, error, phase, heartbeat }) => { alerts.send(`heartbeat ${agentId}/${heartbeat.name} failed in ${phase}: ${error.message}`); }, }, });
Signal shaping
A heartbeat fire surfaces to the agent as a signal. By default it uses the
notificationtype and renders as<heartbeat>…</heartbeat>; overridesignalTypeandtagNameto change either.ifActiveandifIdlemirror theagent.sendSignaloptions shape ({ behavior, attributes }, plusstreamOptionsonifIdle) and stay JSON-serializable so they persist with the schedule.ifIdle.streamOptionscurrently acceptsrequestContext, which is rehydrated onto the woken run. Top-levelattributesare rendered on the signal tag, and top-levelproviderOptionsare merged into the signal payload on every fire.await mastra.heartbeats.create({ agentId: 'chef', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', tagName: 'check-in', // renders as <check-in>…</check-in> attributes: { source: 'cron' }, providerOptions: { openai: { store: false } }, ifIdle: { behavior: 'wake', streamOptions: { requestContext: { locale: 'en-US' } }, }, });
-
Added storage-backed discovery of suspended agent runs, so human-in-the-loop approval UIs can recover a pending run after a page refresh or server restart. (#17898)
agent.listSuspendedRuns()lists runs waiting on a tool-call approval or on a tool that calledsuspend(). Unlike the in-memorygetActiveThreadRunId(), it reads from storage, so it works after a restart and across multiple server instances:const { runs, total } = await agent.listSuspendedRuns({ threadId, resourceId }); if (runs[0]) { // runs[0].toolCalls -> [{ toolCallId, toolName, args, requiresApproval }] await agent.approveToolCall({ runId: runs[0].runId, toolCallId: runs[0].toolCalls[0].toolCallId }); }
Supports
threadId/resourceId/date filters and pagination, mirroringlistWorkflowRuns(). The same surface is exposed over HTTP asGET /agents/:agentId/suspended-runsand on the client SDK asagent.listSuspendedRuns(); server-enforced request-context values take precedence over client query parameters, so clients cannot list runs outside their scope.sendToolApproval()now falls back to this storage-backed discovery when no active run is found in memory for the thread, so approvals keep working after a restart. If several suspended runs match, it throws an error asking for atoolCallIdto disambiguate.Why: approval UIs previously had no public way to recover a suspended run after a refresh or restart, forcing apps to parse internal workflow snapshots.
Patch Changes
-
Fix
listLogsandgetLogForRundropping thepageandperPagequery parameters when they are0. Requesting the first page withpage: 0(orperPage: 0) now sends those values instead of falling back to the server defaults. Closes #18631. (#18632) -
add Studio support for observational memory extractors (#18655)
Adds
bufferedObservationChunksand extraction metadata to the buffer-status API and client types so extracted values flow through during live streaming. Renders observational memory indicators from a normalized cycle model that preserves extraction data across streaming, refetch, reload, activation, and failure transitions.
@mastra/deployer@1.48.0
Minor Changes
-
You can now define agents by file convention instead of registering each one in code: drop a directory under
src/mastra/agents/<name>/, run a Mastra build/dev, and the agent is bundled and registered onto your Mastra instance automatically. A directory becomes an agent when it has aconfig.tsorinstructions.md;tools/*.tsadd tools,skills/add skills (acreateSkill()module, a packagedSKILL.mdwith itsreferences/, or a flat<skill>.md), amemory.tsdefault export supplies the agent'smemory, andsubagents/<childId>/(one level deep) add delegatable subagents. Each agent gets a default workspace unlessworkspace.ts/config.workspaceoverrides it, and files committed underagents/<name>/workspace/are mirrored into the bundle to seed that workspace at runtime. Projects with no file-based agents are unaffected — the original entry is used unchanged. (#18609)src/mastra/agents/weather/ config.ts # export default agentConfig({ model: 'openai/gpt-4o' }) instructions.md memory.ts # export default new Memory() tools/get_weather.ts workspace/cities.json # mirrored into the agent's workspace
Patch Changes
-
Fix
FileEnvService.setEnvValuecorrupting env values that contain$when updating an existing key. Values such as database URLs and passwords that include$&,$$, or$1are now written exactly as provided instead of being mangled byString.prototype.replacespecial patterns. Closes #18633. (#18672) -
Fix
ENOENT: .mastra-fs-agents-entry.mjswhen runningmastra dev/mastra buildin a project that uses file-based agents. The generated fs-agents wrapper entry was written beforebundler.prepare()emptied the output directory, so it was wiped before the bundler could read it. Wrapper generation is now split:prepareFsAgentsEntryreturns the generated source without writing, and the newwriteFsAgentsEntrywrites it afterprepare()runs. (#18694)const fsAgents = await prepareFsAgentsEntry({ entryFile, mastraDir, outputDirectory }); await bundler.prepare(outputDirectory); // empties output dir await writeFsAgentsEntry(fsAgents); // wrapper now survives for the bundler
@mastra/editor@0.13.3
Patch Changes
-
Agent Builder agents now default observational memory to
__GATEWAY_OPENAI_MODEL_MINI__instead of__GATEWAY_GOOGLE_MODEL__. SetOPENAI_API_KEYin any environment where Builder agents run. Core (non-builder) agents are unaffected and keep the framework default. Admins can still override the model: (#18650)new MastraEditor({ builder: { enabled: true, configuration: { agent: { memory: { observationalMemory: { model: '__GATEWAY_OPENAI_MODEL_MINI__' } }, }, }, }, });
@mastra/evals@1.5.1
Patch Changes
- Eval scorers now receive the original user message for runs started through the agent subscription /
sendMessageAPI. PreviouslygetUserMessageFromRunInputreturned an empty value for these runs, so scorers could not see what the user said (onlyagent.streamandagent.generateworked). (#18546)
@mastra/inngest@1.8.0
Minor Changes
-
Added support for the fine-grained authorization (FGA)
actorsignal on the Inngest execution engine. (#18674)Workflows running on the Inngest engine can now pass a trusted
actorthroughrun.start(),startAsync(),resume(),stream(), andtimeTravel(). The signal is re-threaded across durable step and nested-workflow boundaries, so every nested agent, tool, and memory FGA check sees the same actor. Previouslyactorwas only threaded through the default engine, so trusted background workflows on Inngest lost the membership bypass at each step re-entry.Usage
const run = await workflow.createRun(); await run.start({ inputData, requestContext, // includes organizationId / tenant scope actor: { actorKind: 'system', sourceWorkflow: 'nightly-sync' }, });
-
Bring
InngestAgent(Inngest-backed durable agent) to parity withDurableAgentfor per-call execution options, abort handling, idle-aware resume, andgenerate(). (#18615)InngestAgent.stream()andresume()now accept the same execution-option surface asDurableAgent, includingstopWhen,activeTools,structuredOutput,versions,system,disableBackgroundTasks,tracingOptions,actor,transform,prepareStep,isTaskComplete,delegation, function-formrequireToolApproval, and the lifecycle callbacksonAbort/onIterationComplete. Closure-shaped options (prepareStep,transform, function-formisTaskComplete/requireToolApproval,stopWhencallbacks) continue to work in-process; they degrade after a worker hop the same way they do for in-memoryDurableAgent.const result = await inngestAgent.stream(messages, { runId: 'run-1', abortSignal: controller.signal, stopWhen: stepCountIs(5), onIterationComplete: ({ iteration }) => console.log('done', iteration), }); // Cancel a live run from the caller result.abort(); // Resume and drive the run to completion in a single call await inngestAgent.resume({ runId: 'run-1', resumeData, untilIdle: true }); // Durable equivalents of Agent.generate / resumeGenerate const out = await inngestAgent.generate(messages, { runId: 'run-2' }); const resumed = await inngestAgent.resumeGenerate({ runId: 'run-2', resumeData });
@mastra/corere-exportsglobalRunRegistryandrunResumeDurableStreamUntilIdlefrom@mastra/core/agent/durableso durable-agent integrations can share the same registry and idle-wrapper plumbing.
Patch Changes
-
Fix
observe()on Inngest durable agents so late subscribers replay buffered events before the live stream. (#18535)Previously, calling
observe()after a run had started — or reconnecting after a disconnect — only delivered chunks emitted from that moment on. Anything published earlier in the run was lost, including events from nested durable steps.observe()now replays the full history ofchunk,finish, andsuspendedevents for the requestedrunId, then continues on the live stream, matching the in-memoryDurableAgentbehavior.// First connection kicks off the run await inngestAgent.stream(messages, { runId: 'run-1' }); // Second connection replays earlier events, then continues live const { fullStream } = await inngestAgent.observe('run-1');
When no cache is configured, an in-process cache is used as a fallback so single-process replay works out of the box. Cross-process
observe()still requires a shared cache backend (e.g. Redis) passed viacacheormastra.serverCache.
@mastra/libsql@1.14.3
Patch Changes
- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. (#18655)
@mastra/mcp@1.12.1
Patch Changes
- Fixed @mastra/mcp crashing Cloudflare Workers at module initialization. MCPClient can now be safely imported on workerd without the Worker failing to start. (#18664)
@mastra/memory@1.22.0
Minor Changes
-
add observational memory extractors (#18653)
Introduces a public Extractor API for Observational Memory
with inline XML extraction and structured follow-up modes.
Includes built-in extractors for current task, suggested
response, and thread title. Persists extracted values into
thread OM metadata with key-level merging and carry-forward
into future observer/reflector prompts. -
add OM-managed working memory (#18654)
Adds
observationalMemory.observation.manageWorkingMemoryso the Observer can update working memory automatically instead of requiring the main agent to call the working memory tool.new Memory({ options: { workingMemory: { enabled: true }, observationalMemory: { enabled: true, observation: { manageWorkingMemory: true }, }, }, });
This option adds
WorkingMemoryExtractor, defaultsworkingMemory.agentManagedtofalse, and defaultsworkingMemory.useStateSignalstotruewhen working memory is enabled. SetworkingMemory.agentManaged: trueto keep the main agent's working memory tool and instructions enabled.
Patch Changes
-
Expose
providerMetadataon Observational MemoryObserveHooksresults (#18563)onObservationEndandonReflectionEndnow receive the OM model call'sproviderMetadataalongsideusage, so you can read per-call provider details — for example the AI Gateway's cost and generation id — straight from the hook instead of wrapping the observer/reflector models in a model-stream middleware:const hooks: ObserveHooks = { onObservationEnd: ({ usage, providerMetadata }) => { const gateway = providerMetadata?.gateway; recordCost({ tokens: usage?.totalTokens, cost: gateway?.cost, generationId: gateway?.generationId }); }, onReflectionEnd: ({ usage, providerMetadata }) => { recordCost({ tokens: usage?.totalTokens, cost: providerMetadata?.gateway?.cost }); }, };
The field is additive and optional, and is omitted entirely when the provider emits no metadata, so existing hook consumers are unaffected. For batched observations and multi-attempt reflections it reflects the last batch/attempt that emitted provider metadata.
-
add Studio support for observational memory extractors (#18655)
Adds
bufferedObservationChunksand extraction metadata to the buffer-status API and client types so extracted values flow through during live streaming. Renders observational memory indicators from a normalized cycle model that preserves extraction data across streaming, refetch, reload, activation, and failure transitions.
@mastra/mongodb@1.11.1
Patch Changes
-
Made MongoDB store writes safe against partial failures, preventing orphaned records when an operation fails partway through. (#18393)
Atomic multi-collection writes. Creates, deletes, and updates across the agents, mcp-clients, mcp-servers, prompt-blocks, scorer-definitions, skills, workspaces, schedules, and datasets domains now run in a transaction on replica sets, so a failed write leaves no half-written state. On standalone servers (which can't run transactions) these degrade to sequential best-effort, matching the previous behavior.
Scalable cascade deletes. Deleting a thread (with its messages) or a dataset (with its items) is deliberately not wrapped in a transaction, because those children are unbounded and a transactional delete is capped by MongoDB's 60-second transaction limit — a large thread or dataset would abort and become permanently undeletable. Instead the children are removed first and the parent record last, so a failure mid-delete leaves the parent in place and re-running the delete safely finishes the job.
-
Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. (#18655)
@mastra/mysql@0.3.2
Patch Changes
- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. (#18655)
@mastra/pg@1.14.3
Patch Changes
- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. (#18655)
@mastra/playground-ui@38.0.0
Minor Changes
-
Added public subpath entrypoints for shared Playground UI domain components, hooks, resize helpers, primitives, and the playground store. Applications can now import focused APIs such as
TracesLayoutandusePlaygroundStoredirectly from those subpaths. (#18511)import { TracesLayout } from '@mastra/playground-ui/domains/traces/components/traces-layout'; import { usePlaygroundStore } from '@mastra/playground-ui/store/playground-store';
Patch Changes
@mastra/qdrant@1.1.1
Patch Changes
- Improve type safety for
describeIndexmetric field — removes compiler suppression so TypeScript can fully validate the returnedIndexStatsshape (#18573)
@mastra/rag@2.4.0
Minor Changes
- Added MongoDBConfig to DatabaseConfig, exposing numCandidates for MongoDB Atlas Vector Search queries via the RAG tool layer. (#18393)
Patch Changes
@mastra/railway@0.2.1
Patch Changes
- Fixed Railway sandbox templates so they are built once during sandbox creation. (#18605)
@mastra/schema-compat@1.3.2
Patch Changes
-
Fix the Zod v4 string handler silently dropping unrecognized
string_formatchecks. Formats without a textual description (such asipv4,ipv6,datetime,date,time,base64,cuid2,ulid,nanoid,jwt) are now preserved as validation instead of being removed, so schemas using them keep rejecting invalid input. Closes #18634. (#18673) -
Fix inverted date constraint descriptions in the Zod v4 schema handler.
z.date().min()andz.date().max()were described with their bounds swapped (a lower bound was labelled "older than" and an upper bound "newer than"), so the schema sent to the model stated the opposite and impossible constraint. The handler now matches Zod semantics and the existing v3 handler. Closes #18581. (#18582) -
Fixed 'Type instantiation is excessively deep' (TS2589) errors that occurred when defining workflows with Zod schemas. Workflow and step type inference is now significantly faster and no longer causes TypeScript to crash or report depth errors. (#18608)
@mastra/server@1.48.0
Minor Changes
-
Added heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. (#18184)
A heartbeat fires a prompt to an agent on a schedule. When it has a thread, the run is delivered into that thread as a normal agent signal, so anything watching the thread sees it like any other message; without a thread, the agent just runs in isolation. Each heartbeat has its own id and an optional
name, so one agent or thread can have several heartbeats with different schedules and prompts. The id is generated for you, or you can pass your ownidtocreatefor a stable handle (it's normalized tohb_<slug>). Heartbeats are persisted, so they keep firing across process restarts with no extra setup.const hb = await mastra.heartbeats.create({ agentId: 'chef', name: 'morning-checkin', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', ifActive: { behavior: 'discard' }, // skip if the user is mid-conversation ifIdle: { behavior: 'wake' }, // wake the agent if the thread is idle }); // Threadless: run the agent on a cron with no conversation. await mastra.heartbeats.create({ agentId: 'chef', cron: '0 * * * *', prompt: 'Run the hourly summary', }); await mastra.heartbeats.list({ agentId: 'chef' }); await mastra.heartbeats.get(hb.id); await mastra.heartbeats.update(hb.id, { prompt: 'check in gently' }); await mastra.heartbeats.pause(hb.id); await mastra.heartbeats.resume(hb.id); await mastra.heartbeats.run(hb.id); // fire once now await mastra.heartbeats.delete(hb.id);
The same CRUD is available over HTTP through
@mastra/server(under/api/heartbeats) and as top-level methods on the@mastra/client-jsclient (client.createHeartbeat,client.getHeartbeat,client.listHeartbeats, etc.).Lifecycle hooks
React to heartbeat runs via
heartbeaton theMastraconstructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firingagentIdso you can branch on it.prepareresolves fire-time parameters (for example, creating a fresh thread per fire), andonFinish/onError/onAbortmirroragent.stream.new Mastra({ // ... heartbeat: { // Return overrides, `null` to skip this fire, or `undefined` to use defaults. prepare: async ({ agentId, heartbeat }) => { if (agentId === 'chef' && heartbeat.name === 'daily-digest') { return { threadId: await createDailyThread(), resourceId: 'slack:U095PUH0FKL' }; } }, onFinish: ({ agentId, outcome, result, heartbeat }) => { metrics.record({ agentId, heartbeat: heartbeat.name, outcome }); }, onError: ({ agentId, error, phase, heartbeat }) => { alerts.send(`heartbeat ${agentId}/${heartbeat.name} failed in ${phase}: ${error.message}`); }, }, });
Signal shaping
A heartbeat fire surfaces to the agent as a signal. By default it uses the
notificationtype and renders as<heartbeat>…</heartbeat>; overridesignalTypeandtagNameto change either.ifActiveandifIdlemirror theagent.sendSignaloptions shape ({ behavior, attributes }, plusstreamOptionsonifIdle) and stay JSON-serializable so they persist with the schedule.ifIdle.streamOptionscurrently acceptsrequestContext, which is rehydrated onto the woken run. Top-levelattributesare rendered on the signal tag, and top-levelproviderOptionsare merged into the signal payload on every fire.await mastra.heartbeats.create({ agentId: 'chef', threadId, resourceId, cron: '*/5 * * * *', prompt: 'Check in on the user', tagName: 'check-in', // renders as <check-in>…</check-in> attributes: { source: 'cron' }, providerOptions: { openai: { store: false } }, ifIdle: { behavior: 'wake', streamOptions: { requestContext: { locale: 'en-US' } }, }, });
-
Added storage-backed discovery of suspended agent runs, so human-in-the-loop approval UIs can recover a pending run after a page refresh or server restart. (#17898)
agent.listSuspendedRuns()lists runs waiting on a tool-call approval or on a tool that calledsuspend(). Unlike the in-memorygetActiveThreadRunId(), it reads from storage, so it works after a restart and across multiple server instances:const { runs, total } = await agent.listSuspendedRuns({ threadId, resourceId }); if (runs[0]) { // runs[0].toolCalls -> [{ toolCallId, toolName, args, requiresApproval }] await agent.approveToolCall({ runId: runs[0].runId, toolCallId: runs[0].toolCalls[0].toolCallId }); }
Supports
threadId/resourceId/date filters and pagination, mirroringlistWorkflowRuns(). The same surface is exposed over HTTP asGET /agents/:agentId/suspended-runsand on the client SDK asagent.listSuspendedRuns(); server-enforced request-context values take precedence over client query parameters, so clients cannot list runs outside their scope.sendToolApproval()now falls back to this storage-backed discovery when no active run is found in memory for the thread, so approvals keep working after a restart. If several suspended runs match, it throws an error asking for atoolCallIdto disambiguate.Why: approval UIs previously had no public way to recover a suspended run after a refresh or restart, forcing apps to parse internal workflow snapshots.
Patch Changes
-
Allow
'fs'as an agent/scorer definition source in the server handlers and response schemas. File-based agents are registered withsource: 'fs', and the scorer/agent list endpoints now surface and validate that value instead of failing schema validation. (#18609)// GET /api/agents now returns file-based agents alongside code/stored ones: { "weather": { "name": "weather", "source": "fs" /* was rejected before */ } }
-
Fixed inline skills (created via createSkill()) not appearing in the Dev Portal. The server now uses agent.listSkills() and agent.getSkill() which return both inline and workspace skills, instead of only querying workspace skills. (#18569)
-
add Studio support for observational memory extractors (#18655)
Adds
bufferedObservationChunksand extraction metadata to the buffer-status API and client types so extracted values flow through during live streaming. Renders observational memory indicators from a normalized cycle model that preserves extraction data across streaming, refetch, reload, activation, and failure transitions.
@mastra/vercel@1.2.0
Minor Changes
- Breaking change: Renamed the Vercel sandbox exports to make the MicroVM and serverless implementations explicit.
VercelSandboxnow refers to the MicroVM-backed Vercel Sandbox product. The serverless implementation is now exported asVercelServerlessSandbox. (#18667)-
If you have been using
VercelSandboxin your code, you should update your imports to useVercelServerlessSandboxinstead.-import { VercelSandbox } from '@mastra/vercel'; -import type { VercelSandboxOptions } from '@mastra/vercel'; +import { VercelServerlessSandbox } from '@mastra/vercel'; +import type { VercelServerlessSandboxOptions } from '@mastra/vercel'; -const sandbox = new VercelSandbox({ +const sandbox = new VercelServerlessSandbox({ token: process.env.VERCEL_TOKEN, }); -const options: VercelSandboxOptions = { +const options: VercelServerlessSandboxOptions = { token: process.env.VERCEL_TOKEN, };
-
If you have been using
VercelMicroVMSandboxin your code, you should update your imports to useVercelSandboxinstead.-import { VercelMicroVMSandbox } from '@mastra/vercel'; +import { VercelSandbox } from '@mastra/vercel'; -import type { VercelMicroVMSandboxOptions } from '@mastra/vercel'; +import type { VercelSandboxOptions } from '@mastra/vercel'; -const sandbox = new VercelMicroVMSandbox(); +const sandbox = new VercelSandbox(); -const options: VercelMicroVMSandboxOptions = { +const options: VercelSandboxOptions = { runtime: 'node24', };
-
Provider descriptors are also split by runtime:
import { vercelSandboxProvider, vercelServerlessSandboxProvider } from '@mastra/vercel';
Use
vercelSandboxProviderfor MicroVM-backed Vercel Sandbox instances andvercelServerlessSandboxProviderfor Vercel Functions-backed serverless instances.
-
Patch Changes
Other updated packages
The following packages were updated with dependency changes only:
- @mastra/agent-builder@1.1.3
- @mastra/deployer-cloud@1.48.0
- @mastra/deployer-cloudflare@1.2.3
- @mastra/deployer-netlify@1.2.3
- @mastra/deployer-vercel@1.2.3
- @mastra/express@1.4.3
- @mastra/fastify@1.4.3
- @mastra/hono@1.5.3
- @mastra/koa@1.6.3
- @mastra/longmemeval@1.1.3
- @mastra/mcp-docs-server@1.2.3
- @mastra/nestjs@0.2.3
- @mastra/next@0.2.2
- @mastra/opencode@0.1.3
- @mastra/react@1.2.1
- @mastra/tanstack-start@0.2.2
- @mastra/temporal@0.2.3
- @mastra/voice-google-gemini-live@0.14.2
- @mastra/voice-openai-realtime@0.13.2
- @mastra/voice-xai-realtime@0.2.2