@mastra/core@1.48.0

longbridge/gpui-kit@mastra/core@1.48.0Jul 1, 2026by PaulieScanlon

AI Summary

Introduces new heartbeats APIs for scheduling agents on cron, file-based agent definitions, and significant upgrades to Observational Memory capabilities.

Key Highlights

  • New `mastra.heartbeats` APIs for scheduling agents on recurring cron schedules
  • File-based agents & subagents with automatic bundler registration
  • Observational Memory Extractor API and OM-managed working memory
  • Storage-backed suspended run discovery for Human-in-the-loop reliability
  • Durable/Inngest execution parity

Breaking Changes

  • Sandbox exports renamed: `VercelSandbox` now means MicroVM-backed, `VercelServerlessSandbox` is the serverless implementation
  • AgentController interval API renamed: `heartbeatHandlers` -> `intervalHandlers`, `HeartbeatHandler` -> `IntervalHandler`

New Features

  • Heartbeat CRUD APIs (create, list, get, update, pause, resume, run, delete)
  • Subagent support with delegatable tools
  • Inline XML extraction for Observational Memory

Full Release Notes

## 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](https://github.com/mastra-ai/mastra/blob/@mastra/core@1.48.0/packages/core/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18665))

  **Before**

  ```ts
  const { controller } = await createMastraCode({
    heartbeatHandlers: [{ id: 'sync', intervalMs: 60_000, handler: async () => {} }],
  });
  await controller.removeHeartbeat({ id: 'sync' });
  await controller.stopHeartbeats();
  ```

  **After**

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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 signals** — `TaskSignalProvider` 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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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`.

  ```ts
  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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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**

  ```ts
  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)**

  ```ts
  // 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](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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:

  ```ts
  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`](https://github.com/mastra-ai/mastra/commit/b9a2961c1be81e3639c0879e58588c26dd0ae866))

- 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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18548))

