@mastra/core@1.56.0

gitroomhq/postiz-app@mastra/core@1.56.0Aug 6, 2026by Patrycja-J

AI Summary

Introduces persistable declarative workflows (JSON graphs), stored workflow APIs, and a predicate DSL for conditionals. Also upgrades experiments with finer scorer control and persistence options.

Key Highlights

  • Persistable Declarative Workflows allowing graphs to be authored as data and round-trip through JSON.
  • Stored workflows end-to-end with HTTP APIs, Client SDK, and DB persistence.
  • Declarative Predicate DSL for conditionals and nested workflow steps.
  • Experiments & Evals upgrades including item-level scorerIds and persistence toggles.
  • Observability improvements including batch traceId filtering and tool span IDing.

Breaking Changes

  • Removed `MASTRA_PLATFORM_SECRET_KEY` auth for workspace providers; use `MASTRA_PLATFORM_ACCESS_TOKEN` or pass `accessToken` explicitly.

New Features

  • Declarative step entry types (`agent`, `tool`, `mapping`).
  • New `toStorableGraph` and `rehydrateWorkflow` helpers.
  • Item-level scorer selection for dataset experiments.
  • Awaited `onEvent` observer for ordered lifecycle events.

Full Release Notes

## Highlights

### Persistable Declarative Workflows (JSON graphs + rehydration)
Workflows can now be authored as data and round-trip through JSON via new declarative step entry types (`agent`, `tool`, `mapping`, plus composite entries), with `toStorableGraph()` and `rehydrateWorkflow()` enabling UI/LLM-built graphs that survive restarts and keep schema-typed chaining.

### Stored Workflows End-to-End: HTTP APIs + Client SDK + DB Persistence
Stored workflows are now manageable over HTTP (`POST/GET/DELETE /stored/workflows`) with actionable validation at the API boundary, a new `StoredWorkflow` client resource in `@mastra/client-js`, and a new `workflowDefinitions` storage domain implemented across major DB adapters so stored definitions persist beyond in-memory core.

### Declarative Predicate DSL + Nested Workflow Steps (storage-safe conditionals/loops)
Conditionals and loops can be persisted using a structural predicate DSL (instead of closure-only predicates), and nested workflows are now a first-class serialized step type (`type: 'workflow'`) with reference resolution and cycle detection—unlocking stored workflow graphs that branch/loop and compose other workflows reliably.

### Experiments & Evals Upgrades: finer scorer control, persistence controls, and safer tool policies
Dataset experiments gain item-level `scorerIds`, per-run persistence toggles (run targets/scorers without writing experiments/scores), scorer failure results via `ScorerRunError`, and `unmockedToolPolicy` to deny undeclared tool calls—plus a new awaited `onEvent` observer for ordered lifecycle events.

### Observability & Streaming Reliability Improvements
Metric queries now support batch `traceIds` filtering across observability clients/stores, tool spans gain `toolCallId` for pairing calls/results, and bridges can release filtered spans via optional `releaseSpan` to prevent leaks; streaming stability also improves (multi-consumer streams no longer break when one disconnects, workflow streams emit final results on `workflow-finish`).

### Breaking Changes
- `@mastra/platform-workspace@1.0.0`: removed `MASTRA_PLATFORM_SECRET_KEY` auth for workspace providers; use platform-injected `MASTRA_PLATFORM_ACCESS_TOKEN` or pass `accessToken` explicitly.

## Changelog

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

