github mastra-ai/mastra @mastra/core@1.56.0
August 5, 2026

5 hours ago

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

Minor Changes

  • Added item-level scorer selection for dataset experiments. (#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.

    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:

    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:

    await dataset.startExperiment({
      targetType: 'agent',
      targetId: 'support-agent',
      scorers: ['latency', 'accuracy'],
    });
  • Declarative, persistable workflow graphs. (#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.

    // 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 WorkflowDefinitions) 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 types):

    • 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:

    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)

    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)

    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.

    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)

    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)

    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)

    await dataset.startExperiment({
      targetType: 'agent',
      targetId: 'weather-agent',
      unmockedToolPolicy: 'deny',
    });
  • Added an awaited semantic event observer for experiments. (#20644)

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

    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)

    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.

    // 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 #19857 (#19865)

    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)

  • Update provider registry and model documentation with latest models and providers (7f4e26d)

  • Fixed dropped non-text stream parts when emitOnNonText: false. (#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)

  • 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)

    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:

    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)

  • Added an optional releaseSpan method to the observability bridge interface. (#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.

    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.

  • Use the configured ID generator for message rows persisted through agent signal APIs. (#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)

  • 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)

  • Fixed sub-agent delegation prompts being ordered before forwarded supervisor context. (#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)

  • 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)

  • 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)

  • 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)

  • Fixed custom data chunks emitted by processors so they are saved with thread messages unless marked transient. (#19375)

  • Fixed agent thread subscriptions so every instance in a multi-instance deployment sees the same conversation: (#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 #20470 (#20487)

  • Fixed durable delegated tool approvals so persisted approvals resume the suspended sub-agent, including after a server restart. (#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)

  • 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)

  • Fixed RegexFilterProcessor leaving part of a matched value in the output when two rules match overlapping text. (#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:

    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)

  • 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)

  • 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)

    // 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)

@mastra/ai-sdk@1.7.1

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 (#20084)

  • Fixed persisted signal finish chunks so AI SDK consumers can convert both newly produced and previously retained stream events. (#19806)

@mastra/braintrust@1.3.2

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)

@mastra/clickhouse@1.14.0

Minor Changes

  • Added batch trace ID filtering to observability metric queries. (#20535)

    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)

    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.

    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

Minor Changes

  • Added batch trace ID filtering to observability metric queries. (#20535)

    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)

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

    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 (#19857). (#19865)

    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)

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

@mastra/cloudflare@1.6.1

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)

    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.

  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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

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)

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

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

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

@mastra/convex@1.5.2

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)

    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.

@mastra/datadog@1.3.7

