@mastra/core
Version:
1 lines • 105 kB
Source Map (JSON)
{"version":3,"file":"validate-C0iI8guc.cjs","names":["z","collectTemplateStepIds","createWorkflow","cloneWorkflow","getSingleStepEntryId","derivePredicateLabel","predicateToCondition","_exhaustive","createStepFromAgent","createStepFromTool","mapVariable","inputSchemaOf"],"sources":["../src/workflows/stored/json-schema-to-zod.ts","../src/workflows/stored/validate/schema-utils.ts","../src/workflows/stored/mapping-config.ts","../src/workflows/stored/rehydrate.ts","../src/workflows/stored/graph.ts","../src/workflows/stored/validate/refs.ts","../src/workflows/stored/validate/types.ts","../src/workflows/stored/validate/repair-actions.ts","../src/workflows/stored/validate/schema-flow.ts","../src/workflows/stored/validate/schemas.ts","../src/workflows/stored/validate/structure.ts","../src/workflows/stored/validate/index.ts"],"sourcesContent":["/**\n * Minimal JSON-Schema ↔ Zod bridge for stored workflows: a converter for the\n * static subset Zod round-trips through `standardSchemaToJSONSchema`, plus a\n * non-throwing validator for the write path.\n */\nimport { z } from 'zod';\n\n/**\n * Minimal JSON-Schema shape we accept. Intentionally untyped on the value side\n * — different JSON Schema producers emit slightly different shapes and the\n * inline converter below just inspects the fields it cares about.\n */\nexport type JsonSchema = Record<string, any>;\n\n/**\n * Options controlling how `jsonSchemaToZod` handles JSON Schema keywords the\n * MVP converter doesn't support.\n *\n * - `throw` (default): hard-crash with a targeted error. Correct for the save\n * path — the author is right there and can simplify the schema.\n * - `warn`: emit a warning via `onUnsupported` (if provided) and fall back to\n * `z.any()` for the unsupported subtree. Correct for the boot-time load\n * path — one bad pre-existing row must not take down startup for every\n * other workflow.\n */\nexport interface JsonSchemaToZodOptions {\n onUnsupportedSchema?: 'throw' | 'warn';\n onUnsupported?: (message: string) => void;\n}\n\n/**\n * Inline converter sufficient for the static subset Zod typically emits when\n * round-tripped through `standardSchemaToJSONSchema`. Handles:\n *\n * - `object` with `properties` + `required`\n * - `string` / `number` / `integer` / `boolean` / `null`\n * - `array` with `items`\n * - `enum`\n * - `description` (propagated via `.describe`)\n *\n * For more exotic schemas (unions, intersections, recursive refs) swap in\n * `json-schema-to-zod` from npm. Kept inline to avoid pulling a dependency\n * for the MVP demo.\n */\nexport function jsonSchemaToZod(schema: JsonSchema, opts?: JsonSchemaToZodOptions): z.ZodTypeAny {\n return walk(schema, opts ?? {});\n}\n\n// JSON Schema keywords that this MVP converter does not support. If a stored\n// workflow's inputSchema/outputSchema uses any of these, silently converting\n// to z.any() would strip the constraint at rehydration and let bad data flow\n// through at execution — hard-crash instead so the corruption surfaces at\n// load time.\nconst UNSUPPORTED_SCHEMA_KEYS = [\n 'oneOf',\n 'anyOf',\n 'allOf',\n 'not',\n '$ref',\n 'patternProperties',\n 'discriminator',\n] as const;\n\n/** Values `z.literal()` can represent — the only const/enum members that survive conversion losslessly. */\nfunction isLiteralValue(v: unknown): v is string | number | boolean | null {\n return v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';\n}\n\n/** Throw or warn-and-fallback per `onUnsupportedSchema`, matching the unsupported-keyword behavior. */\nfunction unsupported(message: string, opts: JsonSchemaToZodOptions): z.ZodTypeAny {\n if (opts.onUnsupportedSchema === 'warn') {\n opts.onUnsupported?.(message);\n return z.any();\n }\n throw new Error(message);\n}\n\nfunction walk(schema: JsonSchema, opts: JsonSchemaToZodOptions): z.ZodTypeAny {\n if (!schema || typeof schema !== 'object') return z.any();\n\n for (const key of UNSUPPORTED_SCHEMA_KEYS) {\n if (key in schema) {\n return unsupported(\n `Stored workflow schema uses unsupported JSON Schema keyword \"${key}\". ` +\n `This converter only supports the static subset that Zod round-trips through ` +\n `standardSchemaToJSONSchema (object, array, string, number, integer, boolean, null, enum, const). ` +\n `Simplify the schema or extend jsonSchemaToZod to cover this keyword.`,\n opts,\n );\n }\n }\n\n let out: z.ZodTypeAny;\n\n if ('const' in schema) {\n // Zod emits `{ const: value }` for z.literal() — preserve it instead of\n // silently dropping the constraint. Non-primitive consts (objects/arrays)\n // can't be represented by z.literal, so treat them as unsupported.\n if (!isLiteralValue(schema.const)) {\n return unsupported(\n `Stored workflow schema uses a non-primitive \"const\" value (${JSON.stringify(schema.const)}). ` +\n `Only string, number, boolean, and null literals are supported.`,\n opts,\n );\n }\n out = z.literal(schema.const);\n } else if (Array.isArray(schema.enum) && schema.enum.length > 0) {\n const values = schema.enum as unknown[];\n if (!values.every(isLiteralValue)) {\n return unsupported(\n `Stored workflow schema uses an \"enum\" with non-primitive members. ` +\n `Only string, number, boolean, and null enum members are supported.`,\n opts,\n );\n }\n if (values.every(v => typeof v === 'string')) {\n out = z.enum(values as [string, ...string[]]);\n } else {\n // Mixed/non-string enums (e.g. [1, 2, 3] or ['a', 1]): preserve the\n // original member types via literal union instead of coercing to string.\n const literals: z.ZodTypeAny[] = values.map(v => z.literal(v as string | number | boolean | null));\n out = literals.length === 1 ? literals[0]! : z.union(literals as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]);\n }\n } else if (Array.isArray(schema.type)) {\n const options = schema.type.map((t: string) => walk({ ...schema, type: t }, opts));\n // z.union requires a tuple of at least two members; guard shorter arrays.\n if (options.length === 1) {\n out = options[0]!;\n } else {\n out = z.union(options as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]);\n }\n } else {\n switch (schema.type) {\n case 'object': {\n const shape: Record<string, z.ZodTypeAny> = {};\n const required = new Set<string>(Array.isArray(schema.required) ? schema.required : []);\n for (const [key, child] of Object.entries(schema.properties ?? {})) {\n const childSchema = walk(child as JsonSchema, opts);\n shape[key] = required.has(key) ? childSchema : childSchema.optional();\n }\n const obj = z.object(shape);\n out = schema.additionalProperties === true ? obj.passthrough() : obj;\n break;\n }\n case 'array':\n // Tuple-form `items: [...]` positional schemas aren't representable by\n // z.array(); converting to z.array(z.any()) would strip every\n // positional constraint. Reject instead of silently widening.\n if (Array.isArray(schema.items)) {\n return unsupported(\n `Stored workflow schema uses tuple-form \"items\" (an array of positional schemas). ` +\n `Only a single item schema is supported; use \"items\": { ... } instead.`,\n opts,\n );\n }\n out = z.array(walk(schema.items ?? {}, opts));\n break;\n case 'string':\n out = z.string();\n break;\n case 'number':\n out = z.number();\n break;\n case 'integer':\n out = z.number().int();\n break;\n case 'boolean':\n out = z.boolean();\n break;\n case 'null':\n out = z.null();\n break;\n case undefined:\n // No `type` and no enum/typed-array — schema is just a description\n // or annotation wrapper; permit z.any() for these.\n out = z.any();\n break;\n default:\n return unsupported(\n `Stored workflow schema uses unsupported JSON Schema type \"${String(schema.type)}\". ` +\n `This converter only supports object, array, string, number, integer, boolean, null, and enum.`,\n opts,\n );\n }\n }\n\n if (typeof schema.description === 'string' && schema.description.length > 0) {\n out = out.describe(schema.description);\n }\n return out;\n}\n\n/**\n * Result of a `validateStorableJsonSchema` call.\n * `unsupported` lists every offending keyword usage as `<jsonPointer>: <keyword>`\n * so callers can log or surface a targeted message per offense.\n */\nexport type StorableJsonSchemaValidation = { ok: true } | { ok: false; unsupported: string[] };\n\n/**\n * Non-throwing companion to `jsonSchemaToZod`. Walks a JSON Schema and reports\n * every unsupported-keyword usage without converting. Use this at write time\n * (e.g. inside `Mastra.addStoredWorkflow`) to surface a warning before the\n * schema is persisted — the row will still fail to rehydrate on the next boot\n * (`jsonSchemaToZod` throws), so this is a heads-up, not a guarantee.\n *\n * Callers decide whether to warn, reject, or ignore. This function never\n * throws for any input shape.\n */\nexport function validateStorableJsonSchema(schema: JsonSchema | undefined): StorableJsonSchemaValidation {\n if (!schema || typeof schema !== 'object') return { ok: true };\n const unsupported: string[] = [];\n const visit = (node: unknown, path: string): void => {\n if (!node || typeof node !== 'object') return;\n const n = node as Record<string, unknown>;\n for (const key of UNSUPPORTED_SCHEMA_KEYS) {\n if (key in n) unsupported.push(`${path || '#'}: ${key}`);\n }\n if (n.properties && typeof n.properties === 'object') {\n for (const [prop, child] of Object.entries(n.properties as Record<string, unknown>)) {\n visit(child, `${path}/properties/${prop}`);\n }\n }\n if (n.items) {\n if (Array.isArray(n.items)) {\n n.items.forEach((child, i) => visit(child, `${path}/items/${i}`));\n } else {\n visit(n.items, `${path}/items`);\n }\n }\n if (n.additionalProperties && typeof n.additionalProperties === 'object') {\n visit(n.additionalProperties, `${path}/additionalProperties`);\n }\n };\n visit(schema, '');\n return unsupported.length === 0 ? { ok: true } : { ok: false, unsupported };\n}\n","/**\n * Pure JSON-Schema helpers shared by schema-flow analysis and mapping-config\n * analysis. Everything here is best-effort and three-valued: a check only\n * reports `incompatible` when it can prove a mismatch, so absent or partial\n * schemas degrade to `unknown` instead of producing false positives.\n */\nimport { standardSchemaToJSONSchema, toStandardSchema } from '../../../schema';\nimport type { JsonSchema } from '../json-schema-to-zod';\n\nexport type SchemaCompatibility = 'compatible' | 'incompatible' | 'unknown';\n\n/**\n * Best-effort conversion of a live (Zod / standard) schema to JSON Schema for\n * registry-index building. Unconvertible or absent schemas yield `undefined`\n * (\"unknown\"), which schema-flow treats as never-incompatible.\n */\nexport function toJsonSchemaOrUndefined(schema: unknown): JsonSchema | undefined {\n if (schema === undefined || schema === null) return undefined;\n try {\n return standardSchemaToJSONSchema(toStandardSchema(schema)) as JsonSchema;\n } catch {\n return undefined;\n }\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** True when both types are numeric, i.e. some mix of `integer` and `number`. */\nfunction isNumeric(sourceType: string, destinationType: string): boolean {\n const numeric = new Set(['integer', 'number']);\n return numeric.has(sourceType) && numeric.has(destinationType);\n}\n\n/**\n * Structural compatibility of `source` output feeding a `destination` input.\n * Recurses through array items and object properties; a destination `required`\n * key missing from the source is a proven incompatibility.\n */\nexport function schemaCompatibility(source: unknown, destination: unknown): SchemaCompatibility {\n if (!isRecord(source) || !isRecord(destination)) return 'unknown';\n const sourceType = typeof source.type === 'string' ? source.type : undefined;\n const destinationType = typeof destination.type === 'string' ? destination.type : undefined;\n if (!sourceType || !destinationType) return 'unknown';\n // `integer` is a subtype of `number`, so a whole-number source satisfies a\n // `number` destination. The reverse isn't provably wrong either: a `number`\n // source can hold whole values at runtime, and this function only reports\n // incompatibilities it can prove.\n if (sourceType !== destinationType && !isNumeric(sourceType, destinationType)) return 'incompatible';\n if (destinationType === 'array') return schemaCompatibility(source.items, destination.items);\n if (destinationType !== 'object') return 'compatible';\n\n const sourceProperties = isRecord(source.properties) ? source.properties : {};\n const destinationProperties = isRecord(destination.properties) ? destination.properties : {};\n const required = Array.isArray(destination.required)\n ? destination.required.filter((key): key is string => typeof key === 'string')\n : [];\n for (const key of required) {\n if (!(key in sourceProperties)) return 'incompatible';\n }\n for (const [key, destinationProperty] of Object.entries(destinationProperties)) {\n if (!(key in sourceProperties)) continue;\n if (schemaCompatibility(sourceProperties[key], destinationProperty) === 'incompatible') return 'incompatible';\n }\n return 'compatible';\n}\n\n/** Follows a dotted mapping path through object `properties`; `''`/`'.'` is the root. */\nexport function schemaAtPath(schema: JsonSchema | undefined, path: string): JsonSchema | undefined {\n if (!schema || path === '' || path === '.') return schema;\n let current: unknown = schema;\n for (const segment of path.split('.')) {\n if (!isRecord(current) || !isRecord(current.properties) || !isRecord(current.properties[segment])) return undefined;\n current = current.properties[segment];\n }\n return current as JsonSchema;\n}\n\n/** Plain dotted segments only — no `$.`, brackets, or empty segments. */\nexport function isCanonicalMappingPath(path: string): boolean {\n return path === '' || path === '.' || /^[^.[$\\]]+(?:\\.[^.[$\\]]+)*$/.test(path);\n}\n\n/** Infers a JSON Schema for a literal `{ value }` mapping source. */\nexport function schemaForValue(value: unknown): JsonSchema {\n if (value === null) return { type: 'null' };\n if (Array.isArray(value)) return { type: 'array' };\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return { type: typeof value };\n case 'number':\n return { type: Number.isInteger(value) ? 'integer' : 'number' };\n case 'object':\n return { type: 'object' };\n default:\n return {};\n }\n}\n","/**\n * The single home for stored `mapConfig` handling.\n *\n * A mapping entry's config crosses the storage boundary as a JSON string.\n * - {@link parseMapConfig} is the one parser (rehydration + validation both\n * use it; rehydration via the throwing form).\n * - {@link analyzeMapConfig} is the one validator: it walks each descriptor,\n * collects issues, and infers the mapping's output schema in the same pass\n * (the two are inseparable — a descriptor's validity determines its\n * contribution to the output shape).\n *\n * Template syntax checking delegates to `mapping-template.ts`'s\n * `validateTemplate` — the same parser the runtime uses — plus a scope check\n * over the placeholders' step ids.\n */\nimport { collectTemplateStepIds, validateTemplate } from '../mapping-template';\nimport type { JsonSchema } from './json-schema-to-zod';\nimport { isCanonicalMappingPath, isRecord, schemaAtPath, schemaForValue } from './validate/schema-utils';\nimport type { WorkflowValidationIssue } from './validate/types';\n\n/** Parses a stored mapConfig JSON string; throws with the step id on malformed JSON. */\nexport function parseMapConfig(raw: string, stepId: string): Record<string, any> {\n try {\n return JSON.parse(raw) as Record<string, any>;\n } catch (e) {\n throw new Error(`Stored mapping step \"${stepId}\" has invalid JSON mapConfig: ${(e as Error).message}`);\n }\n}\n\n/** A recognizable Handlebars/Mustache placeholder: `{{ name }}`, `{{a.b}}`, … */\nconst HANDLEBARS_PLACEHOLDER = /\\{\\{\\s*[\\w$][\\w.$-]*\\s*\\}\\}/;\n\nexport interface MapConfigAnalysisOptions {\n /** Issue path prefix of the mapping entry, e.g. `graph.2`. */\n path: string;\n /** Outputs of preceding workflow-local steps (schema may be undefined when unknown). */\n availableOutputs: ReadonlyMap<string, JsonSchema | undefined>;\n /** The workflow's input schema (for `{ initData: true }` sources). */\n inputSchema: JsonSchema | undefined;\n /** The workflow's request-context schema (for `{ requestContextPath }` sources). */\n requestContextSchema: JsonSchema | undefined;\n}\n\nexport interface MapConfigAnalysis {\n issues: WorkflowValidationIssue[];\n /** Inferred output schema of the mapping step; undefined when the config is unusable. */\n outputSchema: JsonSchema | undefined;\n}\n\n/**\n * Validates a mapping entry's raw `mapConfig` string and infers the step's\n * output schema. Every key must define exactly one source\n * (`value` | `template` | `requestContextPath` | `initData`/`step` + `path`);\n * step references must point at preceding workflow-local steps.\n */\nexport function analyzeMapConfig(rawConfig: string, opts: MapConfigAnalysisOptions): MapConfigAnalysis {\n const issues: WorkflowValidationIssue[] = [];\n const { path, availableOutputs } = opts;\n\n let config: unknown;\n try {\n config = JSON.parse(rawConfig);\n } catch {\n config = undefined;\n }\n if (!isRecord(config)) {\n issues.push({\n code: 'invalid-map-config',\n path: `${path}.mapConfig`,\n message: 'Mapping config must be a JSON object.',\n });\n return { issues, outputSchema: undefined };\n }\n\n const properties: Record<string, JsonSchema> = {};\n for (const [key, descriptor] of Object.entries(config)) {\n const descriptorPath = `${path}.mapConfig.${key}`;\n if (!isRecord(descriptor)) {\n issues.push({\n code: 'invalid-map-config',\n path: descriptorPath,\n message: 'Mapping descriptor must be an object.',\n });\n continue;\n }\n const forms = [\n 'value' in descriptor,\n typeof descriptor.template === 'string',\n typeof descriptor.requestContextPath === 'string',\n 'path' in descriptor,\n ].filter(Boolean).length;\n if (forms !== 1) {\n issues.push({\n code: 'invalid-map-config',\n path: descriptorPath,\n message: 'Mapping descriptor must define exactly one source.',\n });\n continue;\n }\n if ('value' in descriptor) {\n properties[key] = schemaForValue(descriptor.value);\n continue;\n }\n if (typeof descriptor.template === 'string') {\n let syntaxError: string | undefined;\n try {\n validateTemplate(descriptor.template);\n } catch (err) {\n syntaxError = (err as Error).message;\n }\n // Handlebars-style `{{name}}` is not a workflow placeholder — the runtime\n // would emit it literally, which is never what the author meant.\n if (syntaxError === undefined && HANDLEBARS_PLACEHOLDER.test(descriptor.template)) {\n syntaxError = `Templates use \\${...} placeholders (e.g. \"\\${initData.name}\"), not {{...}}. \"${descriptor.template}\" would be emitted literally.`;\n }\n const unknownStep =\n syntaxError === undefined\n ? collectTemplateStepIds(descriptor.template).find(stepId => !availableOutputs.has(stepId))\n : undefined;\n if (syntaxError !== undefined || unknownStep !== undefined) {\n issues.push({\n code: 'invalid-map-reference',\n path: `${descriptorPath}.template`,\n message: syntaxError ?? 'Template references must use an available workflow-local source.',\n });\n }\n properties[key] = { type: 'string' };\n continue;\n }\n if (typeof descriptor.requestContextPath === 'string') {\n if (!isCanonicalMappingPath(descriptor.requestContextPath) || descriptor.requestContextPath === '') {\n issues.push({\n code: 'invalid-map-config',\n path: `${descriptorPath}.requestContextPath`,\n message: 'Mapping paths must use plain dotted segments.',\n });\n }\n properties[key] = schemaAtPath(opts.requestContextSchema, descriptor.requestContextPath) ?? {};\n continue;\n }\n\n if (typeof descriptor.path !== 'string' || !isCanonicalMappingPath(descriptor.path)) {\n issues.push({\n code: 'invalid-map-config',\n path: `${descriptorPath}.path`,\n message: 'Mapping paths must use plain dotted segments.',\n });\n continue;\n }\n const hasInitData = descriptor.initData === true;\n const stepIds =\n typeof descriptor.step === 'string' ? [descriptor.step] : Array.isArray(descriptor.step) ? descriptor.step : [];\n if (hasInitData === stepIds.length > 0 || stepIds.some(stepId => typeof stepId !== 'string')) {\n issues.push({\n code: 'invalid-map-config',\n path: descriptorPath,\n message: 'Path mappings must reference exactly one of initData or step.',\n });\n continue;\n }\n let sourceSchema: JsonSchema | undefined;\n if (hasInitData) {\n sourceSchema = opts.inputSchema;\n } else {\n const missing = stepIds.find(stepId => !availableOutputs.has(stepId));\n if (missing) {\n issues.push({\n code: 'invalid-map-reference',\n path: `${descriptorPath}.step`,\n message: `Mapping source \"${missing}\" must be a preceding workflow-local step.`,\n });\n continue;\n }\n sourceSchema = stepIds.map(stepId => availableOutputs.get(stepId)).find(Boolean);\n }\n const selectedSchema = schemaAtPath(sourceSchema, descriptor.path);\n if (sourceSchema && !selectedSchema) {\n issues.push({\n code: 'invalid-map-config',\n path: `${descriptorPath}.path`,\n message: `Path \"${descriptor.path}\" does not exist in the source schema.`,\n });\n }\n properties[key] = selectedSchema ?? {};\n }\n return { issues, outputSchema: { type: 'object', properties, required: Object.keys(config) } };\n}\n","/**\n * Storable → Runnable half of the workflow round-trip: rebuild a runnable\n * `Workflow` from the stored JSON form. References to agents/tools/workflows\n * are resolved against the live Mastra instance; throws if a reference is\n * missing — better to surface the failure at load time than at run time.\n */\nimport type { Mastra } from '../../mastra';\nimport { cloneWorkflow, createWorkflow } from '../create';\nimport { derivePredicateLabel } from '../predicate';\nimport type { Step } from '../step';\nimport { createStepFromAgent, createStepFromTool } from '../step-factories';\nimport type {\n SerializedSingleStepEntry,\n SerializedStepFlowEntry,\n SerializedStepOptions,\n SingleStepEntry,\n StepFlowEntry,\n} from '../types';\nimport { getSingleStepEntryId } from '../utils';\nimport { mapVariable, predicateToCondition } from '../workflow';\nimport { jsonSchemaToZod } from './json-schema-to-zod';\nimport type { JsonSchema, JsonSchemaToZodOptions } from './json-schema-to-zod';\nimport { parseMapConfig } from './mapping-config';\n\n/** JSON shape persisted to WorkflowDefinitionsStorage. */\nexport interface StoredWorkflowGraph {\n id: string;\n description?: string;\n metadata?: Record<string, unknown>;\n inputSchema: JsonSchema;\n outputSchema: JsonSchema;\n stateSchema?: JsonSchema;\n requestContextSchema?: JsonSchema;\n graph: SerializedStepFlowEntry[];\n}\n\n/**\n * Wrapper so the return value isn't recognized as a thenable by `await`.\n * `Workflow` carries a `.then(step)` builder method — returning one directly\n * from an `async` function (or any `await`-ed call) makes the runtime call\n * that builder method as a Promise resolver and the call hangs forever.\n * Always destructure: `const { workflow } = await rehydrateWorkflow(...)`.\n */\nexport interface RehydratedWorkflow {\n workflow: any;\n}\n\n/**\n * Options controlling how `rehydrateWorkflow` handles unsupported JSON Schema\n * keywords. Forwarded to `jsonSchemaToZod` for every schema on the definition\n * (top-level + per-step `agent.outputSchema`). See `JsonSchemaToZodOptions`.\n */\nexport type RehydrateWorkflowOptions = JsonSchemaToZodOptions;\n\nexport async function rehydrateWorkflow(\n def: StoredWorkflowGraph,\n mastra: Mastra,\n opts?: RehydrateWorkflowOptions,\n): Promise<RehydratedWorkflow> {\n const inputSchema = jsonSchemaToZod(def.inputSchema, opts);\n const outputSchema = jsonSchemaToZod(def.outputSchema, opts);\n const stateSchema = def.stateSchema ? jsonSchemaToZod(def.stateSchema, opts) : undefined;\n const requestContextSchema = def.requestContextSchema ? jsonSchemaToZod(def.requestContextSchema, opts) : undefined;\n\n const wf = createWorkflow({\n id: def.id,\n description: def.description,\n metadata: def.metadata,\n inputSchema: inputSchema as any,\n outputSchema: outputSchema as any,\n stateSchema: stateSchema as any,\n requestContextSchema: requestContextSchema as any,\n });\n\n for (const entry of def.graph) {\n applyGraphEntry(wf, entry, mastra, opts);\n }\n const built: any = wf.commit();\n built.origin = 'stored';\n return { workflow: built };\n}\n\nfunction applyGraphEntry(\n wf: any,\n entry: SerializedStepFlowEntry,\n mastra: Mastra,\n schemaOpts?: JsonSchemaToZodOptions,\n): void {\n switch (entry.type) {\n case 'agent':\n case 'tool':\n wf.__pushStepFlowEntry(rehydrateSingleEntry(entry, mastra, schemaOpts), entry);\n return;\n case 'mapping': {\n const cfg = parseMapConfig(entry.mapConfig, entry.id);\n const live = rehydrateMapConfig(cfg, mastra);\n wf.map(live, { id: entry.id });\n return;\n }\n case 'sleep': {\n if (typeof entry.duration !== 'number') {\n throw new Error(`Stored sleep \"${entry.id}\" missing literal duration.`);\n }\n // Push directly (not wf.sleep()) so the stored step id survives the\n // round-trip — the builder generates a fresh random id per call.\n const live: StepFlowEntry = { type: 'sleep', id: entry.id, duration: entry.duration };\n wf.__pushStepFlowEntry(live, live);\n return;\n }\n case 'sleepUntil': {\n if (!(entry.date instanceof Date) && typeof entry.date !== 'string') {\n throw new Error(`Stored sleepUntil \"${entry.id}\" missing literal date.`);\n }\n const date = entry.date instanceof Date ? entry.date : new Date(entry.date);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Stored sleepUntil \"${entry.id}\" has an unparseable date: ${String(entry.date)}`);\n }\n const live: StepFlowEntry = { type: 'sleepUntil', id: entry.id, date };\n wf.__pushStepFlowEntry(live, { type: 'sleepUntil', id: entry.id, date });\n return;\n }\n case 'parallel': {\n const live: StepFlowEntry = {\n type: 'parallel',\n steps: entry.steps.map(s => rehydrateSingleEntry(s, mastra, schemaOpts)),\n };\n wf.__pushStepFlowEntry(live, entry);\n return;\n }\n case 'foreach': {\n if (entry.step.type === 'mapping') {\n throw new Error(\n `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.`,\n );\n }\n const live: StepFlowEntry = {\n type: 'foreach',\n step: rehydrateSingleEntry(entry.step, mastra, schemaOpts),\n opts: { concurrency: entry.opts?.concurrency ?? 1 },\n };\n wf.__pushStepFlowEntry(live, entry);\n return;\n }\n case 'step': {\n const live = rehydrateSingleEntry(entry, mastra, schemaOpts);\n wf.__pushStepFlowEntry(live, entry);\n return;\n }\n case 'workflow': {\n const nested = assertWorkflowExists(mastra, entry.workflowId);\n // A nested workflow executes as its own `Workflow`, so the engine keys its\n // result by the workflow's intrinsic id. The portable definition addresses\n // it by the declared call-site id, which is what mappings, predicates and\n // `${stepResults...}` templates reference. Clone it under the declared id so\n // every reference resolves instead of silently falling back to `initData`.\n wf.then(entry.id && entry.id !== nested.id ? cloneWorkflow(nested as any, { id: entry.id }) : nested);\n return;\n }\n case 'conditional': {\n const predicates = entry.predicates;\n if (!predicates || predicates.length !== entry.steps.length || predicates.some(p => !p)) {\n throw new Error(\n `Cannot rehydrate conditional step: missing or mismatched predicates. Only declarative predicate branches round-trip.`,\n );\n }\n const steps = entry.steps.map(s => rehydrateSingleEntry(s, mastra, schemaOpts));\n // Wire graphs may omit the Studio-facing condition labels; derive them\n // from the predicates (same convention as the fluent builder).\n const serializedConditions =\n entry.serializedConditions ??\n steps.map((s, i) => ({ id: `${getSingleStepEntryId(s)}-condition`, fn: derivePredicateLabel(predicates[i]!) }));\n const live: StepFlowEntry = {\n type: 'conditional',\n steps,\n conditions: predicates.map(p => predicateToCondition(p!)),\n serializedConditions,\n predicates,\n };\n wf.__pushStepFlowEntry(live, { ...entry, serializedConditions });\n return;\n }\n case 'loop': {\n const { predicate, loopType } = entry;\n if (!predicate || (loopType !== 'dowhile' && loopType !== 'dountil')) {\n throw new Error(\n `Cannot rehydrate loop step: missing declarative predicate or loopType. Only declarative predicate loops round-trip.`,\n );\n }\n const step = rehydrateSingleEntry(entry.step, mastra, schemaOpts);\n const serializedCondition = entry.serializedCondition ?? {\n id: `${getSingleStepEntryId(step)}-condition`,\n fn: derivePredicateLabel(predicate),\n };\n const live: StepFlowEntry = {\n type: 'loop',\n step,\n condition: predicateToCondition(predicate),\n loopType,\n serializedCondition,\n predicate,\n };\n wf.__pushStepFlowEntry(live, { ...entry, serializedCondition });\n return;\n }\n default: {\n const _exhaustive: never = entry;\n throw new Error(`Unknown stored step type: ${JSON.stringify(_exhaustive)}`);\n }\n }\n}\n\n/**\n * Reconstruct the options bag `.agent()` accepts from a serialized entry.\n * Restores `structuredOutput.schema` from `outputSchema` (JSON Schema → Zod)\n * and merges in `retries` / `metadata`. Returns `undefined` when nothing to\n * restore so `.agent(agentId)` stays a clean call.\n */\nfunction rebuildAgentOptions(\n entry: {\n outputSchema?: Record<string, any>;\n options?: SerializedStepOptions;\n },\n schemaOpts?: JsonSchemaToZodOptions,\n): Record<string, any> | undefined {\n const opts: Record<string, any> = {};\n if (entry.outputSchema) {\n opts.structuredOutput = { schema: jsonSchemaToZod(entry.outputSchema, schemaOpts) };\n }\n if (entry.options?.retries !== undefined) opts.retries = entry.options.retries;\n if (entry.options?.metadata !== undefined) opts.metadata = entry.options.metadata;\n return Object.keys(opts).length > 0 ? opts : undefined;\n}\n\nfunction rebuildToolOptions(entry: { options?: SerializedStepOptions }): Record<string, any> | undefined {\n const opts: Record<string, any> = {};\n if (entry.options?.retries !== undefined) opts.retries = entry.options.retries;\n if (entry.options?.metadata !== undefined) opts.metadata = entry.options.metadata;\n return Object.keys(opts).length > 0 ? opts : undefined;\n}\n\n/**\n * Build the live `SingleStepEntry` for a stored entry. Declarative agent/tool\n * entries stay declarative — both engines interpret them per-kind at\n * execution time (`runAgentEntry` / `runToolEntry`) — so no fake `Step`\n * wrapper is needed and the stored `id` / `outputSchema` / `retries` /\n * `metadata` round-trip losslessly in every position (top-level, parallel,\n * branch, foreach and loop bodies).\n *\n * `step` descriptors resolve agent-then-tool by id against the live Mastra\n * instance; `workflow` entries resolve the registered instance. Both become\n * plain `{ type: 'step' }` entries, same as the fluent builder emits.\n */\nfunction rehydrateSingleEntry(\n entry: SerializedSingleStepEntry,\n mastra: Mastra,\n schemaOpts?: JsonSchemaToZodOptions,\n): SingleStepEntry {\n switch (entry.type) {\n case 'agent': {\n const agent = tryGetAgentById(mastra, entry.agentId);\n if (!agent) {\n throw new Error(\n `Stored workflow references agent \"${entry.agentId}\" which is not registered on this Mastra instance.`,\n );\n }\n return {\n type: 'agent',\n id: entry.id,\n agentId: entry.agentId,\n agent,\n options: rebuildAgentOptions(entry, schemaOpts),\n };\n }\n case 'tool': {\n const tool = mastra.getTool?.(entry.toolId);\n if (!tool) {\n throw new Error(\n `Stored workflow references tool \"${entry.toolId}\" which is not registered on this Mastra instance.`,\n );\n }\n return { type: 'tool', id: entry.id, toolId: entry.toolId, tool, options: rebuildToolOptions(entry) };\n }\n case 'step': {\n const { id } = entry.step;\n // Wrap the resolved agent/tool in a real Step (same adapters `createStep`\n // uses) so the entry honors the executeStep contract instead of casting a\n // raw Agent/Tool instance — those don't carry a step-shaped `execute`.\n const agent = tryGetAgentById(mastra, id);\n if (agent) {\n return { type: 'step', step: createStepFromAgent(agent) as unknown as Step };\n }\n const tool = tryGetToolById(mastra, id);\n if (tool) {\n return { type: 'step', step: createStepFromTool(tool as any) as unknown as Step };\n }\n throw new Error(\n `Stored workflow references step \"${id}\" which is not registered as an agent or tool on this Mastra instance.`,\n );\n }\n case 'workflow': {\n const nested = assertWorkflowExists(mastra, entry.workflowId);\n // Same call-site identity rule as top-level nested workflows: run the\n // clone under the declared id so results are keyed the way the portable\n // definition addresses them.\n const step = entry.id && entry.id !== nested.id ? cloneWorkflow(nested as any, { id: entry.id }) : nested;\n return { type: 'step', step: step as unknown as Step };\n }\n case 'mapping':\n throw new Error(\n `mapping entries cannot appear inside .parallel(), .branch(), or .foreach(); they must be top-level.`,\n );\n }\n}\n\n/**\n * Rebuild the object shape that `.map()` accepts. Step sources remain workflow-local\n * step IDs because mapping execution resolves them from the run's step results.\n */\nfunction rehydrateMapConfig(cfg: Record<string, any>, mastra: Mastra): Record<string, any> {\n const out: Record<string, any> = {};\n for (const [key, source] of Object.entries(cfg)) {\n if (!source || typeof source !== 'object') {\n out[key] = source;\n continue;\n }\n if ('template' in source) {\n out[key] = { template: source.template };\n } else if ('value' in source) {\n out[key] = { value: source.value };\n } else if ('requestContextPath' in source) {\n out[key] = { requestContextPath: source.requestContextPath };\n } else if ('initData' in source && typeof source.initData === 'string') {\n const wf = mastra.getWorkflow?.(source.initData);\n if (!wf) {\n throw new Error(`Mapping references unknown workflow init-data \"${source.initData}\".`);\n }\n out[key] = mapVariable({ initData: wf as any, path: source.path });\n } else if ('step' in source) {\n out[key] = mapVariable({ step: source.step as any, path: source.path });\n } else {\n out[key] = source;\n }\n }\n return out;\n}\n\n/**\n * Mastra.getAgentById throws when the id isn't registered; every by-id\n * resolution path in this file wants a nullable \"does it exist?\" answer so it\n * can fall through to a tool lookup or a targeted error. Swallow the not-found\n * throw and return undefined.\n */\nfunction tryGetAgentById(mastra: Mastra, id: string): any | undefined {\n if (!id || typeof mastra.getAgentById !== 'function') return undefined;\n try {\n return mastra.getAgentById(id);\n } catch {\n return undefined;\n }\n}\n\n/** Same nullable-lookup contract as `tryGetAgentById`, for tools — `Mastra.getTool` throws on a missing id. */\nfunction tryGetToolById(mastra: Mastra, id: string): any | undefined {\n if (!id || typeof mastra.getTool !== 'function') return undefined;\n try {\n return mastra.getTool(id);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Workflow references resolve like agent references: intrinsic workflow id\n * first (`getWorkflowById` scans registered workflows by their own `id`),\n * falling back to the registration key. Stored definitions reference the\n * intrinsic id — the identity discovery advertises — which may differ from\n * the key the workflow was registered under (`workflows: { greetingWorkflow }`\n * vs `id: 'greeting-workflow'`).\n */\nfunction tryGetWorkflowById(mastra: Mastra, id: string): any | undefined {\n if (!id) return undefined;\n if (typeof (mastra as any).getWorkflowById === 'function') {\n try {\n return (mastra as any).getWorkflowById(id);\n } catch {\n // fall through to registration-key lookup\n }\n }\n if (typeof (mastra as any).getWorkflow !== 'function') return undefined;\n try {\n return (mastra as any).getWorkflow(id);\n } catch {\n return undefined;\n }\n}\n\nfunction assertWorkflowExists(mastra: Mastra, workflowId: string): any {\n const wf = tryGetWorkflowById(mastra, workflowId);\n if (!wf) {\n throw new Error(\n `Stored workflow references nested workflow \"${workflowId}\" which is not registered on this Mastra instance.`,\n );\n }\n return wf;\n}\n","/**\n * Shared typed walker over a serialized workflow graph. Every consumer that\n * needs \"all the leaf entries in this graph\" (schema validation, reference\n * validation, nested-workflow dependency collection) goes through this one\n * function, so recursion into container entries lives in exactly one place\n * and is exhaustiveness-checked against `SerializedStepFlowEntry`.\n */\nimport type { SerializedSingleStepEntry, SerializedStepFlowEntry } from '../types';\nimport type { ValidatableStepFlowEntry } from './validate/types';\n\n/**\n * Invoke `visit` for every single-step (leaf) entry in the graph, recursing\n * into `parallel`/`conditional` children and `loop`/`foreach` bodies.\n *\n * Does NOT recurse into a nested workflow's inlined `serializedStepFlow` —\n * a nested workflow's own graph is validated when that workflow is added.\n * `sleep`/`sleepUntil` entries carry no references or schemas and are skipped.\n */\nexport function forEachSingleStepEntry(\n entries: readonly SerializedStepFlowEntry[],\n visit: (entry: SerializedSingleStepEntry) => void,\n): void {\n for (const entry of entries) {\n switch (entry.type) {\n case 'step':\n case 'agent':\n case 'tool':\n case 'mapping':\n case 'workflow':\n visit(entry);\n break;\n case 'parallel':\n case 'conditional':\n entry.steps.forEach(visit);\n break;\n case 'loop':\n case 'foreach':\n visit(entry.step);\n break;\n case 'sleep':\n case 'sleepUntil':\n break;\n default: {\n const _exhaustive: never = entry;\n void _exhaustive;\n }\n }\n }\n}\n\n/**\n * Collect the ids of every nested workflow referenced by a stored graph.\n * Used by boot-time loading to hydrate stored definitions in dependency order.\n */\nexport function collectNestedWorkflowIds(graph: readonly SerializedStepFlowEntry[]): Set<string> {\n const out = new Set<string>();\n forEachSingleStepEntry(graph, entry => {\n if (entry.type === 'workflow') out.add(entry.workflowId);\n });\n return out;\n}\n\n/**\n * Same traversal as {@link forEachSingleStepEntry} but reports each leaf's\n * position as a dotted path (`graph.2`, `graph.2.steps.0`, `graph.2.step`) —\n * the path contract shared by validation issues and the Studio draft UI.\n *\n * Accepts the wider {@link ValidatableStepFlowEntry} union so both persisted\n * graphs and wire-shaped authoring submissions can be walked.\n */\nexport function forEachSingleStepEntryWithPath(\n entries: readonly ValidatableStepFlowEntry[],\n visit: (entry: SerializedSingleStepEntry, path: string) => void,\n): void {\n entries.forEach((entry, index) => {\n const path = `graph.${index}`;\n switch (entry.type) {\n case 'step':\n case 'agent':\n case 'tool':\n case 'mapping':\n case 'workflow':\n visit(entry, path);\n break;\n case 'parallel':\n case 'conditional':\n entry.steps.forEach((child, childIndex) => visit(child, `${path}.steps.${childIndex}`));\n break;\n case 'loop':\n case 'foreach':\n visit(entry.step, `${path}.step`);\n break;\n case 'sleep':\n case 'sleepUntil':\n break;\n default: {\n const _exhaustive: never = entry;\n void _exhaustive;\n }\n }\n });\n}\n","/**\n * Reference checks against a caller-supplied registry index.\n *\n * Checks are gated per kind: a kind whose key is absent from the index is\n * skipped entirely, so callers that cannot enumerate (say) workflows never\n * produce false missing-reference issues. Mis-classified references get swap\n * hints (agent id that is actually a registered tool, and vice versa).\n *\n * `type: 'step'` descriptors are intentionally not checked — they resolve\n * late against the live Mastra instance at rehydration time.\n */\nimport { forEachSingleStepEntryWithPath } from '../graph';\nimport type { WorkflowRegistryIndex, WorkflowValidationInput, WorkflowValidationIssue } from './types';\n\nexport function validateWorkflowRefs(\n def: WorkflowValidationInput,\n index: WorkflowRegistryIndex,\n): WorkflowValidationIssue[] {\n const issues: WorkflowValidationIssue[] = [];\n forEachSingleStepEntryWithPath(def.graph, (entry, path) => {\n switch (entry.type) {\n case 'agent': {\n if (!index.agents || index.agents[entry.agentId]) return;\n issues.push({\n code: 'missing-reference',\n path: `${path}.agentId`,\n message: index.tools?.[entry.agentId]\n ? `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}\" }.`\n : `Step \"${entry.id}\" declares agentId \"${entry.agentId}\" which is not a registered agent.`,\n });\n return;\n }\n case 'tool': {\n if (!index.tools || index.tools[entry.toolId]) return;\n issues.push({\n code: 'missing-reference',\n path: `${path}.toolId`,\n message: index.agents?.[entry.toolId]\n ? `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}\" }.`\n : `Step \"${entry.id}\" declares toolId \"${entry.toolId}\" which is not a registered tool.`,\n });\n return;\n }\n case 'workflow': {\n // Self-references are a structural issue (`self-reference`), and the\n // registry may well contain a previous version of this very workflow\n // on upsert — skip the existence check for them.\n if (entry.workflowId === def.id) return;\n if (!index.workflows || index.workflows[entry.workflowId]) return;\n issues.push({\n code: 'missing-reference',\n path: `${path}.workflowId`,\n message: `Step \"${entry.id}\" declares workflowId \"${entry.workflowId}\" which is not a registered workflow.`,\n });\n return;\n }\n default:\n return;\n }\n });\n return issues;\n}\n","/**\n * Shared vocabulary for the one stored-workflow validation domain.\n *\n * Every validation surface (Mastra save path, builder preflight, Studio draft\n * UI) speaks in `WorkflowValidationIssue`s produced by the collect-mode core\n * in `./index`. Throwing behavior is a presentation concern layered on top\n * (`assertValidStoredWorkflow`), not a separate rule set.\n */\nimport type { Predicate } from '../../predicate';\nimport type { SerializedSingleStepEntry, SerializedStepFlowEntry } from '../../types';\nimport type { JsonSchema } from '../json-schema-to-zod';\n\nexport type WorkflowValidationIssueCode =\n | 'empty-graph'\n | 'missing-step-id'\n | 'duplicate-step-id'\n | 'missing-reference'\n | 'invalid-nested-workflow-id'\n | 'invalid-map-config'\n | 'invalid-map-reference'\n | 'invalid-map-placement'\n | 'invalid-parallel'\n | 'invalid-foreach'\n | 'invalid-conditional'\n | 'invalid-loop'\n | 'invalid-predicate-reference'\n | 'incompatible-schema'\n | 'unsupported-schema-keyword'\n | 'self-reference';\n\nexport interface WorkflowValidationRepairSource {\n source: { initData: true; path: string } | { step: string; path: string };\n schema?: JsonSchema;\n compatibility: 'compatible' | 'incompatible' | 'unknown';\n}\n\nexport interface WorkflowValidationRepairAction {\n issueCode: WorkflowValidationIssueCode;\n path: string;\n entryId?: string;\n containerId?: string;\n childId?: string;\n destinationField?: string;\n expectedSchema?: JsonSchema;\n actualSchema?: JsonSchema;\n legalSources?: WorkflowValidationRepairSource[];\n operation:\n | 'insert-workflow-mapping-before'\n | 'insert-workflow-mapping-after'\n | 'set-workflow-mapping-source'\n | 'set-workflow-predicate'\n | 'update-workflow-step'\n | 'remove-workflow-step';\n arguments: Record<string, string | number | boolean>;\n blocksCheckpoint: boolean;\n blocksFinalize: boolean;\n}\n\nexport interface WorkflowValidationIssue {\n code: WorkflowValidationIssueCode;\n path: string;\n message: string;\n repair?: WorkflowValidationRepairAction;\n}\n\n/** Input/output shapes known for one registered dependency. */\nexport interface WorkflowRegistrySchemas {\n inputSchema?: JsonSchema;\n outputSchema?: JsonSchema;\n}\n\n/**\n * What the validator knows about the surrounding registries. Presence of a\n * top-level key means \"this kind was indexed, check references against it\";\n * an absent key skips reference checks for that kind (a caller that cannot\n * enumerate, say, workflows must not produce false missing-reference issues).\n * Schemas are optional per entry — when present they power schema-flow\n * analysis, when absent compatibility degrades to `unknown` (never a false\n * incompatibility).\n */\nexport interface WorkflowRegistryIndex {\n agents?: Record<string, WorkflowRegistrySchemas>;\n tools?: Record<string, WorkflowRegistrySchemas>;\n workflows?: Record<string, WorkflowRegistrySchemas>;\n}\n\n/**\n * The graph-entry union validation accepts: the canonical serialized union,\n * widened only where the wire legitimately diverges from the fluent\n * serializer's output —\n * - `sleepUntil.date` arrives as an ISO string over HTTP (Date at runtime)\n * - `serializedConditions` / `serializedCondition` are fluent-builder debug\n * labels; clients don't send them (rehydration derives them)\n *\n * `SerializedStepFlowEntry` is assignable to this union, and so is the\n * authoring subset (`WorkflowBuilderGraphEntry`) — asserted statically in\n * `workflows/builder`.\n */\nexport type ValidatableStepFlowEntry =\n | SerializedSingleStepEntry\n | Extract<SerializedStepFlowEntry, { type: 'sleep' }>\n | (Omit<Extract<SerializedStepFlowEntry, { type: 'sleepUntil' }>, 'date'> & { date?: Date | string })\n | Extract<SerializedStepFlowEntry, { type: 'parallel' }>\n | (Omit<Extract<SerializedStepFlowEntry, { type: 'conditional' }>, 'serializedConditions'> & {\n serializedConditions?: { id: string; fn: string }[];\n })\n | (Omit<Extract<SerializedStepFlowEntry, { type: 'loop' }>, 'serializedCondition'> & {\n serializedCondition?: { id: string; fn: string };\n predicate?: Predicate;\n })\n | Extract<SerializedStepFlowEntry, { type: 'foreach' }>;\n\n/**\n * The definition shape validation operates on — the common structural core of\n * `StoredWorkflowGraph` (persistence) and `WorkflowBuilderDefinition`\n * (authoring wire shape).\n */\nexport interface WorkflowValidationInput {\n id: string;\n description?: string;\n inputSchema: JsonSchema;\n outputS