- Added item-level scorer selection for dataset experiments. ([#20190](https://github.com/mastra-ai/mastra/pull/20190))

  Dataset items now accept `scorerIds`. Experiments select one scorer source in this order: explicitly provided run-level scorers, item scorer IDs, then dataset scorer IDs. Use `[]` at the run or item level to select no scorers.

  ```typescript
  await dataset.addItem({
    input: 'Evaluate this response',
    scorerIds: ['accuracy'],
  });

  await dataset.updateItem({
    itemId: 'item-id',
    scorerIds: null,
  });
  ```

  Omit `scorerIds` to inherit or preserve the current override, use `[]` to run no scorers for an item, and update with `null` to restore dataset inheritance. Missing item-level scorer IDs fail only the affected item before target execution.

  Run-level scorers now replace dataset-attached defaults instead of merging with them. Previously, this configuration ran both `latency` and the dataset's `accuracy` scorer:

  ```typescript
  await dataset.startExperiment({
    targetType: 'agent',
    targetId: 'support-agent',
    scorers: ['latency'],
  });
  ```

  It now runs only `latency`. To keep both scorers, provide both in the run-level list:

  ```typescript
  await dataset.startExperiment({
    targetType: 'agent',
    targetId: 'support-agent',
    scorers: ['latency', 'accuracy'],
  });
  ```

- Declarative, persistable workflow graphs. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Workflows can now be authored as data — a UI, an LLM, or an operator can construct a workflow and have it survive process restarts. The step graph carries dedicated `agent` / `tool` / `mapping` entries (plus static `parallel` / `foreach` / `sleep` / `sleepUntil`) that round-trip through JSON, persist through the new stored-workflow endpoints, rehydrate, and run.

  **New declarative entry types in the step graph.** `.agent(agentOrId)`, `.tool(toolOrId)`, and `.map(...)` now emit dedicated `type: 'agent' | 'tool' | 'mapping'` entries into both `stepFlow` (live) and `serializedStepFlow` (JSON-safe), instead of collapsing into an opaque generic `step`. Existing `.then(createStep(agent))` / `.then(createStep(tool))` / `.map()` calls keep working and are auto-migrated to the new entries. `SingleStepEntry` (a new union of `step | agent | tool | mapping`) is now the shape used inside `parallel` and `conditional` `steps` arrays as well.

  Both engines interpret declarative entries per kind at the invoke point (via internal `step-entry` accessors and per-kind entry executors — `getEntryId` / `getEntryWorkflow` are exported for integrations) instead of materializing them into synthetic `Step` objects. The internal deep-import module `@mastra/core/dist/workflows/inner-step` (`getInnerStepId` / `materializeInnerStep`, never part of the public barrel) has been removed.

  **New builder ergonomics.**

  ```ts
  // Before: agents/tools were wrapped via createStep and lost their identity in the graph
  workflow.then(createStep(myAgent)).then(createStep(myTool));

  // After: dedicated builders (createStep still works)
  workflow
    .agent(myAgent) // output inferred as { text: string }
    .agent(myAgent, { structuredOutput: { schema } }) // output inferred from the schema
    .tool(myTool) // output inferred from the tool's outputSchema
    .agent(myAgent, undefined, { id: 'reviewer' }) // reuse the same agent under a distinct step id
    .agent('my-registered-agent-id'); // resolved against the Mastra instance at run time
  ```

  `.tool()` and `.agent()` enforce input/output schema chaining the same way `.then()` does — mismatched chains are compile-time errors. Agent steps type their input as `{ prompt: string }`.

  **New workflow-definitions storage domain.** `WorkflowDefinitionsStorage` (`upsert` / `get` / `list` / `delete` on JSON-safe `WorkflowDefinition`s) plus `Mastra.addStoredWorkflow(definition)` for persisting and live-registering a workflow. An in-memory implementation ships in core; database-backed stores provide their own implementations of the same domain interface.

  **New (de)serialization helpers.** `toStorableGraph(stepFlow)` turns a live workflow into a JSON-safe graph; `rehydrateWorkflow(def, mastra, opts?)` reconstructs the live workflow (including top-level workflow `metadata`). Referenced agents/tools must be registered on the target `Mastra` at rehydration time — otherwise rehydration hard-crashes rather than silently dropping.

  **Two-sided contract for unsupported JSON Schema keywords.** The MVP `jsonSchemaToZod` doesn't support `oneOf` / `anyOf` / `allOf` / `not` / `$ref` / `patternProperties` / `discriminator` (or unknown `type`s):

  - **Save path** (`Mastra.addStoredWorkflow`) is strict: the author is right there, so it throws before touching storage or registry, naming the offending schema (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, or `step "<id>" outputSchema` reached through `parallel` / `foreach` / `conditional` / `loop`). Simplify the schema or extend the converter before saving.
  - **Load path** (boot-time `#loadStoredWorkflows`) is lenient: `jsonSchemaToZod` accepts an `{ onUnsupportedSchema: 'warn', onUnsupported }` option that degrades the unsupported subtree to `z.any()` and emits a warning through the Mastra logger. One bad pre-existing row (e.g. a definition written by an older version) can't take down startup for every other workflow.

  **Agent-step `structuredOutput` and JSON-safe options now round-trip.** The serialized `agent` entry carries an `outputSchema` field (JSON Schema Draft 2020-12) and rehydration reconstructs the equivalent `structuredOutput` wiring. `retries` and `metadata` round-trip on both `agent` and `tool` entries. Closure-valued options (`onFinish`, `onChunk`, `onError`, `onStepFinish`, `onAbort`, function-valued `scorers` / `toolChoice`) hard-crash at `toStorableGraph` time instead of silently dropping. This is what makes patterns like `tool → agent-with-array-outputSchema → foreach(agent)` persistable end-to-end.

  **`foreach` / `dowhile` / `dountil` inner steps are now `SingleStepEntry`.** Both at the live `stepFlow` level and, for `foreach`, in the serialized graph. Fixes the previous round-trip bug where an agent-bodied `foreach` was persisted as an id-only descriptor and rehydrated as the wrong kind of step (looked up in the tool registry). `foreach.step` preserves the stored step id (which can differ from the underlying agent/tool id), and the agent/tool `outputSchema` + JSON-safe options round-trip through the foreach body. `loop.step` is typed as `SerializedSingleStepEntry` in the serialized graph as well, matching `foreach` and matching the shape the builder actually emits. Mapping entries are rejected inside `foreach` / `parallel` at serialize and rehydrate time — mappings project data, they don't execute per item.

  **Mapping templates now accept `${stepResults.<stepId>}` with no subpath, and stringify objects/arrays as JSON.** Primitive step outputs render via `String(v)`; object and array outputs render via `JSON.stringify` and are inlined into the template. This makes `foreach(agent) → mapping → synthesis-agent` work naturally — the mapping hands the full `{ text: string }[]` output to a downstream agent as one JSON blob, instead of forcing callers to fake indexed access (`${stepResults.<id>.0.text}`, `.1.text`, …) up to a fixed slot count. A step whose whole result is nullish is reported as missing (the template throws with the offending placeholder); nullish values resolved from a subpath inside a present result render as empty strings. Unrepresentable values (circular references, `BigInt`) throw with a hint pointing at the placeholder.

  **Workflow streams now publish the final workflow result before closing.** Successful runs include the canonical result on the closing `workflow-finish` chunk (`payload.finalWorkflowResult`), so stream-only consumers can read the result without a race-prone second fetch. Non-success and tripwire payloads are unchanged.

  **New `Mastra.removeWorkflow(keyOrId)` public API** mirroring `removeAgent` / `removeTool`. `Mastra.addStoredWorkflow(def)` now unregisters any existing live workflow with the same id before rehydrating and re-registering, so re-saving a stored workflow surfaces the new graph immediately instead of being silently no-op'd by `addWorkflow`'s first-write-wins guard. Fixes the stale-workflow bug where `deleteWorkflow` + `addStoredWorkflow` served the previous graph until the process restarted.

  **`Mastra.addStoredWorkflow` now performs a registry pre-flight before rehydrating.** Every `agentId` in the graph must resolve via `listAgents()` (and must not collide with a tool id), and every `toolId` must resolve via `listTools()` (and must not collide with an agent id). Previously, invalid or mis-classified ids failed deep inside `rehydrateWorkflow` with a less-actionable error (`Tool with name X not found`, or a silent lookup of an agent id in the tool registry). HTTP callers and direct `addStoredWorkflow` consumers now share one contract with the same actionable error messages.

  **One validation domain for stored workflow definitions.** All stored-definition checks now live in `@mastra/core`'s internal `workflows/stored/validate/` modules as a single issue-collecting core (`validateStoredWorkflow(def, registryIndex) → { code, path, message }[]`, plus a throwing `assertValidStoredWorkflow` used by the save path). Structure rules (ids, duplicates, mapping placement, nested-workflow identity, self-reference, declarative-predicate arity), reference checks (with mis-classification swap hints; mappings and declarative predicates may reference any runtime-visible step — including steps inside `parallel` / `conditional` / `foreach` / `loop` containers — and references to unknown steps are rejected before publication), JSON-Schema keyword checks, and the schema-flow type-checker (each step's input checked against the preceding output, foreach item schemas, loop feedback, final output vs. `outputSchema`) all run from the same walker over the serialized graph union. The builder authoring types (`WorkflowBuilderGraphEntry` and friends) from `@mastra/core/workflows/builder` are derived from `SerializedStepFlowEntry` instead of hand-duplicated, so the two can no longer drift. **Behavior change:** `Mastra.addStoredWorkflow` (and therefore `POST /stored/workflows`) now runs the schema-flow analysis with the live registries' schemas at save time — schema-incompatible graphs that previously saved silently are rejected with `incompatible-schema` issues. Boot-time loading of existing stored rows stays lenient.

  **New declarative predicate DSL for `.branch()` / `.dowhile()` / `.dountil()`.** Conditional branches and loop conditions can now be authored as a small structural JSON expression instead of (or alongside) a JS closure — the shape that finally lets `conditional` and `loop` step entries round-trip through storage. Nothing about existing closure-based conditions changes: the previous `(ctx) => boolean` overloads still work, still evaluate exactly the way they did, and their `serializedCondition.fn` string is still serialized unchanged.

  Opt in by passing `{ predicate }` in place of the closure:

  ```ts
  import type { Predicate } from '@mastra/core/workflows';

  workflow
    .then(loadUser)
    .branch([
      [{ predicate: { op: 'eq', left: { path: 'inputData.role' }, right: 'admin' } }, adminStep],
      [{ predicate: { op: 'truthy', value: { path: 'inputData.isGuest' } } }, guestStep],
    ])
    .commit();

  workflow
    .then(tick)
    .dountil(tick, { predicate: { op: 'gte', left: { path: 'inputData.count' }, right: 3 } })
    .commit();
  ```

  The DSL supports `eq` / `ne` / `lt` / `lte` / `gt` / `gte` / `in` / `notIn` / `exists` / `notExists` / `truthy` / `falsy` and the logical combinators `and` / `or` / `not`. Values are either literals or `{ path: '<scope>.<field>...' }` references — `inputData.*` for the previous step's output, `initData.*` for the workflow's initial input, `stepResults.<id>[.<path>]` for a named earlier step's output (scalar step results resolve with no subpath), and `state.*` for the workflow state slot. Missing paths resolve to `undefined` rather than throwing, so `exists` / `notExists` do what you'd expect. `evaluatePredicate(predicate, context)` and `derivePredicateLabel(predicate)` are exported from `@mastra/core/workflows` for callers that want to reuse the evaluator or render the human-readable summary.

  The declarative form is what unlocks persistence for `conditional` and `loop` step entries: their serialized shape now carries a `predicates: Predicate[]` (conditional) / `predicate: Predicate` (loop) field that survives `toStorableGraph` and `rehydrateWorkflow`. Closure-only `.branch()` / `.dowhile()` / `.dountil()` calls remain live-only and continue to throw at `toStorableGraph` time with a message pointing at the predicate DSL. Stored `conditional` / `loop` entries also carry the derived human-readable condition labels (`serializedConditions` / `serializedCondition`) generated from the predicate, so UIs render the same labels for stored and code-authored workflows. Rehydrated `parallel` / `conditional` inner agent steps now preserve `outputSchema` (structured output), `retries`, and `metadata` — previously these were silently dropped on load — and serialize → rehydrate → serialize is idempotent.

  **Nested workflows as a first-class serialized step type.** `SerializedSingleStepEntry` and `SerializedStepFlowEntry` gain a new `{ type: 'workflow', id, workflowId, description? }` variant. Any `.then(subWorkflow)` (or nesting inside `parallel` / `conditional` / `foreach` / `dowhile` / `dountil`) now serializes to this variant instead of a generic `type: 'step'` entry, and stored (JSON) workflows can reference other registered workflows by id. The live `stepFlow` is unchanged — `SingleStepEntry` / `StepFlowEntry` still use `type: 'step'` for nested workflows at runtime, so all existing engine code, `component === 'WORKFLOW'` checks, and execution paths continue to work. Rehydration resolves `workflowId` against `mastra.listWorkflows()` and hard-crashes with an actionable error if the reference is missing.

  `Mastra.addStoredWorkflow`'s pre-flight `collectRefs` and boot-time `#loadStoredWorkflows` both understand the new variant. Cross-workflow references between stored workflows are supported and load-ordered via a two-pass topological sort with Kahn's algorithm; cycles (including self-reference) are detected and rejected with a "detected cycle: A → B → A" error rather than infinite-looping the rehydrator. This is what makes patterns like `parent-workflow → conditional → { child-workflow-A, child-workflow-B }` seedable end-to-end from JSON.

  **Backward compatibility.** Existing `.then(createStep(agent))`, `.then(createStep(tool))`, `.map()`, `.parallel()`, and `.branch()` usages keep working and now emit the new declarative entries automatically. Closure-based `.branch()` / `.dowhile()` / `.dountil()` continue to evaluate exactly as before. Adopt the declarative predicate form only if you want the condition to survive `toStorableGraph` / `rehydrateWorkflow`.

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

- `RegexFilterProcessor` now reports what the `redact` strategy rewrote, through the existing `Processor.onViolation` callback. ([#20445](https://github.com/mastra-ai/mastra/pull/20445))

  Redaction used to be silent. The processor found its matches, replaced the text, and dropped the match list, leaving nothing downstream to audit. It now reports once per redacted message, message part, or stream chunk, with offsets relative to that piece of text.

  ```ts
  const filter = new RegexFilterProcessor({
    presets: ['pii'],
    strategy: 'redact',
  });

  filter.onViolation = async ({ detail }) => {
    const redaction = detail as RegexRedactionDetail;

    for (const entry of redaction.redactions) {
      await auditLog.write({
        phase: redaction.phase, // processInput | processOutputStream | processOutputResult
        messageId: redaction.messageId,
        rule: entry.rule, // 'credit-card'
        offset: entry.index,
        length: entry.length,
      });
    }
  };
  ```

  Async callbacks are awaited. When no callback is attached the redact path stays synchronous, so nothing changes for existing callers. When two rules match the same span, the winning rule is reported as `rule` and every rule that matched is listed in `overlappingRules`.

  **Values are withheld by default**

  Reports carry offsets and rule names, not the matched text. An audit trail that copies the data it protects widens the exposure it was added to narrow, which is why the `block` strategy already withholds matched text from its `TripWire` metadata. Set `includeRedactedValues: true` to add a `value` field when the destination is as protected as the original.

- Added per-run controls for experiment and score persistence. Targets and scorers continue to run, while callers can keep results in memory without writing selected record types to storage. ([#20643](https://github.com/mastra-ai/mastra/pull/20643))

  ```typescript
  const summary = await dataset.startExperiment({
    targetType: 'agent',
    targetId: 'my-agent',
    scorers: ['accuracy'],
    persistence: { experiments: 'none', scores: 'none' },
  });
  ```

- Added scorer failure results so callers can inspect completed stages and `status: 'failed'` judge executions in `error.result.judge` after `scorer.run()` rejects. ([#20195](https://github.com/mastra-ai/mastra/pull/20195))

  ```typescript
  import { ScorerRunError } from '@mastra/core/evals';

  try {
    await scorer.run(input);
  } catch (error) {
    if (error instanceof ScorerRunError) {
      console.log(error.failedStep);
      console.log(error.completedSteps);
      console.log(error.result);
    }
  }
  ```

- Added `unmockedToolPolicy` to experiments and dataset items so undeclared agent tool calls can be blocked before execution. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.startExperiment({
    targetType: 'agent',
    targetId: 'weather-agent',
    unmockedToolPolicy: 'deny',
  });
  ```

- Added an awaited semantic event observer for experiments. ([#20644](https://github.com/mastra-ai/mastra/pull/20644))

  Use `onEvent` to consume versioned, JSON-safe run and item lifecycle events in strict sequence order:

  ```ts
  await runExperiment(mastra, {
    task: async ({ input }) => processItem(input),
    data: items,
    onEvent: async event => {
      await publish(event);
    },
  });
  ```

  Terminal events are delivered before final experiment status persistence, allowing external workers to treat the ordered event stream as authoritative. Observer failures stop the run with an `EXPERIMENT_EVENT_OBSERVER_FAILED` error so workers can distinguish delivery failures from partial experiment results.

- Add optional `idleTimeoutMs` and `isAlive` options to `DurableAgent.observe()`. ([#20442](https://github.com/mastra-ai/mastra/pull/20442))

  When the process running a durable agent stops unexpectedly, the run stops producing updates but never emits a completion event — so a client that reconnects with `observe()` previously waited forever, with no way to tell the run was gone. With `idleTimeoutMs` set, the observed stream ends after that many milliseconds of silence. An optional `isAlive` check is consulted first: if it reports the run is still being worked on (for example a long-running tool call, or a run paused waiting for human input), the stream keeps waiting instead of ending. Fully backward-compatible — with neither option set, `observe()` behaves exactly as before.

  ```ts
  // Reconnect to an in-flight run, but stop waiting if the run is no longer running.
  const { output } = await agent.observe(runId, {
    idleTimeoutMs: 30_000,
    // Consulted only after idleTimeoutMs of silence. Return true while the run is
    // still being worked on to keep waiting; false (or omitted) ends the stream.
    isAlive: () => runHeartbeat.isFresh(runId),
  });

  // Omit both options for the previous behavior (wait indefinitely):
  const { output: legacy } = await agent.observe(runId);
  ```

#### Patch Changes

- Fixed review comments on experiment results not being saved. Experiment results now have a persisted comment field, and updateExperimentResult accepts a comment alongside status and tags. Fixes https://github.com/mastra-ai/mastra/issues/19857 ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

  ```ts
  const experimentsStore = await storage.getStore('experiments');
  await experimentsStore.updateExperimentResult({
    id: resultId,
    experimentId,
    comment: 'Agent hallucinated an API that does not exist',
  });
  ```

- Fixed conversations becoming permanently stuck after approving or declining a `requireApproval` tool call with Anthropic extended thinking enabled. Resuming now saves the model's continuation in a new assistant message, so the paused response stays intact and later turns keep working. ([#19486](https://github.com/mastra-ai/mastra/pull/19486))

- Update provider registry and model documentation with latest models and providers ([`7f4e26d`](https://github.com/mastra-ai/mastra/commit/7f4e26dd57bd9b23c278ea21235ab823a3810a6c))

- Fixed dropped non-text stream parts when `emitOnNonText: false`. ([#18561](https://github.com/mastra-ai/mastra/pull/18561))

  Tool calls, tool results, objects, and reasoning parts now stay in order with the text output instead of being silently lost during batching.

- Fixed a crash in tool input validation when using Zod v4 compatibility schemas that don't provide a native JSON Schema. ([#19030](https://github.com/mastra-ai/mastra/pull/19030))

- Fixed TokenLimiter silently dropping agent output when used as an output processor. It counted every stream part — including lifecycle parts like `step-start` (which embeds the full serialized request) and reasoning deltas — so a realistic limit could be exhausted before any answer text arrived. Once the limit was hit, all later parts were withheld, including `tool-call` parts, so agents returned empty text or stopped executing tools with no error. ([#20256](https://github.com/mastra-ai/mastra/pull/20256))

  Now only generated output counts against the limit (text and object parts), tool and lifecycle parts always pass through, and the first time output is withheld the processor emits a transient `data-token-limit-reached` part so truncation is visible:

  ```typescript
  for await (const part of stream.fullStream) {
    if (part.type === 'data-token-limit-reached') {
      console.log('output truncated at', part.data.limit, 'tokens');
    }
  }
  ```

  Fixes #20250

- Fixed delegated tool approvals not resuming after a page refresh or server restart. Approvals saved in conversation metadata previously pointed at a sub-agent run that could not be resumed. They now point at the supervisor run, so the saved approval works directly with `resumeStream()` and `approveToolCall()`. Approvals saved before this fix keep working. ([#19645](https://github.com/mastra-ai/mastra/pull/19645))

- Added an optional `releaseSpan` method to the observability bridge interface. ([#20463](https://github.com/mastra-ai/mastra/pull/20463))

  Bridges hold per-span state from `createSpan()` until the span ends, but span-end events are only delivered for spans that survive export filtering, so a bridge had no way to learn that a filtered span had finished. `releaseSpan(spanId, traceId)` is now called for those spans.

  If you maintain a custom bridge, implement it to drop whatever `createSpan()` allocated. Do not end or send the span — it was filtered out on purpose.

  ```typescript
  class MyBridge implements ObservabilityBridge {
    private spans = new Map<string, MySpan>();

    createSpan(options) {
      const span = myTracer.start(options.name);
      this.spans.set(span.id, span);
      return { spanId: span.id, traceId: span.traceId };
    }

    // Called when a span ends but is dropped by excludeSpanTypes,
    // a spanFilter, or a span output processor.
    releaseSpan(spanId: string, _traceId: string) {
      this.spans.delete(spanId);
    }
  }
  ```

  The method is optional, so bridges that omit it keep working unchanged.

  Fixes [#20368](https://github.com/mastra-ai/mastra/issues/20368).

- Use the configured ID generator for message rows persisted through agent signal APIs. ([#17857](https://github.com/mastra-ai/mastra/pull/17857))

- Fixed an issue in the agentic loop where an aborted or failed LLM stream would still trigger output processors and improperly persist the user's input message as an orphaned record. ([#19716](https://github.com/mastra-ai/mastra/pull/19716))

- Fixed durable agents losing track of parallel sub-agent approvals. When a durable supervisor delegated to multiple sub-agents in parallel and more than one required approval, only the first approval was persisted — the rest disappeared from the conversation metadata. Also fixed listSuspendedRuns() never returning suspended durable agent runs. Both parallel approvals now persist, the suspended run is discoverable, and the approvals can be resumed in any order. ([#20529](https://github.com/mastra-ai/mastra/pull/20529))

- Fixed sub-agent delegation prompts being ordered before forwarded supervisor context. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

- Fixed a bug where disconnecting one consumer of a workflow's `fullStream` (or a model's evented stream) would silently stop every other concurrent consumer of the same run from receiving further chunks. This affected cases like two `/stream` requests for the same `runId`, or `/stream` combined with `/observe` — one client disconnecting no longer breaks the others, which now keep receiving chunks and close normally. ([#19745](https://github.com/mastra-ai/mastra/pull/19745))

- Fixed tool executions silently losing request context when a bundler or monorepo loads more than one copy of @mastra/core. Previously, a request context created by a different copy of the package was not recognized, so the tool received an empty context or the entries passed at execution time were dropped from the merge. Request context values now reach the tool regardless of which copy of the package created them. Closes #19772. ([#19863](https://github.com/mastra-ai/mastra/pull/19863))

- Added toolCallId to TOOL_CALL and MCP_TOOL_CALL span attributes so observability exporters can pair tool results with their calls. Previously the tool call ID was available at the call site but never forwarded to the span, causing downstream exporters to lose the association between a tool call and its result. ([#19405](https://github.com/mastra-ai/mastra/pull/19405))

- Fixed provider-executed tool spans appearing outside the model step in traces. `PROVIDER_TOOL_CALL` spans now nest under the model step that delivered the tool result — matching how regular tool calls are traced — with their start time backdated to the tool call, so tools like OpenAI-hosted web search show up in the right place in the timeline. Tool input is now also captured whenever the provider supplies arguments. Calls whose result never arrives stay anchored to the agent run span. Fixes #20335. ([#20522](https://github.com/mastra-ai/mastra/pull/20522))

- Fixed custom data chunks emitted by processors so they are saved with thread messages unless marked transient. ([#19375](https://github.com/mastra-ai/mastra/pull/19375))

- Fixed agent thread subscriptions so every instance in a multi-instance deployment sees the same conversation: ([#19806](https://github.com/mastra-ai/mastra/pull/19806))

  - Subscribers on any instance now replay completed runs identically instead of diverging from the instance that ran them.
  - Reconnecting to a thread no longer wedges `agent.stream()` behind a stale run left by a crashed or finished process.
  - Aborting a thread now works from any instance — the request is routed to the process that owns the run.
  - A `stream()`/`generate()` call started while another instance is mid-run on the same thread now waits its turn instead of interleaving output.

- Fixed `declineToolCall` and `declineToolCallGenerate` so declined approval-gated tools no longer execute or cause side effects after resume. Fixes https://github.com/mastra-ai/mastra/issues/20470 ([#20487](https://github.com/mastra-ai/mastra/pull/20487))

- Fixed durable delegated tool approvals so persisted approvals resume the suspended sub-agent, including after a server restart. ([#20492](https://github.com/mastra-ai/mastra/pull/20492))

- Fixed thread title generation using messages from other threads when memory is resource-scoped. Titles for new threads are now derived only from the messages of the thread being titled, instead of the full message list which can include recalled messages from the user's other conversations. ([#19856](https://github.com/mastra-ai/mastra/pull/19856))

- Added an optional `instructions` field to the Observational Memory `retrieval` config type. It appends application-specific recall guidance after Mastra's built-in retrieval instructions. ([#20160](https://github.com/mastra-ai/mastra/pull/20160))

- Fixed `RegexFilterProcessor` leaving part of a matched value in the output when two rules match overlapping text. ([#20445](https://github.com/mastra-ai/mastra/pull/20445))

  **What was wrong**

  Rules were applied one at a time with `String.replace`, so an earlier rule could consume the start of a longer match and leave the rest in the clear. With the `pii` preset, `phone` runs before `credit-card`, so a card number written without separators lost only its first ten digits:

  ```ts
  const filter = new RegexFilterProcessor({ presets: ['pii'], strategy: 'redact' });

  // Before: "card [PHONE]111111"
  // After:  "card [CREDIT_CARD]"
  ```

  Overlapping matches are now combined into one region and replaced once, using the replacement of the longest match.

  **Two smaller changes to redaction**

  - A replacement that references capture groups (`$1`, `$&`) now falls back to the replacement string as written when the rule cannot match the text it matched in isolation, which happens with a lookbehind or lookahead. The region is still redacted.
  - A rule that only matches empty strings no longer inserts its replacement between every character.

- Fixed suspended DurableAgent tools to receive primitive resume data such as native ask_user answers. ([#19750](https://github.com/mastra-ai/mastra/pull/19750))

- Fixed `RequestContext.toJSON()` so deeply shared object graphs no longer block the event loop during serialization. Values that exceed the serialization safety limit are filtered instead. ([#20375](https://github.com/mastra-ai/mastra/pull/20375))

- Added a typed `model` override to `approveToolCall`, `declineToolCall`, `approveToolCallGenerate`, and `declineToolCallGenerate`. The resume path already honored `model` (via `resumeStream`/`resumeGenerate`), but the public approve/decline signatures omitted it, so agents whose model is resolved per run had no typed way to pick the model for the resumed segment without casting past the signature. ([#20212](https://github.com/mastra-ai/mastra/pull/20212))

  ```ts
  // Now type-checks — previously required casting past the public type
  await agent.approveToolCall({ runId, model: myRunModel });
  ```

- Fixed durable agents failing to resume delegated sub-agent or workflow tools that suspend mid-execution without an approval. The suspended inner run is now persisted with the tool call, so resumeStream() continues that run instead of restarting the delegate — including after a server restart. See #20496 ([#20502](https://github.com/mastra-ai/mastra/pull/20502))

### [@mastra/ai-sdk@1.7.1](https://github.com/mastra-ai/mastra/blob/@mastra/ai-sdk@1.7.1/client-sdks/ai-sdk/CHANGELOG.md)
#### Patch Changes

- Fixed the AI SDK stream announcing a response message id that matches no stored message when a processor rotates the response message id before the model runs (for example observational memory sealing a buffer chunk). The stream's `start` chunk now announces the id the assistant response is actually persisted under, so `useChat` and other AI SDK consumers can reconcile streamed messages with memory. Fixes [#19810](https://github.com/mastra-ai/mastra/issues/19810) ([#20084](https://github.com/mastra-ai/mastra/pull/20084))

- Fixed persisted signal finish chunks so AI SDK consumers can convert both newly produced and previously retained stream events. ([#19806](https://github.com/mastra-ai/mastra/pull/19806))

### [@mastra/braintrust@1.3.2](https://github.com/mastra-ai/mastra/blob/@mastra/braintrust@1.3.2/observability/braintrust/CHANGELOG.md)
#### Patch Changes

- Fixed observability exporters to read toolCallId from span attributes (with metadata fallback). Braintrust Thread view now shows tool results by pairing them via the real tool call ID. Sentry and OTel exporters also pick up the ID consistently across all tool span types. ([#19405](https://github.com/mastra-ai/mastra/pull/19405))

### [@mastra/clickhouse@1.14.0](https://github.com/mastra-ai/mastra/blob/@mastra/clickhouse@1.14.0/stores/clickhouse/CHANGELOG.md)
#### Minor Changes

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

#### Patch Changes

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

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

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

- Stored-workflow client API. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  New `StoredWorkflow` resource and client methods for the stored-workflow endpoints:

  ```ts
  const { workflows } = await client.listStoredWorkflows({ status: 'active' });
  await client.upsertStoredWorkflow({ id: 'greeting-workflow' /* definition */ });

  const stored = client.getStoredWorkflow('greeting-workflow');
  const definition = await stored.details();
  await stored.delete();
  ```

  Workflow list/detail responses also gain an `origin` field (`'code' | 'stored'`) indicating how the workflow entered the live registry.

#### Patch Changes

- Added comment support to the experiment result API. The PATCH experiment result endpoint and the client updateDatasetExperimentResult method now accept and return a comment field, so review comments persist server-side instead of being lost on reload (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

  ```ts
  const result = await client.updateDatasetExperimentResult({
    datasetId,
    experimentId,
    resultId,
    comment: 'Agent hallucinated an API that does not exist',
  });
  ```

- Added typed item-level `scorerIds` support for creating, batch-creating, updating, reading, and inspecting dataset item versions. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await client.addDatasetItem({
    datasetId: 'dataset-id',
    input: 'Evaluate this response',
    scorerIds: [],
  });
  ```

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

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

### [@mastra/code-sdk@1.1.2](https://github.com/mastra-ai/mastra/blob/@mastra/code-sdk@1.1.2/mastracode/sdk/CHANGELOG.md)
#### Patch Changes

- Added `skipGlobalInstructions` to session state. When set, a session ignores the agent instruction files in the machine's home directory (`~/.claude/CLAUDE.md`, `~/.mastracode/AGENTS.md`, and the other supported locations) and reads only the ones in the project it works on. Servers that run sessions on behalf of other people set it so a run never inherits the personal configuration of whoever hosts the process. ([#20633](https://github.com/mastra-ai/mastra/pull/20633))

  Seed it on the controller to cover every session it creates:

  ```ts
  prepareAgentControllerMount({
    initialState: { skipGlobalInstructions: true },
  });
  ```

  Sessions you drive yourself are unaffected and still read your home directory instructions.

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

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

### [@mastra/datadog@1.3.7](https://github.com/mastra-ai/mastra/blob/@mastra/datadog@1.3.7/observability/datadog/CHANGELOG.md)
#### Patch Changes

- Released bridge state for spans that end without being exported. ([#20463](https://github.com/mastra-ai/mastra/pull/20463))

  Spans dropped by `excludeSpanTypes`, a `spanFilter`, or a span output processor previously kept the span map entry and open-span count that `createSpan()` allocated for them, because that state was only freed when a span was exported. Both are now released when such a span ends.

  The dd span is not finished, so filtered spans are still not sent to Datadog.

  Fixes [#20368](https://github.com/mastra-ai/mastra/issues/20368).

### [@mastra/deployer@1.56.0](https://github.com/mastra-ai/mastra/blob/@mastra/deployer@1.56.0/packages/deployer/CHANGELOG.md)
#### Patch Changes

- Fixed workspace package changes not being picked up during mastra dev hot reload ([#20262](https://github.com/mastra-ai/mastra/pull/20262))

- Prevent background workflow recovery failures from terminating the server. ([#19639](https://github.com/mastra-ai/mastra/pull/19639))

- Fixed builds for transitive workspace dependencies that only expose subpath exports. ([#19808](https://github.com/mastra-ai/mastra/pull/19808))

### [@mastra/deployer-vercel@1.2.13](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-vercel@1.2.13/deployers/vercel/CHANGELOG.md)
#### Patch Changes

- Fixed custom API routes being unreachable when deploying to Vercel with `studio: true`. ([#20517](https://github.com/mastra-ai/mastra/pull/20517))

  Routes registered with `registerApiRoute()` are mounted at the root of the server, but the generated Vercel route table only forwarded `/api/*` and `/health` to your app. Every other path fell through to Studio's `index.html`, so a request to a custom route returned the Studio HTML page and the handler never ran. Moving the route under `/api` was not an option either, since that prefix is reserved for built-in routes.

  The route table now serves the paths Studio owns from the CDN and sends everything else to your server, so custom routes behave the same as they do with `mastra dev` and `studio: false`. Studio and its assets are still served as static files with no function invocations.

  ```ts
  export const mastra = new Mastra({
    deployer: new VercelDeployer({ studio: true }),
    server: {
      apiRoutes: [registerApiRoute('/my/webhook', { method: 'POST', handler: c => c.json({ ok: true }) })],
    },
  });
  ```

  `POST /my/webhook` now returns `{"ok":true}` instead of Studio's `index.html`.

  Requests to a custom `server.apiPrefix` now reach your server too, instead of being answered with the Studio page. Studio's own UI still calls `/api`, so pointing Studio at a custom prefix is not supported yet.

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

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

### [@mastra/duckdb@1.6.0](https://github.com/mastra-ai/mastra/blob/@mastra/duckdb@1.6.0/stores/duckdb/CHANGELOG.md)
#### Minor Changes

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

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

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

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

- Added a `summarization` scorer to `@mastra/evals`. It grades a summary on two axes and returns the lower score, so a summary cannot pass by being faithful but empty, or thorough but wrong. ([#20293](https://github.com/mastra-ai/mastra/pull/20293))

  **Alignment** checks that every claim in the summary is supported by the source text. **Coverage** draws closed-ended questions from the source and answers them using the summary alone, in a separate call that never receives the source, so a missing fact cannot be answered from the source instead. The final score is `min(alignment, coverage) × scale`, and the reason names the axis that produced it.

  The source text defaults to the user message of the run input. Pass `source` or `sourceExtractor` when the text being summarized comes from somewhere else, such as a tool result. `maxQuestions` bounds the coverage questions so cost does not grow with document length.

  ```ts
  import { createSummarizationScorer } from '@mastra/evals/scorers/prebuilt';

  const scorer = createSummarizationScorer({
    model: 'openai/gpt-5.5',
    options: { maxQuestions: 10 },
  });

  const result = await scorer.run(run);
  result.score;
  ```

  This restores the summarization metric that was removed with the legacy evals system, rebuilt on the scorers pipeline.

### [@mastra/express@1.4.13](https://github.com/mastra-ai/mastra/blob/@mastra/express@1.4.13/server-adapters/express/CHANGELOG.md)
#### Patch Changes

- Fixed a security issue where DELETE requests could bypass the server's body size limit. ([#20015](https://github.com/mastra-ai/mastra/pull/20015))

  Body size limits, both the global `server.bodySizeLimit` option and any route-specific `maxBodySize` override, were only enforced for POST, PUT, and PATCH requests. A DELETE request with a large body skipped this check entirely, so it was still read into memory in full. A malicious or misbehaving client could send DELETE requests with oversized bodies to exhaust server memory, regardless of the configured limit.

  DELETE requests are now checked against the same body size limits, global and route-specific, as the other body-bearing methods.

  While adding test coverage for this, we also found and fixed two related bugs that meant the body size limit wasn't being enforced at all for any method in some cases:

  - **Fastify**: the configured limit was attached to the route's `config` object, which Fastify's body parser never reads. Oversized requests of any method were let through.
  - **Hono**: when a custom `onError` handler was configured, its return value was passed straight through as the response instead of being wrapped in an HTTP response. This produced a broken response for any oversized request, instead of the intended `413`.

### [@mastra/factory@0.4.0](https://github.com/mastra-ai/mastra/blob/@mastra/factory@0.4.0/mastracode/factory/CHANGELOG.md)
#### Minor Changes

- Added a lightweight pending changes viewer with per-file line counts for Factory session workspaces and improved chat composer readability. ([#20418](https://github.com/mastra-ai/mastra/pull/20418))

#### Patch Changes

- Self-hosted GitHub deployments now detect merged pull requests. ([#20361](https://github.com/mastra-ai/mastra/pull/20361))

  Merge state previously reached the factory only through GitHub webhooks. A deployment GitHub cannot reach — local development, or any server behind a private network — never received one, so its pull request cards stayed `open` forever and merge rules never fired.

  A background sweep now reads live pull request state for the cards that are still open and replays missed merges through the normal rules ingress, which dedupes them against the webhook path. Webhooks remain the fast path; this is the safety net that was already running on platform-backed deployments.

  The sweep runs every 5 minutes, is scoped to repositories linked to a factory project, and coordinates across replicas so only one sweeps at a time.

  It also retires the thread's pull request subscription, which the webhook handler was previously the only thing to do. That is what the PR chip in a thread and the workspace sidebar row read, so on both self-hosted and platform deployments they now show merged or closed instead of staying open indefinitely.

  **Configuration**

  ```bash
  MASTRACODE_GITHUB_RECONCILE_ENABLED=false   # opt out entirely
  MASTRACODE_GITHUB_RECONCILE_INTERVAL_MS=60000  # change the cadence
  ```

- Improved Factory triage so editing a linked GitHub issue or creating, editing, or deleting a human comment re-runs investigation and refreshes the existing handoff comment. ([#20516](https://github.com/mastra-ai/mastra/pull/20516))

- Factory work item transitions now require explicit approval before execution. ([#20622](https://github.com/mastra-ai/mastra/pull/20622))

- Fixed Factory rule dispatches so concurrent skill wakeups stay bounded until their agent runs finish or terminal observation times out. ([#20623](https://github.com/mastra-ai/mastra/pull/20623))

- Improved Factory pull-request reviews by requiring comparison with analogous codebase patterns. ([#20524](https://github.com/mastra-ai/mastra/pull/20524))

- Fixed the Factory getting stuck after a GitHub App is uninstalled and reinstalled. ([#20481](https://github.com/mastra-ai/mastra/pull/20481))

  GitHub assigns a new installation ID on reinstall, which left every token request failing against the old one — recovering it needed a manual database edit. The Factory already knew how to repoint a repository at the replacement installation, but only triggered that recovery when the platform reported the old installation as missing (404). A suspended or soft-deleted installation reports as a conflict (409) instead, so the recovery never ran. It now covers both.

  A failed token mint that could equally be a transient GitHub outage (502) still surfaces as an error rather than repointing the repository, so a passing incident never migrates a healthy repository.

- Fixed GitHub issue intake pagination when platform responses contain fewer issues after filtering pull requests. ([#20637](https://github.com/mastra-ai/mastra/pull/20637))

- Fixed factory sessions inheriting the personal agent instructions of the machine hosting them. ([#20633](https://github.com/mastra-ai/mastra/pull/20633))

  A factory should behave the same wherever it runs. It did not: alongside the repository's AGENTS.md and the skill it was started with, every session also loaded the instruction files sitting in the home directory of whatever machine hosted the factory (`~/.claude/CLAUDE.md`, `~/.mastracode/AGENTS.md`, and the other supported home directory locations). Those files are the operator's personal preferences, so the same review rule produced a differently written review depending on who was running the factory, and nothing in the session showed why.

  Factory sessions now read only the repository's instructions (served from the pull request's base branch when the checkout is untrusted) and the skill. This applies to every session the factory creates: work items it picks up on its own, sessions a GitHub webhook resumes, and the ones you open yourself in the factory UI.

  If you were relying on a home directory file to steer factory output, move those instructions into the repository's AGENTS.md.

- Updated Factory triage to keep new features in Intake until manually advanced. ([#20624](https://github.com/mastra-ai/mastra/pull/20624))

### [@mastra/fastify@1.4.13](https://github.com/mastra-ai/mastra/blob/@mastra/fastify@1.4.13/server-adapters/fastify/CHANGELOG.md)
#### Patch Changes

- Fixed a security issue where DELETE requests could bypass the server's body size limit. ([#20015](https://github.com/mastra-ai/mastra/pull/20015))

  Body size limits, both the global `server.bodySizeLimit` option and any route-specific `maxBodySize` override, were only enforced for POST, PUT, and PATCH requests. A DELETE request with a large body skipped this check entirely, so it was still read into memory in full. A malicious or misbehaving client could send DELETE requests with oversized bodies to exhaust server memory, regardless of the configured limit.

  DELETE requests are now checked against the same body size limits, global and route-specific, as the other body-bearing methods.

  While adding test coverage for this, we also found and fixed two related bugs that meant the body size limit wasn't being enforced at all for any method in some cases:

  - **Fastify**: the configured limit was attached to the route's `config` object, which Fastify's body parser never reads. Oversized requests of any method were let through.
  - **Hono**: when a custom `onError` handler was configured, its return value was passed straight through as the response instead of being wrapped in an HTTP response. This produced a broken response for any oversized request, instead of the intended `413`.

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

- Fixed GitHub PR subscription notifications never firing on macOS. The gitcrawl database location is now resolved by asking gitcrawl itself (with the macOS ~/Library/Application Support location as a fallback) instead of assuming the Linux ~/.config path. Snapshot read failures are no longer silently swallowed - they are recorded on the subscription so polling problems are visible. ([#19465](https://github.com/mastra-ai/mastra/pull/19465))

### [@mastra/hono@1.5.13](https://github.com/mastra-ai/mastra/blob/@mastra/hono@1.5.13/server-adapters/hono/CHANGELOG.md)
#### Patch Changes

- Fixed server crashes when clients disconnect during streaming. ([#20312](https://github.com/mastra-ai/mastra/pull/20312))

- Fixed a security issue where DELETE requests could bypass the server's body size limit. ([#20015](https://github.com/mastra-ai/mastra/pull/20015))

  Body size limits, both the global `server.bodySizeLimit` option and any route-specific `maxBodySize` override, were only enforced for POST, PUT, and PATCH requests. A DELETE request with a large body skipped this check entirely, so it was still read into memory in full. A malicious or misbehaving client could send DELETE requests with oversized bodies to exhaust server memory, regardless of the configured limit.

  DELETE requests are now checked against the same body size limits, global and route-specific, as the other body-bearing methods.

  While adding test coverage for this, we also found and fixed two related bugs that meant the body size limit wasn't being enforced at all for any method in some cases:

  - **Fastify**: the configured limit was attached to the route's `config` object, which Fastify's body parser never reads. Oversized requests of any method were let through.
  - **Hono**: when a custom `onError` handler was configured, its return value was passed straight through as the response instead of being wrapped in an HTTP response. This produced a broken response for any oversized request, instead of the intended `413`.

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

- Agent and tool steps in Inngest workflows now behave identically to `@mastra/core` workflows — same streaming, tripwire, and tool-execution semantics — while keeping Inngest durability (steps still run inside `step.run` with retries). ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Previously, `createStep(agent)` and `createStep(tool)` from `@mastra/inngest` carried their own inline copies of the agent-streaming and tool-execution logic, forked from `@mastra/core`. Both now execute through core's shared entry executors, whether the step enters a workflow graph or its `execute` is invoked directly. The forked inline implementations were deleted.

  What changes for users:

  - **Tripwire chunks now abort the step.** The old inline copy had no tripwire handling — a `tripwire` chunk emitted by an output processor was forwarded downstream and the step returned `{ text }` as a success. The step now throws `TripWire` (with the processor's reason/retry/metadata), matching `@mastra/core` workflows.
  - **The agent's `onFinish` result is the sole source of the step's final text.** The old copy raced `modelOutput.text` against `onFinish`, so a throwing output processor could resolve the step with `{ text: '' }`. This adopts core's fix.
  - **Tool execution context gains `abortSignal`, top-level `resumeData`, and the resolved observability context**, matching what tools receive in `@mastra/core` workflows.
  - **A v1 model without `streamLegacy` no longer throws the Inngest-specific "does not implement streamLegacy" error** — it falls through to `stream()` like core does.

- Added toolCallId to TOOL_CALL and MCP_TOOL_CALL span attributes so observability exporters can pair tool results with their calls. Previously the tool call ID was available at the call site but never forwarded to the span, causing downstream exporters to lose the association between a tool call and its result. ([#19405](https://github.com/mastra-ai/mastra/pull/19405))

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

- Fixed message, thread, and resource updates to avoid recreating incomplete records when an update target is missing. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

### [@mastra/libsql@1.19.0](https://github.com/mastra-ai/mastra/blob/@mastra/libsql@1.19.0/stores/libsql/CHANGELOG.md)
#### Minor Changes

- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.addItem({
    input: 'What is the weather?',
    unmockedToolPolicy: 'deny',
  });
  ```

#### Patch Changes

- Added a comment column to experiment results so review comments persist. The column is added automatically and non-destructively on startup for existing databases (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

- Dataset item scorer selections now persist across LibSQL writes and reads. Setting `scorerIds` to `null` clears an item override, while `[]` remains an explicit override with no scorers. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await dataset.addItem({
    input: 'Evaluate this response',
    scorerIds: [],
  });
  ```

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

### [@mastra/longmemeval@1.1.13](https://github.com/mastra-ai/mastra/blob/@mastra/longmemeval@1.1.13/explorations/longmemeval/CHANGELOG.md)
#### Patch Changes

- Fixed a prompt-injection risk by rejecting system-role messages embedded in `prompt` or `messages` for AI SDK v5 calls. ([#20558](https://github.com/mastra-ai/mastra/pull/20558))

### [@mastra/mcp@1.15.1](https://github.com/mastra-ai/mastra/blob/@mastra/mcp@1.15.1/packages/mcp/CHANGELOG.md)
#### Patch Changes

- Fixed concurrent MCP tool calls failing while the client reconnects after a dropped connection. ([#20530](https://github.com/mastra-ai/mastra/pull/20530))

- Speed up MCP discovery when an `MCPClient` is configured with multiple servers. `listTools()`, `listToolsets()`, `resources.list()`, `resources.templates()`, and `prompts.list()` now query all configured servers concurrently instead of one at a time, so total discovery time is roughly the slowest single server rather than the sum of all of them, and one slow or unresponsive server no longer stalls discovery for the rest. Tool, resource, and prompt ordering and per-server error reporting are unchanged. ([#19919](https://github.com/mastra-ai/mastra/pull/19919))

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

- Added config-level `hooks` to Observational Memory so apps can track what OM's background model calls cost. Previously the `ObserveHooks` lifecycle callbacks only fired when calling `observe()` manually — the automatic pipeline (turn-driven observation and fire-and-forget async buffering) computed token `usage` and `providerMetadata` and dropped them. Hooks set on the OM config (including through `Memory`'s `observationalMemory` options) now fire for every observation and reflection cycle, with `threadId`/`resourceId`/`trigger` call context: ([#19058](https://github.com/mastra-ai/mastra/pull/19058))

  ```ts
  const memory = new Memory({
    storage,
    options: {
      observationalMemory: {
        hooks: {
          onObservationEnd: ({ usage, providerMetadata, error, threadId, trigger }) => {
            recordOmSpend({ usage, providerMetadata, threadId, trigger });
          },
        },
      },
    },
  });
  ```

  Failed async-buffered cycles never throw (they are fire-and-forget), so they report through the end hook's `error` field instead. Errors thrown by config-level hooks are caught and logged, never failing the cycle. Per-call `observe()` hooks keep their existing payloads and semantics.

- Made Observational Memory recall guidance scope-aware and added support for appending custom recall instructions. ([#20160](https://github.com/mastra-ai/mastra/pull/20160))

  With `retrieval.scope: "resource"`, the injected instructions now teach the agent how to route between `mode: "search"`, `mode: "threads"`, and `mode: "messages"` — including falling back to thread discovery when search results are irrelevant, since a short or recent thread may exist in raw message history before any observation of it was created. Search routing is only included when `retrieval.vector: true` is set; browsing-only retrieval guides the agent to `threads` and `messages` instead of a search mode that isn't configured. Resource-scoped recall guidance is now also injected before the first observation group exists, so the agent can browse other threads from the very first message of a conversation.

  Applications can now append recall-specific guidance after Mastra's built-in instructions without replacing them:

  ```ts
  observationalMemory: {
    retrieval: {
      vector: true,
      scope: 'resource',
      instructions: 'Prefer the current conversation when it already contains the answer.',
    },
  },
  ```

  Omitting `instructions` keeps existing behavior. Fixes https://github.com/mastra-ai/mastra/issues/19561

#### Patch Changes

- Fix schema-backed Observational Memory extractors throwing a "wrong resourceId" error during observer or reflector processing when the request context carries a resource ID. Agents configured with a schema-backed `Extractor` now run extraction passes without erroring. ([#19744](https://github.com/mastra-ai/mastra/pull/19744))

- Agents using observational memory or working-memory state signals no longer fail when invoked without a chat thread. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Ephemeral agent invocations (workflow agent steps, sub-agent tool calls) don't have — and don't need — a persistent chat thread, but the `observational-memory` and `working-memory-state` processors previously threw "requires Mastra memory with an active resourceId and threadId" the moment they ran without one, aborting the call.

  Both processors now handle the no-thread case gracefully at execution time: observational memory returns the message list unchanged when no thread context resolves, and working-memory state-signal computation skips when thread/resource identity is unavailable. A genuinely missing memory instance still errors as before, and threaded invocations behave exactly as they did. Processor discovery (`getInputProcessors` / `getOutputProcessors`) attaches both processors unconditionally, so discovery also works before the runtime memory context is populated.

### [@mastra/mongodb@1.16.0](https://github.com/mastra-ai/mastra/blob/@mastra/mongodb@1.16.0/stores/mongodb/CHANGELOG.md)
#### Minor Changes

- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.addItem({
    input: 'What is the weather?',
    unmockedToolPolicy: 'deny',
  });
  ```

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

#### Patch Changes

- Added a comment column to experiment results so review comments persist. The column is added automatically and non-destructively on startup for existing databases (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

- Dataset item scorer selections now persist across MongoDB writes and reads. Setting `scorerIds` to `null` clears an item override, while `[]` remains an explicit override with no scorers. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await dataset.addItem({
    input: 'Evaluate this response',
    scorerIds: [],
  });
  ```

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

- When MongoDB storage initialization fails because an existing non-unique index conflicts with Mastra's required unique index, the error now includes step-by-step migration commands instead of a generic failure message. ([#20486](https://github.com/mastra-ai/mastra/pull/20486))

  **Before:** `Failed to create default index on collection "mastra_threads". Set skipDefaultIndexes to manage indexes yourself.`

  **After:**

  ```text
  Index conflict on collection "mastra_threads": an existing non-unique index on { id: 1 }
  conflicts with Mastra's required unique index.

  To migrate:
    1. Check for duplicates:  db.mastra_threads.aggregate([{ $group: { _id: "$id", n: { $sum: 1 } } }, { $match: { n: { $gt: 1 } } }])
    2. Drop the old index:    db.mastra_threads.dropIndex("id_1")
    3. Recreate as unique:    db.mastra_threads.createIndex({ id: 1 }, { unique: true })

  Alternatively, set skipDefaultIndexes: true to manage indexes yourself.
  ```

### [@mastra/mssql@1.6.0](https://github.com/mastra-ai/mastra/blob/@mastra/mssql@1.6.0/stores/mssql/CHANGELOG.md)
#### Minor Changes

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

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

- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.addItem({
    input: 'What is the weather?',
    unmockedToolPolicy: 'deny',
  });
  ```

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

#### Patch Changes

- Added a comment column to experiment results so review comments persist. The column is added automatically and non-destructively on startup for existing databases (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

- Dataset item scorer selections now persist across MySQL writes and reads. Setting `scorerIds` to `null` clears an item override, while `[]` remains an explicit override with no scorers. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await dataset.addItem({
    input: 'Evaluate this response',
    scorerIds: [],
  });
  ```

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

- Fixed logs and metrics emitted outside of a span (for example server request logs) being stored without an environment or service name. They now inherit the Mastra-level environment (from the environment config option or NODE_ENV) and the configured service name, so filtering by environment in Studio Observability no longer hides these logs and metrics. Relates to https://github.com/mastra-ai/mastra/issues/19870 ([#19892](https://github.com/mastra-ai/mastra/pull/19892))

- Told bridges when a span ends without being exported, so they can free its state. ([#20463](https://github.com/mastra-ai/mastra/pull/20463))

  A span dropped by `excludeSpanTypes`, a `spanFilter`, or a span output processor emits no span-end event, so bridges such as `@mastra/otel-bridge` and `@mastra/datadog` never learned the span was finished and held the state they created for it until shutdown. Those spans now trigger a `releaseSpan` call on the bridge instead.

  Export behavior is unchanged: filtered spans are still not exported, and trace structure is untouched.

  Fixes [#20368](https://github.com/mastra-ai/mastra/issues/20368).

- Fixed RequestContext serialization to skip excluded spans and use its span-safe representation for exported traces. ([#20231](https://github.com/mastra-ai/mastra/pull/20231))

### [@mastra/otel-bridge@1.4.5](https://github.com/mastra-ai/mastra/blob/@mastra/otel-bridge@1.4.5/observability/otel-bridge/CHANGELOG.md)
#### Patch Changes

- Fixed the OpenTelemetry bridge ignoring a span's explicit start time, which would report wrong durations for spans created after the work they represent began (such as provider tool call spans). ([#20522](https://github.com/mastra-ai/mastra/pull/20522))

- Fixed a memory leak where spans removed by export filtering were never released. ([#20463](https://github.com/mastra-ai/mastra/pull/20463))

  The bridge creates an OpenTelemetry span when a Mastra span starts and frees it when the span ends. Span-end events are only delivered for spans that survive export filtering, so every span dropped by `excludeSpanTypes`, a `spanFilter`, or a span output processor left one entry behind for the life of the process. There was no bound or sweep on it, so long-running services grew steadily.

  The filtered spans are still not exported, so what you see in your tracing backend is unchanged.

  ```typescript
  new Observability({
    configs: {
      default: {
        serviceName: 'my-service',
        bridge: new OtelBridge(),
        // Previously leaked one entry per excluded span; now released on span end.
        excludeSpanTypes: [SpanType.MODEL_CHUNK, SpanType.MODEL_STEP],
      },
    },
  });
  ```

  Fixes [#20368](https://github.com/mastra-ai/mastra/issues/20368).

### [@mastra/otel-exporter@1.3.7](https://github.com/mastra-ai/mastra/blob/@mastra/otel-exporter@1.3.7/observability/otel-exporter/CHANGELOG.md)
#### Patch Changes

- Fixed observability exporters to read toolCallId from span attributes (with metadata fallback). Braintrust Thread view now shows tool results by pairing them via the real tool call ID. Sentry and OTel exporters also pick up the ID consistently across all tool span types. ([#19405](https://github.com/mastra-ai/mastra/pull/19405))

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

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.addItem({
    input: 'What is the weather?',
    unmockedToolPolicy: 'deny',
  });
  ```

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpanner`. The composite stores (`LibSQLStore`, `PostgresStore`, `MySQLStore`, `MSSQLStore`, `MongoDBStore`, `SpannerStore`) auto-wire the new domain, so callers do not need to construct it manually — `storage.getStore('workflowDefinitions')` now returns a live handle.

  The pg adapter reads `createdAt` / `updatedAt` from the auto-added `createdAtZ` / `updatedAtZ` `timestamptz` companion columns to avoid the naive-timestamp / local-TZ drift that a plain `TIMESTAMP` read exhibits under node-pg.

  `@mastra/clickhouse` and `@mastra/cloudflare` register the new `mastra_workflow_definitions` table in their table/type maps so shared table constants stay exhaustive (no workflow-definitions domain implementation yet).

#### Patch Changes

- Added a comment column to experiment results so review comments persist. The column is added automatically and non-destructively on startup for existing databases (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

- Dataset item scorer selections now persist across PostgreSQL writes and reads. Setting `scorerIds` to `null` clears an item override, while `[]` remains an explicit override with no scorers. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await dataset.addItem({
    input: 'Evaluate this response',
    scorerIds: [],
  });
  ```

- Improved PostgresStore startup: init now reads the schema catalog up front (3 read-only queries) instead of issuing hundreds of per-object existence checks and no-op DDL statements. On an already-migrated database this cuts init from ~350 serialized queries to 6, dropping init time on a 50ms connection from ~18.5s to ~0.5s. Fixed init failing for roles without CREATE privileges when the schema already exists, and removed a table lock that could block writers while init re-created triggers that were already in place. Fresh and partially-migrated databases are set up exactly as before. ([#20394](https://github.com/mastra-ai/mastra/pull/20394))

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

### [@mastra/platform-workspace@1.0.0](https://github.com/mastra-ai/mastra/blob/@mastra/platform-workspace@1.0.0/workspaces/platform-workspace/CHANGELOG.md)
#### Major Changes

- Removed support for using `MASTRA_PLATFORM_SECRET_KEY` to authenticate workspace providers. Use the platform-injected `MASTRA_PLATFORM_ACCESS_TOKEN` or pass `accessToken` explicitly instead. ([#20695](https://github.com/mastra-ai/mastra/pull/20695))

  **Before:** Set `MASTRA_PLATFORM_SECRET_KEY`.

  **After:** Use the platform-injected `MASTRA_PLATFORM_ACCESS_TOKEN`. For local development, set `MASTRA_PLATFORM_ACCESS_TOKEN` to an organization API token, or pass it explicitly:

  ```typescript
  import { PlatformSandbox } from '@mastra/platform-workspace';

  const sandbox = new PlatformSandbox({
    accessToken: 'sk_your-api-token',
    projectId: 'project_abc',
    environmentId: 'environment_abc',
  });
  ```

#### Minor Changes

- `PlatformSandbox.executeCommand` now retries a dropped connection once and continues using direct execution for later commands. Previously a single connection hiccup permanently downgraded the sandbox to a slower fallback route for the rest of its lifetime. ([#20482](https://github.com/mastra-ai/mastra/pull/20482))

  Execution failures now surface directly:

  - A destroyed sandbox throws the new `SandboxDestroyedError`. The cached sandbox is cleared, so the next call provisions a fresh one.
  - Two connection failures in a row against a live sandbox throw the new `SandboxExecTransportError`, which carries `sandboxId`, `command`, `attempts`, `opened`, `closeCode`, `closeReason`, and `wsEndpoint` for diagnostics.
  - Other platform errors previously masked by the fallback now bubble out as `PlatformApiError`.

  ```ts
  import { SandboxDestroyedError, SandboxExecTransportError } from '@mastra/platform-workspace';

  try {
    await sandbox.executeCommand('pytest');
  } catch (err) {
    if (err instanceof SandboxDestroyedError) {
      // Reprovision and retry.
    } else if (err instanceof SandboxExecTransportError) {
      // Connection failed twice; sandbox is still alive.
    }
  }
  ```

#### Patch Changes

- Fixed `PlatformSandbox.clone()` silently ignoring `checkpointName`. Clones created with `clone({ checkpointName })` now reuse a matching captured checkpoint on `start()` instead of always provisioning a fresh sandbox, so repeated boots of the same session start much faster. ([#20477](https://github.com/mastra-ai/mastra/pull/20477))

  ```ts
  const child = template.clone({ checkpointName: 'mastra-recovery-session-42' });
  await child.start(); // Reuses the captured checkpoint when one is available.
  ```

  An explicit `id` still takes precedence over `checkpointName` when both are passed.

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

- Added per-project trace list columns for duration, token usage, estimated cost, and custom metadata. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

- Added `ChatShell`, a chat page frame with a single scroll container. Bars sit above the scroller, the composer docks inside it as `sticky bottom-0`, and every region — transcript, notices, task list, composer — is centred by one `ChatShell.Column`. That removes the band of background that used to separate the transcript from the composer, and keeps absolutely positioned affordances such as jump-to-latest anchored to the shell instead of escaping to a full-width page wrapper and centring on the wrong axis. ([#20455](https://github.com/mastra-ai/mastra/pull/20455))

  ```tsx
  <ChatShell
    className="[--chat-column:44rem]"
    scroller={{ autoScroll: true, preserveScrollOnPrepend: true, onReachStart: loadOlderHistory }}
  >
    <ChatShell.Bar>
      <SessionHeader />
    </ChatShell.Bar>
    <ChatShell.Stage>
      <ChatShell.Viewport>
        <ChatShell.Content>
          <ChatShell.Column>{messages}</ChatShell.Column>
        </ChatShell.Content>
        <ChatShell.Dock>
          <ChatShell.ScrollButton />
          <ChatShell.Column>
            <Composer />
          </ChatShell.Column>
        </ChatShell.Dock>
      </ChatShell.Viewport>
    </ChatShell.Stage>
  </ChatShell>
  ```

  The dock keeps its place in flow, so its own height is the room the transcript scrolls behind and nothing has to measure it. A composer that grows as the reader types therefore leaves the transcript exactly where it was, instead of dragging it up a line at a time.

  Custom properties tune it: `--chat-column` for the column width, `--chat-surface` for the page colour, `--chat-gutter` for the room kept above and below the composer, `--chat-veil` for what the dock paints with (translucent by default, so a line passing behind the composer stays faintly readable), and `--chat-inset-end` for room an overlay panel claims on the end edge.

  **MessageScroller** `MessageScrollerProvider` gained `onReachStart`, called when the reader reaches the start of the transcript, and `preserveScrollOnPrepend`, which holds the reading position when older messages land above the current ones. `autoScroll` now also follows content that grows mid-stream, and leaves a reader who scrolled away alone instead of pulling them back to the end. `preserveScrollOnPrepend` moved off `MessageScrollerViewport`, where it set a data attribute and nothing else. `MessageScrollerItem` no longer throws outside a scroller, so the same row renderer can be reused on draft pages and previews. `MessageScrollerButton` now carries a minimum size and a soft two-layer shadow, so it reads as a floating control rather than a bare icon.

  **Migration**

  ```tsx
  // Before
  <MessageScrollerViewport preserveScrollOnPrepend />

  // After
  <MessageScrollerProvider preserveScrollOnPrepend>
    <MessageScrollerViewport />
  </MessageScrollerProvider>
  ```

- Added reusable command palette primitives for consistent application search layouts. ([#20453](https://github.com/mastra-ai/mastra/pull/20453))

      import { CommandPaletteDialog } from "@mastra/playground-ui/components/CommandPalette";

      <CommandPaletteDialog open={open} onOpenChange={setOpen}>...</CommandPaletteDialog>

- Added `size` variants to `Kbd` so keyboard hints can sit inside compact controls without hand-written overrides. Sizes are `default` (24px), `sm` (20px) and `xs` (16px), each with a fixed height so the scale stays even across fonts. ([#20453](https://github.com/mastra-ai/mastra/pull/20453))

      <Kbd size="sm">Esc</Kbd>
      <Kbd size="xs">⌘ K</Kbd>

- Preserved browser shortcuts by making the MainSidebar Command+B toggle opt-in. Consumers that want the previous shortcut can enable it explicitly: ([#20614](https://github.com/mastra-ai/mastra/pull/20614))

  ```tsx
  <MainSidebarProvider disableKeyboardShortcut={false}>{children}</MainSidebarProvider>
  ```

  Added selective hooks for consumers that only need sidebar state or mobile drawer state:

  ```tsx
  import { useMaybeSidebarState, useMobileDrawer } from '@mastra/playground-ui/components/MainSidebar';

  const sidebar = useMaybeSidebarState();
  const { openMobile } = useMobileDrawer();
  ```

- Added a pointer-aware ring around the chat composer. At rest it is a plain border; on hover or focus a soft arc lights the edge under the cursor, and while the agent is running the arc rotates on its own so the composer itself shows the run instead of a separate "working…" label. ([#20630](https://github.com/mastra-ai/mastra/pull/20630))

  ```tsx
  import { ComposerBox, ComposerRing } from '@mastra/playground-ui/components/Composer';

  <ComposerRing busy={isRunning}>
    <ComposerBox>{/* input and actions */}</ComposerBox>
  </ComposerRing>;
  ```

  Wrap `ComposerBox` with it and pass `busy` — the ring becomes the composer's edge, so the box no longer needs a border of its own.

#### Patch Changes

- Fixed three problems with the conversation rail in chat threads. ([#20615](https://github.com/mastra-ai/mastra/pull/20615))

  **The rail now marks the turn you are reading after older history loads.** Scrolling up to load earlier messages used to leave the oldest message highlighted as the current turn, so the marker jumped to the top of the thread instead of following the transcript.

  **Scrolling stays responsive in long threads.** The rail re-derived the order of every message on each scroll event. It now does that only when messages are added or removed.

  **The rail can be used in a browser-only app.** Importing it no longer drags the whole `@mastra/react` entry point into the bundle.

- Fixed the last column header in Sankey charts (for example SENTIMENT on the Trace Intelligence page) rendering right up against the chart's edge. It now keeps the same spacing as the first column's header. ([#20589](https://github.com/mastra-ai/mastra/pull/20589))

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

- `WorkflowStepFactory` understands the new declarative step entries. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  **New resolved kinds.** `agent-step` and `tool-step` with matching `AgentStep` / `ToolStep` renderer slots. Entries without a dedicated renderer still fall back to `UnknownStep`.

  **`map-step` resolves from the dedicated `type: 'mapping'` entry.** Previously a generic `type: 'step'` entry carrying `mapConfig`. `ResolvedWorkflowMapStep['flow']` is a union of both shapes — narrow on `flow.type` before reading the mapping code (`flow.mapConfig` for `'mapping'` entries, `flow.step.mapConfig` for legacy `'step'` entries).

  **`nested-workflow-step` also resolves from the first-class `type: 'workflow'` entry** emitted for `.then(subWorkflow)`, which carries `workflowId` and the nested `serializedStepFlow`.

  **Usage.** Pass a `ResolvedWorkflowStep` and the renderer slots you care about; unhandled kinds fall through to `UnknownStep`:

  ```tsx
  import { WorkflowStepFactory } from '@mastra/react';
  import type { ResolvedWorkflowStep } from '@mastra/react';

  function StepNode({ step }: { step: ResolvedWorkflowStep }) {
    return (
      <WorkflowStepFactory
        step={step}
        AgentStep={s => <AgentCard agentId={s.flow.agentId} result={s.result} />}
        ToolStep={s => <ToolCard toolId={s.flow.toolId} result={s.result} />}
        MapStep={s => <MapCard code={s.flow.type === 'mapping' ? s.flow.mapConfig : s.flow.step.mapConfig} />}
        UnknownStep={s => <GenericCard id={s.id} />}
      />
    );
  }
  ```

#### Patch Changes

- Fixed streamed assistant messages in `useChat`-style threads being keyed by a stale message id when observational memory rotates the response message id during a run. The accumulator now follows the rotated id carried by `step-start`, so streamed messages always match the ids they are persisted under and no longer disappear or duplicate after a refresh. Part of the fix for [#19810](https://github.com/mastra-ai/mastra/issues/19810) ([#20084](https://github.com/mastra-ai/mastra/pull/20084))

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

- Fixed nine storage adapters declaring a `@mastra/core` peer range that permitted core versions too old to load them. Each adapter imports `storageMessageMatchesMetadataFilter` from `@mastra/core/storage`, which core only exports from 1.53.0, but every one of them still advertised a floor below that — as low as `>=1.0.0-0`. Package managers accepted the incompatible pair without a warning and the install then failed at import time: ([#20591](https://github.com/mastra-ai/mastra/pull/20591))

  ```
  SyntaxError: The requested module '@mastra/core/storage' does not provide an export named 'storageMessageMatchesMetadataFilter'
  ```

  All nine now declare `>=1.53.0-0 <2.0.0-0`, so npm and pnpm surface a peer conflict at install time instead of letting the project break on first import.

  Fixes [#20586](https://github.com/mastra-ai/mastra/issues/20586).

### [@mastra/sentry@1.2.7](https://github.com/mastra-ai/mastra/blob/@mastra/sentry@1.2.7/observability/sentry/CHANGELOG.md)
#### Patch Changes

- Fixed observability exporters to read toolCallId from span attributes (with metadata fallback). Braintrust Thread view now shows tool results by pairing them via the real tool call ID. Sentry and OTel exporters also pick up the ID consistently across all tool span types. ([#19405](https://github.com/mastra-ai/mastra/pull/19405))

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

- Stored workflows can now be managed over HTTP: create or update a declarative workflow definition with `POST /stored/workflows`, list/fetch with `GET`, and remove with `DELETE` — with malformed graphs rejected at the API boundary with actionable errors instead of failing deep inside rehydration. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  ```ts
  await fetch(`${baseUrl}/stored/workflows`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id: 'greeting-workflow',
      inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
      outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
      graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
    }),
  });
  ```

  **`DELETE /stored/workflows/:storedWorkflowId` now unregisters the live workflow instance** in addition to removing the stored row. Previously the handler only called `store.delete(id)`, leaving the rehydrated `Workflow` on `Mastra` until the process restarted. The handler now calls `mastra.removeWorkflow(id)` after `store.delete`. Idempotent on missing ids.

  **`POST /stored/workflows` body schema is now a typed discriminated union.** The `graph` field was previously typed as `z.array(z.any())` and would only surface malformed entries deep inside `rehydrateWorkflow`. It is now a discriminated union over `type: 'step' | 'agent' | 'tool' | 'mapping' | 'parallel' | 'foreach' | 'sleep' | 'sleepUntil'`, matching the serialized graph shape `toStorableGraph` emits. Combined with the new `Mastra.addStoredWorkflow` pre-flight, invalid ids, mis-classified refs, and JSON Schemas that use converter-unsupported keywords (`oneOf` / `anyOf` / …) are rejected at the HTTP boundary with actionable errors before rehydration runs. `inputSchema` / `outputSchema` / `stateSchema` / `requestContextSchema` remain `z.any()` — they're JSON Schema Draft 2020-12 blobs, validated in `addStoredWorkflow` before the row is persisted.

  **`foreach.opts` is now optional on both sides of the wire.** Previously the Zod schema declared `opts` optional but the underlying `SerializedForeachEntry.opts` was required, forcing a `Parameters<Mastra['addStoredWorkflow']>[0]` cast in the handler that defeated compile-time drift detection. `SerializedForeachEntry.opts` is now optional in core, the Zod schema and the runtime type agree, and the handler cast is gone. Runtime unchanged (engine already read `entry.opts?.concurrency ?? 1`).

  **`conditional` and `loop` entries now round-trip through `POST /stored/workflows`.** The body schema's discriminated union has been extended with `type: 'conditional'` (`steps: SingleStepEntry[]`, `predicates: Predicate[]`) and `type: 'loop'` (`step: SingleStepEntry`, `loopType: 'dowhile' | 'dountil'`, `predicate: Predicate`), where `Predicate` is the same structural JSON shape now exported from `@mastra/core/workflows`. Legacy closure-based `serializedConditions` payloads are rejected at the HTTP boundary rather than silently reaching the rehydrator.

  **Nested workflow references now round-trip through `POST /stored/workflows`.** The body schema's `SingleStepEntry` union gains a `type: 'workflow'` variant (`id`, `workflowId`, optional `description`) that can appear at the top level or inside any composite entry. `serializedStepFlowEntrySchema` (returned by `GET /workflows/:id`) mirrors the same variant so clients see nested-workflow steps in code-defined workflows as well — a stored parent workflow can reference a previously stored child and run it end to end through the standard workflow endpoints.

  **`mastra` CLI:** regenerated API route metadata so the CLI's route table includes the new stored-workflow endpoints.

- Added dataset API support for storing per-item undeclared tool policies and reporting denied tool calls. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await fetch(`/api/datasets/${datasetId}/items`, {
    method: 'POST',
    body: JSON.stringify({
      input: 'What is the weather?',
      unmockedToolPolicy: 'deny',
    }),
  });

  const failureCode = experimentResult.toolMockReport?.failure?.code;
  ```

#### Patch Changes

- Added comment support to the experiment result API. The PATCH experiment result endpoint and the client updateDatasetExperimentResult method now accept and return a comment field, so review comments persist server-side instead of being lost on reload (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))

  ```ts
  const result = await client.updateDatasetExperimentResult({
    datasetId,
    experimentId,
    resultId,
    comment: 'Agent hallucinated an API that does not exist',
  });
  ```

- Added item-level scorer IDs to dataset item create, batch, update, read, history, and version endpoints. Updates distinguish omitted values, `null` inheritance, and explicit empty arrays. ([#20191](https://github.com/mastra-ai/mastra/pull/20191))

  ```typescript
  await dataset.updateItem({
    itemId: 'item-id',
    scorerIds: null,
  });
  ```

- Prevent background workflow recovery failures from terminating the server. ([#19639](https://github.com/mastra-ai/mastra/pull/19639))

### [@mastra/spanner@1.5.0](https://github.com/mastra-ai/mastra/blob/@mastra/spanner@1.5.0/stores/spanner/CHANGELOG.md)
#### Minor Changes

- Added batch trace ID filtering to observability metric queries. ([#20535](https://github.com/mastra-ai/mastra/pull/20535))

  ```ts
  const result = await observability.getMetricBreakdown({
    name: ['mastra_model_total_input_tokens'],
    aggregation: 'sum',
    groupBy: ['traceId'],
    filters: { traceIds: ['trace-1', 'trace-2'] },
  });
  ```

- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))

  ```typescript
  await dataset.addItem({
    input: 'What is the weather?',
    unmockedToolPolicy: 'deny',
  });
  ```

- Stored workflow definitions now persist across restarts on every major database backend. ([#20471](https://github.com/mastra-ai/mastra/pull/20471))

  Implement the `workflowDefinitions` storage domain for libsql, pg, mysql, mssql, mongodb, and spanner. Previously the stored-workflow persistence path (`POST /stored/workflows`, `Mastra.addStoredWorkflow`) only worked against `@mastra/core`'s in-memory store. Persistent adapters returned `undefined` from `storage.getStore('workflowDefinitions')` and threw when the HTTP handler tried to read/write a workflow.

  ```ts
  const workflowDefinitions = await storage.getStore('workflowDefinitions');
  if (!workflowDefinitions) {
    throw new Error('This storage adapter does not support the workflowDefinitions domain');
  }

  await workflowDefinitions.upsert({
    id: 'greeting-workflow',
    inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
    outputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
    graph: [{ type: 'agent', id: 'greet', agentId: 'greeter-agent' }],
  });

  const { definitions, total } = await workflowDefinitions.list({ status: 'active' });
  const definition = await workflowDefinitions.get('greeting-workflow');
  await workflowDefinitions.delete('greeting-workflow');
  ```

  Each adapter now ships a `WorkflowDefinitions*` domain that:

  - Creates the shared `mastra_workflow_definitions` table (or Mongo collection) from `WORKFLOW_DEFINITIONS_SCHEMA` during `init()`, plus a default index on `status`.
  - Implements `upsert` / `get` / `list` / `delete` matching `WorkflowDefinitionsStorage` semantics (`list` supports `status` and `authorId` filters and orders by `updatedAt` desc). Partial upserts preserve unspecified fields, including `authorId` updates and `createdAt` / `updatedAt` semantics.
  - Handles concurrent first-writes race-safely: if two callers upsert the same new id simultaneously, the losing insert detects the duplicate key, re-reads the row, and applies the partial-update path instead of failing.
  - Round-trips the JSON columns (`inputSchema`, `outputSchema`, `stateSchema`, `requestContextSchema`, `metadata`, `graph`) through each adapter's JSON handling, so declarative workflow graphs rehydrate identically no matter which backend they were stored in. Malformed persisted JSON surfaces as an actionable error naming the row and column instead of hydrating raw strings.

  Exported class names by adapter: `WorkflowDefinitionsLibSQL`, `WorkflowDefinitionsPG`, `WorkflowDefinitionsMySQL`, `WorkflowDefinitionsMSSQL`, `MongoDBWorkflowDefinitionsStore`, `WorkflowDefinitionsSpa