@mastra/core
Version:
1,310 lines • 50.5 kB
JavaScript
import { standardSchemaToJSONSchema, toStandardSchema } from "./schema/index.js";
import { $t as createWorkflow, Zt as cloneWorkflow, bn as createStepFromTool, fn as mapVariable, mn as derivePredicateLabel, pn as predicateToCondition, yn as createStepFromAgent } from "./agent-Dj30gJa3.js";
import { l as collectTemplateStepIds, u as validateTemplate, v as getSingleStepEntryId } from "./workflow-event-processor-BbED1LMn.js";
import { z } from "zod";
//#region src/workflows/stored/json-schema-to-zod.ts
/**
* Minimal JSON-Schema ↔ Zod bridge for stored workflows: a converter for the
* static subset Zod round-trips through `standardSchemaToJSONSchema`, plus a
* non-throwing validator for the write path.
*/
/**
* Inline converter sufficient for the static subset Zod typically emits when
* round-tripped through `standardSchemaToJSONSchema`. Handles:
*
* - `object` with `properties` + `required`
* - `string` / `number` / `integer` / `boolean` / `null`
* - `array` with `items`
* - `enum`
* - `description` (propagated via `.describe`)
*
* For more exotic schemas (unions, intersections, recursive refs) swap in
* `json-schema-to-zod` from npm. Kept inline to avoid pulling a dependency
* for the MVP demo.
*/
function jsonSchemaToZod(schema, opts) {
return walk(schema, opts ?? {});
}
const UNSUPPORTED_SCHEMA_KEYS = [
"oneOf",
"anyOf",
"allOf",
"not",
"$ref",
"patternProperties",
"discriminator"
];
/** Values `z.literal()` can represent — the only const/enum members that survive conversion losslessly. */
function isLiteralValue(v) {
return v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
}
/** Throw or warn-and-fallback per `onUnsupportedSchema`, matching the unsupported-keyword behavior. */
function unsupported(message, opts) {
if (opts.onUnsupportedSchema === "warn") {
opts.onUnsupported?.(message);
return z.any();
}
throw new Error(message);
}
function walk(schema, opts) {
if (!schema || typeof schema !== "object") return z.any();
for (const key of UNSUPPORTED_SCHEMA_KEYS) if (key in schema) return unsupported(`Stored workflow schema uses unsupported JSON Schema keyword "${key}". This converter only supports the static subset that Zod round-trips through standardSchemaToJSONSchema (object, array, string, number, integer, boolean, null, enum, const). Simplify the schema or extend jsonSchemaToZod to cover this keyword.`, opts);
let out;
if ("const" in schema) {
if (!isLiteralValue(schema.const)) return unsupported(`Stored workflow schema uses a non-primitive "const" value (${JSON.stringify(schema.const)}). Only string, number, boolean, and null literals are supported.`, opts);
out = z.literal(schema.const);
} else if (Array.isArray(schema.enum) && schema.enum.length > 0) {
const values = schema.enum;
if (!values.every(isLiteralValue)) return unsupported("Stored workflow schema uses an \"enum\" with non-primitive members. Only string, number, boolean, and null enum members are supported.", opts);
if (values.every((v) => typeof v === "string")) out = z.enum(values);
else {
const literals = values.map((v) => z.literal(v));
out = literals.length === 1 ? literals[0] : z.union(literals);
}
} else if (Array.isArray(schema.type)) {
const options = schema.type.map((t) => walk({
...schema,
type: t
}, opts));
if (options.length === 1) out = options[0];
else out = z.union(options);
} else switch (schema.type) {
case "object": {
const shape = {};
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
for (const [key, child] of Object.entries(schema.properties ?? {})) {
const childSchema = walk(child, opts);
shape[key] = required.has(key) ? childSchema : childSchema.optional();
}
const obj = z.object(shape);
out = schema.additionalProperties === true ? obj.passthrough() : obj;
break;
}
case "array":
if (Array.isArray(schema.items)) return unsupported("Stored workflow schema uses tuple-form \"items\" (an array of positional schemas). Only a single item schema is supported; use \"items\": { ... } instead.", opts);
out = z.array(walk(schema.items ?? {}, opts));
break;
case "string":
out = z.string();
break;
case "number":
out = z.number();
break;
case "integer":
out = z.number().int();
break;
case "boolean":
out = z.boolean();
break;
case "null":
out = z.null();
break;
case void 0:
out = z.any();
break;
default: return unsupported(`Stored workflow schema uses unsupported JSON Schema type "${String(schema.type)}". This converter only supports object, array, string, number, integer, boolean, null, and enum.`, opts);
}
if (typeof schema.description === "string" && schema.description.length > 0) out = out.describe(schema.description);
return out;
}
/**
* Non-throwing companion to `jsonSchemaToZod`. Walks a JSON Schema and reports
* every unsupported-keyword usage without converting. Use this at write time
* (e.g. inside `Mastra.addStoredWorkflow`) to surface a warning before the
* schema is persisted — the row will still fail to rehydrate on the next boot
* (`jsonSchemaToZod` throws), so this is a heads-up, not a guarantee.
*
* Callers decide whether to warn, reject, or ignore. This function never
* throws for any input shape.
*/
function validateStorableJsonSchema(schema) {
if (!schema || typeof schema !== "object") return { ok: true };
const unsupported = [];
const visit = (node, path) => {
if (!node || typeof node !== "object") return;
const n = node;
for (const key of UNSUPPORTED_SCHEMA_KEYS) if (key in n) unsupported.push(`${path || "#"}: ${key}`);
if (n.properties && typeof n.properties === "object") for (const [prop, child] of Object.entries(n.properties)) visit(child, `${path}/properties/${prop}`);
if (n.items) if (Array.isArray(n.items)) n.items.forEach((child, i) => visit(child, `${path}/items/${i}`));
else visit(n.items, `${path}/items`);
if (n.additionalProperties && typeof n.additionalProperties === "object") visit(n.additionalProperties, `${path}/additionalProperties`);
};
visit(schema, "");
return unsupported.length === 0 ? { ok: true } : {
ok: false,
unsupported
};
}
//#endregion
//#region src/workflows/stored/validate/schema-utils.ts
/**
* Pure JSON-Schema helpers shared by schema-flow analysis and mapping-config
* analysis. Everything here is best-effort and three-valued: a check only
* reports `incompatible` when it can prove a mismatch, so absent or partial
* schemas degrade to `unknown` instead of producing false positives.
*/
/**
* Best-effort conversion of a live (Zod / standard) schema to JSON Schema for
* registry-index building. Unconvertible or absent schemas yield `undefined`
* ("unknown"), which schema-flow treats as never-incompatible.
*/
function toJsonSchemaOrUndefined(schema) {
if (schema === void 0 || schema === null) return void 0;
try {
return standardSchemaToJSONSchema(toStandardSchema(schema));
} catch {
return;
}
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** True when both types are numeric, i.e. some mix of `integer` and `number`. */
function isNumeric(sourceType, destinationType) {
const numeric = /* @__PURE__ */ new Set(["integer", "number"]);
return numeric.has(sourceType) && numeric.has(destinationType);
}
/**
* Structural compatibility of `source` output feeding a `destination` input.
* Recurses through array items and object properties; a destination `required`
* key missing from the source is a proven incompatibility.
*/
function schemaCompatibility(source, destination) {
if (!isRecord(source) || !isRecord(destination)) return "unknown";
const sourceType = typeof source.type === "string" ? source.type : void 0;
const destinationType = typeof destination.type === "string" ? destination.type : void 0;
if (!sourceType || !destinationType) return "unknown";
if (sourceType !== destinationType && !isNumeric(sourceType, destinationType)) return "incompatible";
if (destinationType === "array") return schemaCompatibility(source.items, destination.items);
if (destinationType !== "object") return "compatible";
const sourceProperties = isRecord(source.properties) ? source.properties : {};
const destinationProperties = isRecord(destination.properties) ? destination.properties : {};
const required = Array.isArray(destination.required) ? destination.required.filter((key) => typeof key === "string") : [];
for (const key of required) if (!(key in sourceProperties)) return "incompatible";
for (const [key, destinationProperty] of Object.entries(destinationProperties)) {
if (!(key in sourceProperties)) continue;
if (schemaCompatibility(sourceProperties[key], destinationProperty) === "incompatible") return "incompatible";
}
return "compatible";
}
/** Follows a dotted mapping path through object `properties`; `''`/`'.'` is the root. */
function schemaAtPath(schema, path) {
if (!schema || path === "" || path === ".") return schema;
let current = schema;
for (const segment of path.split(".")) {
if (!isRecord(current) || !isRecord(current.properties) || !isRecord(current.properties[segment])) return void 0;
current = current.properties[segment];
}
return current;
}
/** Plain dotted segments only — no `$.`, brackets, or empty segments. */
function isCanonicalMappingPath(path) {
return path === "" || path === "." || /^[^.[$\]]+(?:\.[^.[$\]]+)*$/.test(path);
}
/** Infers a JSON Schema for a literal `{ value }` mapping source. */
function schemaForValue(value) {
if (value === null) return { type: "null" };
if (Array.isArray(value)) return { type: "array" };
switch (typeof value) {
case "string":
case "boolean": return { type: typeof value };
case "number": return { type: Number.isInteger(value) ? "integer" : "number" };
case "object": return { type: "object" };
default: return {};
}
}
//#endregion
//#region src/workflows/stored/mapping-config.ts
/**
* The single home for stored `mapConfig` handling.
*
* A mapping entry's config crosses the storage boundary as a JSON string.
* - {@link parseMapConfig} is the one parser (rehydration + validation both
* use it; rehydration via the throwing form).
* - {@link analyzeMapConfig} is the one validator: it walks each descriptor,
* collects issues, and infers the mapping's output schema in the same pass
* (the two are inseparable — a descriptor's validity determines its
* contribution to the output shape).
*
* Template syntax checking delegates to `mapping-template.ts`'s
* `validateTemplate` — the same parser the runtime uses — plus a scope check
* over the placeholders' step ids.
*/
/** Parses a stored mapConfig JSON string; throws with the step id on malformed JSON. */
function parseMapConfig(raw, stepId) {
try {
return JSON.parse(raw);
} catch (e) {
throw new Error(`Stored mapping step "${stepId}" has invalid JSON mapConfig: ${e.message}`);
}
}
/** A recognizable Handlebars/Mustache placeholder: `{{ name }}`, `{{a.b}}`, … */
const HANDLEBARS_PLACEHOLDER = /\{\{\s*[\w$][\w.$-]*\s*\}\}/;
/**
* Validates a mapping entry's raw `mapConfig` string and infers the step's
* output schema. Every key must define exactly one source
* (`value` | `template` | `requestContextPath` | `initData`/`step` + `path`);
* step references must point at preceding workflow-local steps.
*/
function analyzeMapConfig(rawConfig, opts) {
const issues = [];
const { path, availableOutputs } = opts;
let config;
try {
config = JSON.parse(rawConfig);
} catch {
config = void 0;
}
if (!isRecord(config)) {
issues.push({
code: "invalid-map-config",
path: `${path}.mapConfig`,
message: "Mapping config must be a JSON object."
});
return {
issues,
outputSchema: void 0
};
}
const properties = {};
for (const [key, descriptor] of Object.entries(config)) {
const descriptorPath = `${path}.mapConfig.${key}`;
if (!isRecord(descriptor)) {
issues.push({
code: "invalid-map-config",
path: descriptorPath,
message: "Mapping descriptor must be an object."
});
continue;
}
if ([
"value" in descriptor,
typeof descriptor.template === "string",
typeof descriptor.requestContextPath === "string",
"path" in descriptor
].filter(Boolean).length !== 1) {
issues.push({
code: "invalid-map-config",
path: descriptorPath,
message: "Mapping descriptor must define exactly one source."
});
continue;
}
if ("value" in descriptor) {
properties[key] = schemaForValue(descriptor.value);
continue;
}
if (typeof descriptor.template === "string") {
let syntaxError;
try {
validateTemplate(descriptor.template);
} catch (err) {
syntaxError = err.message;
}
if (syntaxError === void 0 && HANDLEBARS_PLACEHOLDER.test(descriptor.template)) syntaxError = `Templates use \${...} placeholders (e.g. "\${initData.name}"), not {{...}}. "${descriptor.template}" would be emitted literally.`;
const unknownStep = syntaxError === void 0 ? collectTemplateStepIds(descriptor.template).find((stepId) => !availableOutputs.has(stepId)) : void 0;
if (syntaxError !== void 0 || unknownStep !== void 0) issues.push({
code: "invalid-map-reference",
path: `${descriptorPath}.template`,
message: syntaxError ?? "Template references must use an available workflow-local source."
});
properties[key] = { type: "string" };
continue;
}
if (typeof descriptor.requestContextPath === "string") {
if (!isCanonicalMappingPath(descriptor.requestContextPath) || descriptor.requestContextPath === "") issues.push({
code: "invalid-map-config",
path: `${descriptorPath}.requestContextPath`,
message: "Mapping paths must use plain dotted segments."
});
properties[key] = schemaAtPath(opts.requestContextSchema, descriptor.requestContextPath) ?? {};
continue;
}
if (typeof descriptor.path !== "string" || !isCanonicalMappingPath(descriptor.path)) {
issues.push({
code: "invalid-map-config",
path: `${descriptorPath}.path`,
message: "Mapping paths must use plain dotted segments."
});
continue;
}
const hasInitData = descriptor.initData === true;
const stepIds = typeof descriptor.step === "string" ? [descriptor.step] : Array.isArray(descriptor.step) ? descriptor.step : [];
if (hasInitData === stepIds.length > 0 || stepIds.some((stepId) => typeof stepId !== "string")) {
issues.push({
code: "invalid-map-config",
path: descriptorPath,
message: "Path mappings must reference exactly one of initData or step."
});
continue;
}
let sourceSchema;
if (hasInitData) sourceSchema = opts.inputSchema;
else {
const missing = stepIds.find((stepId) => !availableOutputs.has(stepId));
if (missing) {
issues.push({
code: "invalid-map-reference",
path: `${descriptorPath}.step`,
message: `Mapping source "${missing}" must be a preceding workflow-local step.`
});
continue;
}
sourceSchema = stepIds.map((stepId) => availableOutputs.get(stepId)).find(Boolean);
}
const selectedSchema = schemaAtPath(sourceSchema, descriptor.path);
if (sourceSchema && !selectedSchema) issues.push({
code: "invalid-map-config",
path: `${descriptorPath}.path`,
message: `Path "${descriptor.path}" does not exist in the source schema.`
});
properties[key] = selectedSchema ?? {};
}
return {
issues,
outputSchema: {
type: "object",
properties,
required: Object.keys(config)
}
};
}
//#endregion
//#region src/workflows/stored/rehydrate.ts
async function rehydrateWorkflow(def, mastra, opts) {
const inputSchema = jsonSchemaToZod(def.inputSchema, opts);
const outputSchema = jsonSchemaToZod(def.outputSchema, opts);
const stateSchema = def.stateSchema ? jsonSchemaToZod(def.stateSchema, opts) : void 0;
const requestContextSchema = def.requestContextSchema ? jsonSchemaToZod(def.requestContextSchema, opts) : void 0;
const wf = createWorkflow({
id: def.id,
description: def.description,
metadata: def.metadata,
inputSchema,
outputSchema,
stateSchema,
requestContextSchema
});
for (const entry of def.graph) applyGraphEntry(wf, entry, mastra, opts);
const built = wf.commit();
built.origin = "stored";
return { workflow: built };
}
function applyGraphEntry(wf, entry, mastra, schemaOpts) {
switch (entry.type) {
case "agent":
case "tool":
wf.__pushStepFlowEntry(rehydrateSingleEntry(entry, mastra, schemaOpts), entry);
return;
case "mapping": {
const live = rehydrateMapConfig(parseMapConfig(entry.mapConfig, entry.id), mastra);
wf.map(live, { id: entry.id });
return;
}
case "sleep": {
if (typeof entry.duration !== "number") throw new Error(`Stored sleep "${entry.id}" missing literal duration.`);
const live = {
type: "sleep",
id: entry.id,
duration: entry.duration
};
wf.__pushStepFlowEntry(live, live);
return;
}
case "sleepUntil": {
if (!(entry.date instanceof Date) && typeof entry.date !== "string") throw new Error(`Stored sleepUntil "${entry.id}" missing literal date.`);
const date = entry.date instanceof Date ? entry.date : new Date(entry.date);
if (Number.isNaN(date.getTime())) throw new Error(`Stored sleepUntil "${entry.id}" has an unparseable date: ${String(entry.date)}`);
const live = {
type: "sleepUntil",
id: entry.id,
date
};
wf.__pushStepFlowEntry(live, {
type: "sleepUntil",
id: entry.id,
date
});
return;
}
case "parallel": {
const live = {
type: "parallel",
steps: entry.steps.map((s) => rehydrateSingleEntry(s, mastra, schemaOpts))
};
wf.__pushStepFlowEntry(live, entry);
return;
}
case "foreach": {
if (entry.step.type === "mapping") throw new Error(`Foreach step cannot iterate a mapping: mappings project data, they don't execute per item. Use an agent, tool, or plain step as the foreach body.`);
const live = {
type: "foreach",
step: rehydrateSingleEntry(entry.step, mastra, schemaOpts),
opts: { concurrency: entry.opts?.concurrency ?? 1 }
};
wf.__pushStepFlowEntry(live, entry);
return;
}
case "step": {
const live = rehydrateSingleEntry(entry, mastra, schemaOpts);
wf.__pushStepFlowEntry(live, entry);
return;
}
case "workflow": {
const nested = assertWorkflowExists(mastra, entry.workflowId);
wf.then(entry.id && entry.id !== nested.id ? cloneWorkflow(nested, { id: entry.id }) : nested);
return;
}
case "conditional": {
const predicates = entry.predicates;
if (!predicates || predicates.length !== entry.steps.length || predicates.some((p) => !p)) throw new Error(`Cannot rehydrate conditional step: missing or mismatched predicates. Only declarative predicate branches round-trip.`);
const steps = entry.steps.map((s) => rehydrateSingleEntry(s, mastra, schemaOpts));
const serializedConditions = entry.serializedConditions ?? steps.map((s, i) => ({
id: `${getSingleStepEntryId(s)}-condition`,
fn: derivePredicateLabel(predicates[i])
}));
const live = {
type: "conditional",
steps,
conditions: predicates.map((p) => predicateToCondition(p)),
serializedConditions,
predicates
};
wf.__pushStepFlowEntry(live, {
...entry,
serializedConditions
});
return;
}
case "loop": {
const { predicate, loopType } = entry;
if (!predicate || loopType !== "dowhile" && loopType !== "dountil") throw new Error(`Cannot rehydrate loop step: missing declarative predicate or loopType. Only declarative predicate loops round-trip.`);
const step = rehydrateSingleEntry(entry.step, mastra, schemaOpts);
const serializedCondition = entry.serializedCondition ?? {
id: `${getSingleStepEntryId(step)}-condition`,
fn: derivePredicateLabel(predicate)
};
const live = {
type: "loop",
step,
condition: predicateToCondition(predicate),
loopType,
serializedCondition,
predicate
};
wf.__pushStepFlowEntry(live, {
...entry,
serializedCondition
});
return;
}
default: throw new Error(`Unknown stored step type: ${JSON.stringify(entry)}`);
}
}
/**
* Reconstruct the options bag `.agent()` accepts from a serialized entry.
* Restores `structuredOutput.schema` from `outputSchema` (JSON Schema → Zod)
* and merges in `retries` / `metadata`. Returns `undefined` when nothing to
* restore so `.agent(agentId)` stays a clean call.
*/
function rebuildAgentOptions(entry, schemaOpts) {
const opts = {};
if (entry.outputSchema) opts.structuredOutput = { schema: jsonSchemaToZod(entry.outputSchema, schemaOpts) };
if (entry.options?.retries !== void 0) opts.retries = entry.options.retries;
if (entry.options?.metadata !== void 0) opts.metadata = entry.options.metadata;
return Object.keys(opts).length > 0 ? opts : void 0;
}
function rebuildToolOptions(entry) {
const opts = {};
if (entry.options?.retries !== void 0) opts.retries = entry.options.retries;
if (entry.options?.metadata !== void 0) opts.metadata = entry.options.metadata;
return Object.keys(opts).length > 0 ? opts : void 0;
}
/**
* Build the live `SingleStepEntry` for a stored entry. Declarative agent/tool
* entries stay declarative — both engines interpret them per-kind at
* execution time (`runAgentEntry` / `runToolEntry`) — so no fake `Step`
* wrapper is needed and the stored `id` / `outputSchema` / `retries` /
* `metadata` round-trip losslessly in every position (top-level, parallel,
* branch, foreach and loop bodies).
*
* `step` descriptors resolve agent-then-tool by id against the live Mastra
* instance; `workflow` entries resolve the registered instance. Both become
* plain `{ type: 'step' }` entries, same as the fluent builder emits.
*/
function rehydrateSingleEntry(entry, mastra, schemaOpts) {
switch (entry.type) {
case "agent": {
const agent = tryGetAgentById(mastra, entry.agentId);
if (!agent) throw new Error(`Stored workflow references agent "${entry.agentId}" which is not registered on this Mastra instance.`);
return {
type: "agent",
id: entry.id,
agentId: entry.agentId,
agent,
options: rebuildAgentOptions(entry, schemaOpts)
};
}
case "tool": {
const tool = mastra.getTool?.(entry.toolId);
if (!tool) throw new Error(`Stored workflow references tool "${entry.toolId}" which is not registered on this Mastra instance.`);
return {
type: "tool",
id: entry.id,
toolId: entry.toolId,
tool,
options: rebuildToolOptions(entry)
};
}
case "step": {
const { id } = entry.step;
const agent = tryGetAgentById(mastra, id);
if (agent) return {
type: "step",
step: createStepFromAgent(agent)
};
const tool = tryGetToolById(mastra, id);
if (tool) return {
type: "step",
step: createStepFromTool(tool)
};
throw new Error(`Stored workflow references step "${id}" which is not registered as an agent or tool on this Mastra instance.`);
}
case "workflow": {
const nested = assertWorkflowExists(mastra, entry.workflowId);
return {
type: "step",
step: entry.id && entry.id !== nested.id ? cloneWorkflow(nested, { id: entry.id }) : nested
};
}
case "mapping": throw new Error(`mapping entries cannot appear inside .parallel(), .branch(), or .foreach(); they must be top-level.`);
}
}
/**
* Rebuild the object shape that `.map()` accepts. Step sources remain workflow-local
* step IDs because mapping execution resolves them from the run's step results.
*/
function rehydrateMapConfig(cfg, mastra) {
const out = {};
for (const [key, source] of Object.entries(cfg)) {
if (!source || typeof source !== "object") {
out[key] = source;
continue;
}
if ("template" in source) out[key] = { template: source.template };
else if ("value" in source) out[key] = { value: source.value };
else if ("requestContextPath" in source) out[key] = { requestContextPath: source.requestContextPath };
else if ("initData" in source && typeof source.initData === "string") {
const wf = mastra.getWorkflow?.(source.initData);
if (!wf) throw new Error(`Mapping references unknown workflow init-data "${source.initData}".`);
out[key] = mapVariable({
initData: wf,
path: source.path
});
} else if ("step" in source) out[key] = mapVariable({
step: source.step,
path: source.path
});
else out[key] = source;
}
return out;
}
/**
* Mastra.getAgentById throws when the id isn't registered; every by-id
* resolution path in this file wants a nullable "does it exist?" answer so it
* can fall through to a tool lookup or a targeted error. Swallow the not-found
* throw and return undefined.
*/
function tryGetAgentById(mastra, id) {
if (!id || typeof mastra.getAgentById !== "function") return void 0;
try {
return mastra.getAgentById(id);
} catch {
return;
}
}
/** Same nullable-lookup contract as `tryGetAgentById`, for tools — `Mastra.getTool` throws on a missing id. */
function tryGetToolById(mastra, id) {
if (!id || typeof mastra.getTool !== "function") return void 0;
try {
return mastra.getTool(id);
} catch {
return;
}
}
/**
* Workflow references resolve like agent references: intrinsic workflow id
* first (`getWorkflowById` scans registered workflows by their own `id`),
* falling back to the registration key. Stored definitions reference the
* intrinsic id — the identity discovery advertises — which may differ from
* the key the workflow was registered under (`workflows: { greetingWorkflow }`
* vs `id: 'greeting-workflow'`).
*/
function tryGetWorkflowById(mastra, id) {
if (!id) return void 0;
if (typeof mastra.getWorkflowById === "function") try {
return mastra.getWorkflowById(id);
} catch {}
if (typeof mastra.getWorkflow !== "function") return void 0;
try {
return mastra.getWorkflow(id);
} catch {
return;
}
}
function assertWorkflowExists(mastra, workflowId) {
const wf = tryGetWorkflowById(mastra, workflowId);
if (!wf) throw new Error(`Stored workflow references nested workflow "${workflowId}" which is not registered on this Mastra instance.`);
return wf;
}
//#endregion
//#region src/workflows/stored/graph.ts
/**
* Invoke `visit` for every single-step (leaf) entry in the graph, recursing
* into `parallel`/`conditional` children and `loop`/`foreach` bodies.
*
* Does NOT recurse into a nested workflow's inlined `serializedStepFlow` —
* a nested workflow's own graph is validated when that workflow is added.
* `sleep`/`sleepUntil` entries carry no references or schemas and are skipped.
*/
function forEachSingleStepEntry(entries, visit) {
for (const entry of entries) switch (entry.type) {
case "step":
case "agent":
case "tool":
case "mapping":
case "workflow":
visit(entry);
break;
case "parallel":
case "conditional":
entry.steps.forEach(visit);
break;
case "loop":
case "foreach":
visit(entry.step);
break;
case "sleep":
case "sleepUntil": break;
default:
}
}
/**
* Collect the ids of every nested workflow referenced by a stored graph.
* Used by boot-time loading to hydrate stored definitions in dependency order.
*/
function collectNestedWorkflowIds(graph) {
const out = /* @__PURE__ */ new Set();
forEachSingleStepEntry(graph, (entry) => {
if (entry.type === "workflow") out.add(entry.workflowId);
});
return out;
}
/**
* Same traversal as {@link forEachSingleStepEntry} but reports each leaf's
* position as a dotted path (`graph.2`, `graph.2.steps.0`, `graph.2.step`) —
* the path contract shared by validation issues and the Studio draft UI.
*
* Accepts the wider {@link ValidatableStepFlowEntry} union so both persisted
* graphs and wire-shaped authoring submissions can be walked.
*/
function forEachSingleStepEntryWithPath(entries, visit) {
entries.forEach((entry, index) => {
const path = `graph.${index}`;
switch (entry.type) {
case "step":
case "agent":
case "tool":
case "mapping":
case "workflow":
visit(entry, path);
break;
case "parallel":
case "conditional":
entry.steps.forEach((child, childIndex) => visit(child, `${path}.steps.${childIndex}`));
break;
case "loop":
case "foreach":
visit(entry.step, `${path}.step`);
break;
case "sleep":
case "sleepUntil": break;
default:
}
});
}
//#endregion
//#region src/workflows/stored/validate/refs.ts
/**
* Reference checks against a caller-supplied registry index.
*
* Checks are gated per kind: a kind whose key is absent from the index is
* skipped entirely, so callers that cannot enumerate (say) workflows never
* produce false missing-reference issues. Mis-classified references get swap
* hints (agent id that is actually a registered tool, and vice versa).
*
* `type: 'step'` descriptors are intentionally not checked — they resolve
* late against the live Mastra instance at rehydration time.
*/
function validateWorkflowRefs(def, index) {
const issues = [];
forEachSingleStepEntryWithPath(def.graph, (entry, path) => {
switch (entry.type) {
case "agent":
if (!index.agents || index.agents[entry.agentId]) return;
issues.push({
code: "missing-reference",
path: `${path}.agentId`,
message: index.tools?.[entry.agentId] ? `Step "${entry.id}" declares { type: "agent", agentId: "${entry.agentId}" } but "${entry.agentId}" is a registered TOOL, not an agent. Change this entry to { type: "tool", toolId: "${entry.agentId}" }.` : `Step "${entry.id}" declares agentId "${entry.agentId}" which is not a registered agent.`
});
return;
case "tool":
if (!index.tools || index.tools[entry.toolId]) return;
issues.push({
code: "missing-reference",
path: `${path}.toolId`,
message: index.agents?.[entry.toolId] ? `Step "${entry.id}" declares { type: "tool", toolId: "${entry.toolId}" } but "${entry.toolId}" is a registered AGENT, not a tool. Change this entry to { type: "agent", agentId: "${entry.toolId}" }.` : `Step "${entry.id}" declares toolId "${entry.toolId}" which is not a registered tool.`
});
return;
case "workflow":
if (entry.workflowId === def.id) return;
if (!index.workflows || index.workflows[entry.workflowId]) return;
issues.push({
code: "missing-reference",
path: `${path}.workflowId`,
message: `Step "${entry.id}" declares workflowId "${entry.workflowId}" which is not a registered workflow.`
});
return;
default: return;
}
});
return issues;
}
//#endregion
//#region src/workflows/stored/validate/types.ts
/** Step id of a single-step (leaf) entry; `step` descriptors carry theirs nested. */
function leafEntryId(entry) {
return entry.type === "step" ? entry.step.id : entry.id;
}
//#endregion
//#region src/workflows/stored/validate/repair-actions.ts
function inputSchemaOf$1(entry, index) {
switch (entry.type) {
case "agent": return index.agents?.[entry.agentId]?.inputSchema ?? {
type: "object",
properties: { prompt: { type: "string" } },
required: ["prompt"]
};
case "tool": return index.tools?.[entry.toolId]?.inputSchema;
case "workflow": return index.workflows?.[entry.workflowId]?.inputSchema;
case "mapping":
case "step": return;
}
}
function entryAtPath(def, path) {
const match = /^graph\.(\d+)(?:\.(steps)\.(\d+)|\.(step))?/.exec(path);
if (!match) return void 0;
const entry = def.graph[Number(match[1])];
if (!entry) return void 0;
if (match[2] === "steps" && (entry.type === "parallel" || entry.type === "conditional")) return entry.steps[Number(match[3])];
if (match[4] === "step" && (entry.type === "foreach" || entry.type === "loop")) return entry.step;
if (entry.type === "agent" || entry.type === "tool" || entry.type === "workflow" || entry.type === "mapping" || entry.type === "step") return entry;
}
function precedingSourceIds(def, targetIndex) {
const ids = [];
def.graph.slice(0, targetIndex).forEach((entry) => {
if (entry.type === "parallel" || entry.type === "conditional") entry.steps.forEach((child) => {
const childId = leafEntryId(child);
if (childId) ids.push(childId);
});
else if (entry.type === "foreach" || entry.type === "loop") {
const childId = leafEntryId(entry.step);
if (childId) ids.push(childId);
}
if (entry.type === "parallel" || entry.type === "conditional" || entry.type === "foreach" || entry.type === "loop") return;
const id = "id" in entry && entry.id ? entry.id : entry.type === "step" ? entry.step.id : void 0;
if (id) ids.push(id);
});
return ids;
}
function legalSources(def, targetIndex, expectedSchema, stepOutputs) {
return [{
source: {
initData: true,
path: ""
},
schema: def.inputSchema,
compatibility: schemaCompatibility(def.inputSchema, expectedSchema)
}, ...precedingSourceIds(def, targetIndex).map((stepId) => {
const schema = stepOutputs.get(stepId);
return {
source: {
step: stepId,
path: ""
},
...schema ? { schema } : {},
compatibility: schemaCompatibility(schema, expectedSchema)
};
})].filter((source) => source.compatibility !== "incompatible");
}
function addWorkflowValidationRepairActions(def, index, issues, stepOutputs, entryInputs, finalOutput) {
return issues.map((issue) => {
const graphMatch = /^graph\.(\d+)/.exec(issue.path);
const targetIndex = graphMatch ? Number(graphMatch[1]) : def.graph.length;
const entry = entryAtPath(def, issue.path);
const entryId = entry ? leafEntryId(entry) : void 0;
let repair;
if (issue.code === "incompatible-schema") {
const containerEntry = graphMatch ? def.graph[targetIndex] : void 0;
const foreachChildInput = containerEntry?.type === "foreach" ? inputSchemaOf$1(containerEntry.step, index) : void 0;
const expectedSchema = containerEntry?.type === "foreach" ? {
type: "array",
...foreachChildInput ? { items: foreachChildInput } : {}
} : entry ? inputSchemaOf$1(entry, index) : def.outputSchema;
const actualSchema = issue.path === "outputSchema" ? finalOutput : entryInputs.get(issue.path) ?? (targetIndex === 0 ? def.inputSchema : void 0);
repair = {
issueCode: issue.code,
path: issue.path,
...entryId ? { entryId } : {},
...expectedSchema ? { expectedSchema } : {},
...actualSchema ? { actualSchema } : {},
legalSources: legalSources(def, targetIndex, expectedSchema, stepOutputs),
operation: containerEntry?.type === "foreach" ? "update-workflow-step" : issue.path === "outputSchema" ? "insert-workflow-mapping-after" : "insert-workflow-mapping-before",
arguments: entryId ? { targetStepId: entryId } : { targetPath: issue.path },
blocksCheckpoint: false,
blocksFinalize: true
};
} else if (issue.code === "invalid-map-config" || issue.code === "invalid-map-reference") {
const destinationField = /\.mapConfig\.([^\.]+)/.exec(issue.path)?.[1];
repair = {
issueCode: issue.code,
path: issue.path,
...entryId ? { entryId } : {},
...destinationField ? { destinationField } : {},
legalSources: legalSources(def, targetIndex, void 0, stepOutputs),
operation: "set-workflow-mapping-source",
arguments: {
...entryId ? { mappingStepId: entryId } : {},
...destinationField ? { field: destinationField } : {}
},
blocksCheckpoint: false,
blocksFinalize: true
};
} else if (issue.code === "invalid-predicate-reference") repair = {
issueCode: issue.code,
path: issue.path,
...entryId ? { childId: entryId } : {},
operation: "set-workflow-predicate",
arguments: { predicatePath: issue.path },
blocksCheckpoint: false,
blocksFinalize: true
};
else if (issue.code === "missing-reference") repair = {
issueCode: issue.code,
path: issue.path,
...entryId ? { entryId } : {},
operation: "update-workflow-step",
arguments: entryId ? { stepId: entryId } : { targetPath: issue.path },
blocksCheckpoint: false,
blocksFinalize: true
};
else if (issue.code === "invalid-map-placement") repair = {
issueCode: issue.code,
path: issue.path,
...entryId ? { childId: entryId } : {},
operation: "remove-workflow-step",
arguments: entryId ? { stepId: entryId } : { targetPath: issue.path },
blocksCheckpoint: false,
blocksFinalize: true
};
return repair ? {
...issue,
repair
} : issue;
});
}
//#endregion
//#region src/workflows/stored/validate/schema-flow.ts
/** Agents accept `{ prompt }` unless the registry says otherwise. */
const agentInputSchema = {
type: "object",
properties: { prompt: { type: "string" } },
required: ["prompt"]
};
/**
* An agent entry without declared structured output returns exactly
* `{ text }` at runtime (`runAgentEntry`), so invented output paths like
* `.response` are provably wrong rather than unknown.
*/
const agentTextOutputSchema = {
type: "object",
properties: { text: { type: "string" } },
required: ["text"]
};
function inputSchemaOf(entry, index) {
switch (entry.type) {
case "agent": return index.agents?.[entry.agentId]?.inputSchema ?? agentInputSchema;
case "tool": return index.tools?.[entry.toolId]?.inputSchema;
case "workflow": return index.workflows?.[entry.workflowId]?.inputSchema;
case "mapping":
case "step": return;
}
}
function outputSchemaOf(entry, index) {
switch (entry.type) {
case "agent": return entry.outputSchema ?? index.agents?.[entry.agentId]?.outputSchema ?? agentTextOutputSchema;
case "tool": return index.tools?.[entry.toolId]?.outputSchema;
case "workflow": return index.workflows?.[entry.workflowId]?.outputSchema;
case "mapping":
case "step": return;
}
}
function validatePredicate(predicate, path, context) {
const issues = [];
const validatePath = (rawPath, issuePath) => {
if (!isCanonicalMappingPath(rawPath)) {
issues.push({
code: "invalid-predicate-reference",
path: issuePath,
message: "Predicate paths must use plain dotted segments rooted at initData, inputData, stepResults, or state."
});
return;
}
const [root, ...segments] = rawPath.split(".");
let schema;
let schemaPath = segments.join(".");
if (root === "initData") schema = context.initData;
else if (root === "inputData") schema = context.inputData;
else if (root === "state") schema = context.state;
else if (root === "stepResults") {
const stepId = segments.shift();
if (!stepId || !context.stepResults.has(stepId)) {
issues.push({
code: "invalid-predicate-reference",
path: issuePath,
message: stepId ? `Predicate step result "${stepId}" must reference a preceding top-level step.` : "Predicate stepResults paths must include a preceding top-level step id."
});
return;
}
schema = context.stepResults.get(stepId);
schemaPath = segments.join(".");
} else {
issues.push({
code: "invalid-predicate-reference",
path: issuePath,
message: "Predicate paths must be rooted at initData, inputData, stepResults, or state."
});
return;
}
if (schemaPath && isRecord(schema) && typeof schema.type === "string" && !schemaAtPath(schema, schemaPath)) issues.push({
code: "invalid-predicate-reference",
path: issuePath,
message: `Predicate path "${rawPath}" does not exist in the known schema.`
});
};
const validateRef = (ref, refPath) => {
if ("path" in ref) validatePath(ref.path, `${refPath}.path`);
};
switch (predicate.op) {
case "and":
case "or":
predicate.args.forEach((arg, index) => issues.push(...validatePredicate(arg, `${path}.args.${index}`, context)));
break;
case "not":
issues.push(...validatePredicate(predicate.arg, `${path}.arg`, context));
break;
case "exists":
case "notExists":
validatePath(predicate.path, `${path}.path`);
break;
case "truthy":
case "falsy":
validateRef(predicate.value, `${path}.value`);
break;
case "in":
case "notIn":
validateRef(predicate.value, `${path}.value`);
break;
default:
validateRef(predicate.left, `${path}.left`);
validateRef(predicate.right, `${path}.right`);
}
return issues;
}
function inferGraphSchemas(def, index) {
const issues = [];
const stepOutputs = /* @__PURE__ */ new Map();
const entryInputs = /* @__PURE__ */ new Map();
/** Evaluates one leaf entry: checks its input against `incoming`, returns its output. */
const evalLeaf = (entry, path, incoming, container) => {
entryInputs.set(path, incoming);
if (entry.type === "mapping") {
if (container) return void 0;
const analysis = analyzeMapConfig(entry.mapConfig, {
path,
availableOutputs: stepOutputs,
inputSchema: def.inputSchema,
requestContextSchema: def.requestContextSchema
});
issues.push(...analysis.issues);
return analysis.outputSchema;
}
if (entry.type === "step") return void 0;
if (schemaCompatibility(incoming, inputSchemaOf(entry, index)) === "incompatible") issues.push({
code: "incompatible-schema",
path,
message: "Step input is incompatible with the preceding workflow output."
});
return outputSchemaOf(entry, index);
};
let current = def.inputSchema;
def.graph.forEach((entry, entryIndex) => {
const path = `graph.${entryIndex}`;
switch (entry.type) {
case "step":
case "agent":
case "tool":
case "mapping":
case "workflow":
current = evalLeaf(entry, path, current, false);
break;
case "sleep":
case "sleepUntil": break;
case "parallel":
case "conditional": {
const incoming = current;
if (entry.type === "conditional") entry.predicates?.forEach((predicate, predicateIndex) => {
if (!predicate) return;
issues.push(...validatePredicate(predicate, `${path}.predicates.${predicateIndex}`, {
initData: def.inputSchema,
inputData: incoming,
state: def.stateSchema,
stepResults: stepOutputs
}));
});
const properties = {};
entry.steps.forEach((child, childIndex) => {
const output = evalLeaf(child, `${path}.steps.${childIndex}`, incoming, true);
const childId = leafEntryId(child);
if (childId) {
stepOutputs.set(childId, output);
if (output) properties[childId] = output;
}
});
current = {
type: "object",
properties,
...entry.type === "parallel" ? { required: Object.keys(properties) } : {}
};
break;
}
case "foreach": {
const incoming = current;
if (isRecord(incoming) && typeof incoming.type === "string" && incoming.type !== "array") issues.push({
code: "incompatible-schema",
path,
message: "Foreach input must be a raw array. A mapping step cannot produce one — mappings always build an object — so the preceding step (or the workflow inputSchema itself) must already be an array of the child input."
});
const items = isRecord(incoming?.items) ? incoming.items : void 0;
const output = evalLeaf(entry.step, `${path}.step`, items, true);
const childId = leafEntryId(entry.step);
if (childId) stepOutputs.set(childId, output);
current = output ? {
type: "array",
items: output
} : output;
break;
}
case "loop": {
const output = evalLeaf(entry.step, `${path}.step`, current, true);
const stepId = leafEntryId(entry.step);
if (stepId) stepOutputs.set(stepId, output);
if (entry.predicate) {
const loopStepOutputs = new Map(stepOutputs);
issues.push(...validatePredicate(entry.predicate, `${path}.predicate`, {
initData: def.inputSchema,
inputData: output,
state: def.stateSchema,
stepResults: loopStepOutputs
}));
}
if (schemaCompatibility(output, inputSchemaOf(entry.step, index)) === "incompatible") issues.push({
code: "incompatible-schema",
path: `${path}.step`,
message: "Loop step output is incompatible with its input for a subsequent iteration."
});
current = output;
break;
}
default:
}
const id = "id" in entry && entry.id ? entry.id : entry.type === "step" ? entry.step.id : void 0;
if (id) stepOutputs.set(id, current);
});
if (schemaCompatibility(current, def.outputSchema) === "incompatible") issues.push({
code: "incompatible-schema",
path: "outputSchema",
message: "Workflow output schema is incompatible with the final step output."
});
return {
stepOutputs,
entryInputs,
finalOutput: current,
issues
};
}
//#endregion
//#region src/workflows/stored/validate/schemas.ts
/**
* JSON-Schema keyword checks: every schema embedded in the definition must be
* convertible by `jsonSchemaToZod` (no oneOf/anyOf/allOf/not/$ref/
* patternProperties/discriminator). Covers the four top-level schemas plus
* each `agent.outputSchema` reachable through containers.
*/
function validateWorkflowSchemas(def) {
const issues = [];
const check = (schema, path, label) => {
const result = validateStorableJsonSchema(schema);
if (result.ok) return;
issues.push({
code: "unsupported-schema-keyword",
path,
message: `${label} uses JSON Schema keyword(s) jsonSchemaToZod cannot convert: ${result.unsupported.join(", ")}. Simplify the schema (or extend the converter).`
});
};
check(def.inputSchema, "inputSchema", "inputSchema");
check(def.outputSchema, "outputSchema", "outputSchema");
if (def.stateSchema) check(def.stateSchema, "stateSchema", "stateSchema");
if (def.requestContextSchema) check(def.requestContextSchema, "requestContextSchema", "requestContextSchema");
forEachSingleStepEntryWithPath(def.graph, (entry, path) => {
if (entry.type === "agent" && entry.outputSchema) check(entry.outputSchema, `${path}.outputSchema`, `step "${entry.id}" outputSchema`);
});
return issues;
}
//#endregion
//#region src/workflows/stored/validate/structure.ts
/**
* Context-free structural rules: everything that can be decided from the
* definition alone — ids, duplicates, entry placement, container arity,
* declarative-predicate presence, nested-workflow identity, self-cycles.
*/
const TOP_LEVEL_PATH = /^graph\.\d+$/;
function validateWorkflowStructure(def) {
const issues = [];
if (def.graph.length === 0) issues.push({
code: "empty-graph",
path: "graph",
message: "Workflow graph must contain at least one step."
});
const seenIds = /* @__PURE__ */ new Set();
forEachSingleStepEntryWithPath(def.graph, (entry, path) => {
const id = leafEntryId(entry);
const idPath = entry.type === "step" ? `${path}.step.id` : `${path}.id`;
if (!id) issues.push({
code: "missing-step-id",
path: idPath,
message: "Step id is required."
});
else if (seenIds.has(id)) issues.push({
code: "duplicate-step-id",
path: idPath,
message: `Step id "${id}" is duplicated.`
});
else seenIds.add(id);
if (entry.type === "mapping" && !TOP_LEVEL_PATH.test(path)) issues.push({
code: "invalid-map-placement",
path,
message: "Persisted mapping steps must be top-level workflow entries."
});
if (entry.type === "workflow") {
if (entry.workflowId === def.id) issues.push({
code: "self-reference",
path: `${path}.workflowId`,
message: `Step "${entry.id}" declares { type: "workflow", workflowId: "${entry.workflowId}" } which refers to itself. Nested workflow cycles are not allowed.`
});
}
});
def.graph.forEach((entry, index) => {
const path = `graph.${index}`;
switch (entry.type) {
case "parallel":
case "conditional":
if (entry.steps.length === 0) issues.push({
code: entry.type === "parallel" ? "invalid-parallel" : "invalid-conditional",
path: `${path}.steps`,
message: `${entry.type} steps cannot be empty.`
});
if (entry.type === "conditional") if (!entry.predicates) issues.push({
code: "invalid-conditional",
path,
message: "Conditional entries must use declarative predicates."
});
else {
if (entry.steps.length !== entry.predicates.length) issues.push({
code: "invalid-conditional",
path,
message: "Conditional steps and predicates must be aligned."
});
entry.predicates.forEach((predicate, predicateIndex) => {
if (predicate === null) issues.push({
code: "invalid-conditional",
path: `${path}.predicates.${predicateIndex}`,
message: "Conditional entries must use declarative predicates."
});
});
}
return;
case "loop":
if (!entry.predicate) issues.push({
code: "invalid-loop",
path,
message: "Loop entries must use a declarative predicate."
});
return;
case "foreach":
if (entry.opts?.concurrency !== void 0 && entry.opts.concurrency < 1) issues.push({
code: "invalid-foreach",
path: `${path}.opts.concurrency`,
message: "Concurrency must be positive."
});
return;
default: return;
}
});
return issues;
}
//#endregion
//#region src/workflows/stored/validate/index.ts
/**
* The one stored-workflow validation domain.
*
* `validateStoredWorkflow` is the collect-mode core every surface shares:
* structure, JSON-Schema keywords, registry references, and schema-flow
* analysis, each emitting `{ code, path, message }` issues. UIs consume the
* array; the save path throws via `assertValidStoredWorkflow`.
*/
/**
* Runs every check and returns the collected issues (empty = valid).
*
* The registry index gates context-dependent checks: reference checks only
* run for kinds present in the index, and schema-flow compatibility only
* proves mismatches where schemas are known.
*/
function validateStoredWorkflow(def, index = {}) {
const inference = inferGraphSchemas(def, index);
return addWorkflowValidationRepairActions(def, index, [
...validateWorkflowStructure(def),
...validateWorkflowSchemas(def),
...validateWorkflowRefs(def, index),
...inference.issues
], inference.stepOutputs, inference.entryInputs, inference.finalOutput);
}
/** Throwing presentation of {@link validateStoredWorkflow} for the save path. */
function assertValidStoredWorkflow(def, index = {}) {
const issues = validateStoredWorkflow(def, index);
if (issues.length === 0) return;
const details = issues.map((issue) => `- [${issue.code}] ${issue.path}: ${issue.message}`).join("\n");
throw new Error(`Stored workflow "${def.id}" failed validation with ${issues.length} issue(s):\n${details}`);
}
//#endregion
export { inferGraphSchemas as a, forEachSingleStepEntry as c, analyzeMapConfig as d, p