github mastra-ai/mastra @mastra/core@1.48.0
July 1, 2026

7 hours ago

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—VercelSandbox now means MicroVM-backed Vercel Sandbox; the serverless implementation is now VercelServerlessSandbox (and VercelMicroVMSandbox is replaced by VercelSandbox).

Changelog

@mastra/core@1.48.0

Minor Changes

  • Renamed the AgentController interval API. heartbeatHandlers is now intervalHandlers, the HeartbeatHandler type is now IntervalHandler, and the removeHeartbeat()/stopHeartbeats() methods are now removeInterval()/stopIntervals(). This better reflects that these are fixed-interval background tasks, not liveness pings, and is distinct from the unrelated mastra.heartbeats scheduled-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 maxSteps to ScorerJudgeConfig and GoalConfig, 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 maxSteps to ScorerJudgeConfig, letting callers control the internal judge agent's agentic-loop iteration limit instead of relying on the implicit default of 5. (#18544)

  • Added createCodingAgent factory and a reusable buildBasePrompt so 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() (set basePath, pass your own workspace, or pass workspace: undefined to opt out entirely).
    • Task signalsTaskSignalProvider so a task list persists across turns.
    • Error handling — retries on ECONNRESET and bad-request errors, plus prefill and provider-history compatibility processors.
    • Goal judging — the default goal judge prompt.

    buildBasePrompt is parameterized with productName, coAuthorName (both default to "Mastra Code"), and coAuthorEmail (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',
    });
  • 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 own id to create for a stable handle (it's normalized to hb_<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-js client (client.createHeartbeat, client.getHeartbeat, client.listHeartbeats, etc.).

    Lifecycle hooks

    React to heartbeat runs via heartbeat on the Mastra constructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firing agentId so you can branch on it. prepare resolves fire-time parameters (for example, creating a fresh thread per fire), and onFinish / onError / onAbort mirror agent.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 notification type and renders as <heartbeat>…</heartbeat>; override signalType and tagName to change either. ifActive and ifIdle mirror the agent.sendSignal options shape ({ behavior, attributes }, plus streamOptions on ifIdle) and stay JSON-serializable so they persist with the schedule. ifIdle.streamOptions currently accepts requestContext, which is rehydrated onto the woken run. Top-level attributes are rendered on the signal tag, and top-level providerOptions are 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.manageWorkingMemory so 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, defaults workingMemory.agentManaged to false, and defaults workingMemory.useStateSignals to true when working memory is enabled. Set workingMemory.agentManaged: true to 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 with new Agent(). (#18609)

    A directory becomes an agent when it has a config.ts or instructions.md. The directory name is the agent name. instructions.md supplies the instructions, tools/*.ts supply tools, skills/ supplies skills (a createSkill() module, a packaged SKILL.md directory, or a flat <skill>.md), and a memory.ts default export supplies the agent's memory (config.memory wins if both are set). Each file-based agent also gets a workspace by default (contained filesystem + shell sandbox rooted at a per-agent workspace/ dir); customize it with a workspace.ts default export or config.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's agents map, so the loop exposes it as a delegation tool named after the directory. A subagent's config.ts must set a non-empty description (build error otherwise), subagents inherit nothing from the parent, and they are one level deep (a nested subagents/ 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 in config.agents keeps the config.agents entry with a warning.

    Code-registered agents win on name collisions, and a config.ts that exports new Agent() is used as-is (its sibling instructions.md, tools/, and subagents/ are ignored with a warning), so existing projects are unaffected.

    The core API surface is agentConfig() plus the assembleAgentFromFsEntry() / Mastra.__registerFsAgents() helpers that turn a discovered directory into a registered agent. Directory discovery itself is performed by the build pipeline; importing the mastra instance directly as a library does not scan agents/<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 called suspend(). Unlike the in-memory getActiveThreadRunId(), 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, mirroring listWorkflowRuns(). The same surface is exposed over HTTP as GET /agents/:agentId/suspended-runs and on the client SDK as agent.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 a toolCallId to 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)

  • DurableAgent now matches Agent behavior in three places where the durable loop previously diverged: (#18677)

    • isTaskComplete scorers receive requestContext as customContext, so the same scorer code works on both agents. Only JSON-serializable entries from requestContext are forwarded; non-serializable values are dropped. Do not store secrets in RequestContext if you persist durable agent snapshots.
    • Provider-defined tools (e.g. OpenAI web_search) resolve and execute when invoked by the model, instead of surfacing as ToolNotFoundError.
    • 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 getWorkflowRunById returning null when workflowName is omitted. workflowName is optional in the storage contract and the pg/libsql adapters match by runId alone when it is not provided, but the in-memory store always compared workflow_name === workflowName, which never matched for an undefined name. It now matches by runId, only filters by workflowName when provided, and returns the most recent run for parity with the persistent adapters. Closes #18585. (#18586)

  • Fix in-memory observability listTraces ignoring the startExclusive and endExclusive flags on startedAt/endedAt filters. 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 listScoresByScorerId returning scores in insertion order instead of newest first. The pg and libsql adapters order by createdAt DESC, and the sibling listScoresBySpan already 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 variable pattern used to classify expected missing-auth errors could backtrack catastrophically on adversarial error messages; it now uses Missing [^ ]+ environment variable, which matches the same real messages without the ambiguous overlap. (#18680)

  • Bring InngestAgent (Inngest-backed durable agent) to parity with DurableAgent for per-call execution options, abort handling, idle-aware resume, and generate(). (#18615)

    InngestAgent.stream() and resume() now accept the same execution-option surface as DurableAgent, including stopWhen, activeTools, structuredOutput, versions, system, disableBackgroundTasks, tracingOptions, actor, transform, prepareStep, isTaskComplete, delegation, function-form requireToolApproval, and the lifecycle callbacks onAbort / onIterationComplete. Closure-shaped options (prepareStep, transform, function-form isTaskComplete / requireToolApproval, stopWhen callbacks) continue to work in-process; they degrade after a worker hop the same way they do for in-memory DurableAgent.

    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/core re-exports globalRunRegistry and runResumeDurableStreamUntilIdle from @mastra/core/agent/durable so 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 the mastracode/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 previous mastracode/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 chatRoute and handleChatStream ignoring 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 own id to create for a stable handle (it's normalized to hb_<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-js client (client.createHeartbeat, client.getHeartbeat, client.listHeartbeats, etc.).

    Lifecycle hooks

    React to heartbeat runs via heartbeat on the Mastra constructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firing agentId so you can branch on it. prepare resolves fire-time parameters (for example, creating a fresh thread per fire), and onFinish / onError / onAbort mirror agent.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 notification type and renders as <heartbeat>…</heartbeat>; override signalType and tagName to change either. ifActive and ifIdle mirror the agent.sendSignal options shape ({ behavior, attributes }, plus streamOptions on ifIdle) and stay JSON-serializable so they persist with the schedule. ifIdle.streamOptions currently accepts requestContext, which is rehydrated onto the woken run. Top-level attributes are rendered on the signal tag, and top-level providerOptions are 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 called suspend(). Unlike the in-memory getActiveThreadRunId(), 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, mirroring listWorkflowRuns(). The same surface is exposed over HTTP as GET /agents/:agentId/suspended-runs and on the client SDK as agent.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 a toolCallId to 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 listLogs and getLogForRun dropping the page and perPage query parameters when they are 0. Requesting the first page with page: 0 (or perPage: 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 bufferedObservationChunks and 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 a config.ts or instructions.md; tools/*.ts add tools, skills/ add skills (a createSkill() module, a packaged SKILL.md with its references/, or a flat <skill>.md), a memory.ts default export supplies the agent's memory, and subagents/<childId>/ (one level deep) add delegatable subagents. Each agent gets a default workspace unless workspace.ts / config.workspace overrides it, and files committed under agents/<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.setEnvValue corrupting env values that contain $ when updating an existing key. Values such as database URLs and passwords that include $&, $$, or $1 are now written exactly as provided instead of being mangled by String.prototype.replace special patterns. Closes #18633. (#18672)

  • Fix ENOENT: .mastra-fs-agents-entry.mjs when running mastra dev/mastra build in a project that uses file-based agents. The generated fs-agents wrapper entry was written before bundler.prepare() emptied the output directory, so it was wiped before the bundler could read it. Wrapper generation is now split: prepareFsAgentsEntry returns the generated source without writing, and the new writeFsAgentsEntry writes it after prepare() 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__. Set OPENAI_API_KEY in 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 / sendMessage API. Previously getUserMessageFromRunInput returned an empty value for these runs, so scorers could not see what the user said (only agent.stream and agent.generate worked). (#18546)

@mastra/inngest@1.8.0

Minor Changes

  • Added support for the fine-grained authorization (FGA) actor signal on the Inngest execution engine. (#18674)

    Workflows running on the Inngest engine can now pass a trusted actor through run.start(), startAsync(), resume(), stream(), and timeTravel(). 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. Previously actor was 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 with DurableAgent for per-call execution options, abort handling, idle-aware resume, and generate(). (#18615)

    InngestAgent.stream() and resume() now accept the same execution-option surface as DurableAgent, including stopWhen, activeTools, structuredOutput, versions, system, disableBackgroundTasks, tracingOptions, actor, transform, prepareStep, isTaskComplete, delegation, function-form requireToolApproval, and the lifecycle callbacks onAbort / onIterationComplete. Closure-shaped options (prepareStep, transform, function-form isTaskComplete / requireToolApproval, stopWhen callbacks) continue to work in-process; they degrade after a worker hop the same way they do for in-memory DurableAgent.

    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/core re-exports globalRunRegistry and runResumeDurableStreamUntilIdle from @mastra/core/agent/durable so 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 of chunk, finish, and suspended events for the requested runId, then continues on the live stream, matching the in-memory DurableAgent behavior.

    // 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 via cache or mastra.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.manageWorkingMemory so 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, defaults workingMemory.agentManaged to false, and defaults workingMemory.useStateSignals to true when working memory is enabled. Set workingMemory.agentManaged: true to keep the main agent's working memory tool and instructions enabled.

Patch Changes

  • Expose providerMetadata on Observational Memory ObserveHooks results (#18563)

    onObservationEnd and onReflectionEnd now receive the OM model call's providerMetadata alongside usage, 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 bufferedObservationChunks and 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 TracesLayout and usePlaygroundStore directly 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 describeIndex metric field — removes compiler suppression so TypeScript can fully validate the returned IndexStats shape (#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_format checks. Formats without a textual description (such as ipv4, 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() and z.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 own id to create for a stable handle (it's normalized to hb_<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-js client (client.createHeartbeat, client.getHeartbeat, client.listHeartbeats, etc.).

    Lifecycle hooks

    React to heartbeat runs via heartbeat on the Mastra constructor. It's a single hook bundle that runs for every agent's heartbeats; each hook receives the firing agentId so you can branch on it. prepare resolves fire-time parameters (for example, creating a fresh thread per fire), and onFinish / onError / onAbort mirror agent.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 notification type and renders as <heartbeat>…</heartbeat>; override signalType and tagName to change either. ifActive and ifIdle mirror the agent.sendSignal options shape ({ behavior, attributes }, plus streamOptions on ifIdle) and stay JSON-serializable so they persist with the schedule. ifIdle.streamOptions currently accepts requestContext, which is rehydrated onto the woken run. Top-level attributes are rendered on the signal tag, and top-level providerOptions are 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 called suspend(). Unlike the in-memory getActiveThreadRunId(), 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, mirroring listWorkflowRuns(). The same surface is exposed over HTTP as GET /agents/:agentId/suspended-runs and on the client SDK as agent.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 a toolCallId to 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 with source: '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 bufferedObservationChunks and 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. VercelSandbox now refers to the MicroVM-backed Vercel Sandbox product. The serverless implementation is now exported as VercelServerlessSandbox. (#18667)
    • If you have been using VercelSandbox in your code, you should update your imports to use VercelServerlessSandbox instead.

      -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 VercelMicroVMSandbox in your code, you should update your imports to use VercelSandbox instead.

      -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 vercelSandboxProvider for MicroVM-backed Vercel Sandbox instances and vercelServerlessSandboxProvider for Vercel Functions-backed serverless instances.

Patch Changes

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.