- `DurableAgent` now matches `Agent` behavior in three places where the durable loop previously diverged: ([#18677](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18619))

- add observational memory extractors ([#18653](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18680))

- Bring `InngestAgent` (Inngest-backed durable agent) to parity with `DurableAgent` for per-call execution options, abort handling, idle-aware resume, and `generate()`. ([#18615](https://github.com/mastra-ai/mastra/pull/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`.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18649))

- Fixed background task execution metadata updates so they no longer rewrite the model-visible tool invocation state. ([#18556](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18608))

- Reduce repeated schema work during sub-agent tool conversion for more stable memory usage. ([#18566](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18713))

- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. ([#18655](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18630))

### [@mastra/ai-sdk@1.6.1](https://github.com/mastra-ai/mastra/blob/@mastra/ai-sdk@1.6.1/client-sdks/ai-sdk/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18592))

### [@mastra/client-js@1.29.0](https://github.com/mastra-ai/mastra/blob/@mastra/client-js@1.29.0/client-sdks/client-js/CHANGELOG.md)
#### Minor Changes

- **Added** heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. ([#18184](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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`.

  ```ts
  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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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:

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/18632))

- add Studio support for observational memory extractors ([#18655](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/blob/@mastra/deployer@1.48.0/packages/deployer/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18609))

  ```text
  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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18694))

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/editor@0.13.3/packages/editor/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18650))

  ```typescript
  new MastraEditor({
    builder: {
      enabled: true,
      configuration: {
        agent: {
          memory: { observationalMemory: { model: '__GATEWAY_OPENAI_MODEL_MINI__' } },
        },
      },
    },
  });
  ```

### [@mastra/evals@1.5.1](https://github.com/mastra-ai/mastra/blob/@mastra/evals@1.5.1/packages/evals/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18546))

### [@mastra/inngest@1.8.0](https://github.com/mastra-ai/mastra/blob/@mastra/inngest@1.8.0/workflows/inngest/CHANGELOG.md)
#### Minor Changes

- Added support for the fine-grained authorization (FGA) `actor` signal on the Inngest execution engine. ([#18674](https://github.com/mastra-ai/mastra/pull/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**

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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`.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  // 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](https://github.com/mastra-ai/mastra/blob/@mastra/libsql@1.14.3/stores/libsql/CHANGELOG.md)
#### Patch Changes

- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. ([#18655](https://github.com/mastra-ai/mastra/pull/18655))

### [@mastra/mcp@1.12.1](https://github.com/mastra-ai/mastra/blob/@mastra/mcp@1.12.1/packages/mcp/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18664))

### [@mastra/memory@1.22.0](https://github.com/mastra-ai/mastra/blob/@mastra/memory@1.22.0/packages/memory/CHANGELOG.md)
#### Minor Changes

- add observational memory extractors ([#18653](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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:

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/blob/@mastra/mongodb@1.11.1/stores/mongodb/CHANGELOG.md)
#### Patch Changes

- Made MongoDB store writes safe against partial failures, preventing orphaned records when an operation fails partway through. ([#18393](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18655))

### [@mastra/mysql@0.3.2](https://github.com/mastra-ai/mastra/blob/@mastra/mysql@0.3.2/stores/mysql/CHANGELOG.md)
#### Patch Changes

- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. ([#18655](https://github.com/mastra-ai/mastra/pull/18655))

### [@mastra/pg@1.14.3](https://github.com/mastra-ai/mastra/blob/@mastra/pg@1.14.3/stores/pg/CHANGELOG.md)
#### Patch Changes

- Fixed buffered observation extraction metadata so stored OM chunks keep extracted values and extraction failures across memory storage adapters. ([#18655](https://github.com/mastra-ai/mastra/pull/18655))

### [@mastra/playground-ui@38.0.0](https://github.com/mastra-ai/mastra/blob/@mastra/playground-ui@38.0.0/packages/playground-ui/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18511))

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/qdrant@1.1.1/stores/qdrant/CHANGELOG.md)
#### Patch Changes

- Improve type safety for `describeIndex` metric field — removes compiler suppression so TypeScript can fully validate the returned `IndexStats` shape ([#18573](https://github.com/mastra-ai/mastra/pull/18573))

### [@mastra/rag@2.4.0](https://github.com/mastra-ai/mastra/blob/@mastra/rag@2.4.0/packages/rag/CHANGELOG.md)
#### Minor Changes

- Added MongoDBConfig to DatabaseConfig, exposing numCandidates for MongoDB Atlas Vector Search queries via the RAG tool layer. ([#18393](https://github.com/mastra-ai/mastra/pull/18393))

#### Patch Changes

### [@mastra/railway@0.2.1](https://github.com/mastra-ai/mastra/blob/@mastra/railway@0.2.1/workspaces/railway/CHANGELOG.md)
#### Patch Changes

- Fixed Railway sandbox templates so they are built once during sandbox creation. ([#18605](https://github.com/mastra-ai/mastra/pull/18605))

### [@mastra/schema-compat@1.3.2](https://github.com/mastra-ai/mastra/blob/@mastra/schema-compat@1.3.2/packages/schema-compat/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/pull/18608))

### [@mastra/server@1.48.0](https://github.com/mastra-ai/mastra/blob/@mastra/server@1.48.0/packages/server/CHANGELOG.md)
#### Minor Changes

- **Added** heartbeats: schedule an agent to run on a recurring cron, either inside an existing conversation thread or on its own. ([#18184](https://github.com/mastra-ai/mastra/pull/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.

  ```ts
  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`.

  ```ts
  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.

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/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:

  ```ts
  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](https://github.com/mastra-ai/mastra/pull/18609))

  ```ts
  // 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](https://github.com/mastra-ai/mastra/pull/18569))

- add Studio support for observational memory extractors ([#18655](https://github.com/mastra-ai/mastra/pull/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](https://github.com/mastra-ai/mastra/blob/@mastra/vercel@1.2.0/workspaces/vercel/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/pull/18667))
  - If you have been using `VercelSandbox` in your code, you should update your imports to use `VercelServerlessSandbox` instead.

    ```diff
    -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.

    ```diff
    -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:

    ```ts
    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:

- [@mastra/agent-builder@1.1.3](https://github.com/mastra-ai/mastra/blob/@mastra/agent-builder@1.1.3/packages/agent-builder/CHANGELOG.md)
- [@mastra/deployer-cloud@1.48.0](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-cloud@1.48.0/deployers/cloud/CHANGELOG.md)
- [@mastra/deployer-cloudflare@1.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-cloudflare@1.2.3/deployers/cloudflare/CHANGELOG.md)
- [@mastra/deployer-netlify@1.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-netlify@1.2.3/deployers/netlify/CHANGELOG.md)
- [@mastra/deployer-vercel@1.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-vercel@1.2.3/deployers/vercel/CHANGELOG.md)
- [@mastra/express@1.4.3](https://github.com/mastra-ai/mastra/blob/@mastra/express@1.4.3/server-adapters/express/CHANGELOG.md)
- [@mastra/fastify@1.4.3](https://github.com/mastra-ai/mastra/blob/@mastra/fastify@1.4.3/server-adapters/fastify/CHANGELOG.md)
- [@mastra/hono@1.5.3](https://github.com/mastra-ai/mastra/blob/@mastra/hono@1.5.3/server-adapters/hono/CHANGELOG.md)
- [@mastra/koa@1.6.3](https://github.com/mastra-ai/mastra/blob/@mastra/koa@1.6.3/server-adapters/koa/CHANGELOG.md)
- [@mastra/longmemeval@1.1.3](https://github.com/mastra-ai/mastra/blob/@mastra/longmemeval@1.1.3/explorations/longmemeval/CHANGELOG.md)
- [@mastra/mcp-docs-server@1.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/mcp-docs-server@1.2.3/packages/mcp-docs-server/CHANGELOG.md)
- [@mastra/nestjs@0.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/nestjs@0.2.3/server-adapters/nestjs/CHANGELOG.md)
- [@mastra/next@0.2.2](https://github.com/mastra-ai/mastra/blob/@mastra/next@0.2.2/server-adapters/next/CHANGELOG.md)
- [@mastra/opencode@0.1.3](https://github.com/mastra-ai/mastra/blob/@mastra/opencode@0.1.3/integrations/opencode/CHANGELOG.md)
- [@mastra/react@1.2.1](https://github.com/mastra-ai/mastra/blob/@mastra/react@1.2.1/client-sdks/react/CHANGELOG.md)
- [@mastra/tanstack-start@0.2.2](https://github.com/mastra-ai/mastra/blob/@mastra/tanstack-start@0.2.2/server-adapters/tanstack-start/CHANGELOG.md)
- [@mastra/temporal@0.2.3](https://github.com/mastra-ai/mastra/blob/@mastra/temporal@0.2.3/workflows/temporal/CHANGELOG.md)
- [@mastra/voice-google-gemini-live@0.14.2](https://github.com/mastra-ai/mastra/blob/@mastra/voice-google-gemini-live@0.14.2/voice/google-gemini-live-api/CHANGELOG.md)
- [@mastra/voice-openai-realtime@0.13.2](https://github.com/mastra-ai/mastra/blob/@mastra/voice-openai-realtime@0.13.2/voice/openai-realtime-api/CHANGELOG.md)
- [@mastra/voice-xai-realtime@0.2.2](https://github.com/mastra-ai/mastra/blob/@mastra/voice-xai-realtime@0.2.2/voice/xai-realtime-api/CHANGELOG.md)