Patch Changes

  • Released bridge state for spans that end without being exported. (#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.

@mastra/deployer@1.56.0

Patch Changes

  • Fixed workspace package changes not being picked up during mastra dev hot reload (#20262)

  • Prevent background workflow recovery failures from terminating the server. (#19639)

  • Fixed builds for transitive workspace dependencies that only expose subpath exports. (#19808)

@mastra/deployer-vercel@1.2.13

Patch Changes

  • Fixed custom API routes being unreachable when deploying to Vercel with studio: true. (#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.

    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

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)

    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.

@mastra/duckdb@1.6.0

Minor Changes

  • Added batch trace ID filtering to observability metric queries. (#20535)

    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

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)

    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.

@mastra/evals@1.7.0

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)

    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.

    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

Patch Changes

  • Fixed a security issue where DELETE requests could bypass the server's body size limit. (#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

Minor Changes

  • Added a lightweight pending changes viewer with per-file line counts for Factory session workspaces and improved chat composer readability. (#20418)

Patch Changes

  • Self-hosted GitHub deployments now detect merged pull requests. (#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

    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)

  • Factory work item transitions now require explicit approval before execution. (#20622)

  • Fixed Factory rule dispatches so concurrent skill wakeups stay bounded until their agent runs finish or terminal observation times out. (#20623)

  • Improved Factory pull-request reviews by requiring comparison with analogous codebase patterns. (#20524)

  • Fixed the Factory getting stuck after a GitHub App is uninstalled and reinstalled. (#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)

  • Fixed factory sessions inheriting the personal agent instructions of the machine hosting them. (#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)

@mastra/fastify@1.4.13

Patch Changes

  • Fixed a security issue where DELETE requests could bypass the server's body size limit. (#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

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)

@mastra/hono@1.5.13

Patch Changes

  • Fixed server crashes when clients disconnect during streaming. (#20312)

  • Fixed a security issue where DELETE requests could bypass the server's body size limit. (#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

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)

    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)

@mastra/lance@1.2.2

Patch Changes

  • Fixed message, thread, and resource updates to avoid recreating incomplete records when an update target is missing. (#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)

    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.

@mastra/libsql@1.19.0

Minor Changes

  • Added persistence for dataset item undeclared tool policies. (#19643)

    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 (#19857). (#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)

    await dataset.addItem({
      input: 'Evaluate this response',
      scorerIds: [],
    });
  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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

Patch Changes

  • Fixed a prompt-injection risk by rejecting system-role messages embedded in prompt or messages for AI SDK v5 calls. (#20558)

@mastra/mcp@1.15.1

Patch Changes

  • Fixed concurrent MCP tool calls failing while the client reconnects after a dropped connection. (#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)

@mastra/memory@1.25.0

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)

    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)

    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:

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

    Omitting instructions keeps existing behavior. Fixes #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)

  • Agents using observational memory or working-memory state signals no longer fail when invoked without a chat thread. (#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

Minor Changes

  • Added persistence for dataset item undeclared tool policies. (#19643)

    await dataset.addItem({
      input: 'What is the weather?',
      unmockedToolPolicy: 'deny',
    });
  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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 (#19857). (#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)

    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)

    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.

  • 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)

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

    After:

    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

Minor Changes

  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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

Minor Changes

  • Added persistence for dataset item undeclared tool policies. (#19643)

    await dataset.addItem({
      input: 'What is the weather?',
      unmockedToolPolicy: 'deny',
    });
  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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 (#19857). (#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)

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

@mastra/observability@1.16.4

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 #19870 (#19892)

  • Told bridges when a span ends without being exported, so they can free its state. (#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.

  • Fixed RequestContext serialization to skip excluded spans and use its span-safe representation for exported traces. (#20231)

@mastra/otel-bridge@1.4.5

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)

  • Fixed a memory leak where spans removed by export filtering were never released. (#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.

    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.

@mastra/otel-exporter@1.3.7

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)

@mastra/pg@1.19.0

Minor Changes

  • Added batch trace ID filtering to observability metric queries. (#20535)

    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)

    await dataset.addItem({
      input: 'What is the weather?',
      unmockedToolPolicy: 'deny',
    });
  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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 (#19857). (#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)

    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)

  • 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)

    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.

@mastra/platform-workspace@1.0.0

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)

    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:

    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)

    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.
    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)

    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

Minor Changes

  • Added per-project trace list columns for duration, token usage, estimated cost, and custom metadata. (#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)

    <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

    // Before
    <MessageScrollerViewport preserveScrollOnPrepend />
    
    // After
    <MessageScrollerProvider preserveScrollOnPrepend>
      <MessageScrollerViewport />
    </MessageScrollerProvider>
  • Added reusable command palette primitives for consistent application search layouts. (#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)

    <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)

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

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

    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)

    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)

    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)

@mastra/react@1.4.0

Minor Changes

  • WorkflowStepFactory understands the new declarative step entries. (#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:

    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 (#20084)

@mastra/redis@1.3.1

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)

    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.

@mastra/sentry@1.2.7

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)

@mastra/server@1.56.0

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)

    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)

    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 (#19857). (#19865)

    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)

    await dataset.updateItem({
      itemId: 'item-id',
      scorerIds: null,
    });
  • Prevent background workflow recovery failures from terminating the server. (#19639)

@mastra/spanner@1.5.0

Minor Changes

  • Added batch trace ID filtering to observability metric queries. (#20535)

    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)

    await dataset.addItem({
      input: 'What is the weather?',
      unmockedToolPolicy: 'deny',
    });
  • Stored workflow definitions now persist across restarts on every major database backend. (#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.

    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

Don't miss a new mastra release

NewReleases is sending notifications on new releases.