Skip to main content

Stored workflow definition

beta

Stored workflows are in beta. Breaking changes may occur without a major version bump until the API is stable.

A stored workflow definition is a JSON-compatible StoredWorkflowGraph accepted by Mastra.addStoredWorkflow(), the stored-workflow server routes, and the Client SDK workflows API.

See Stored workflows for a complete setup and usage example.

Definition fields
Direct link to Definition fields

FieldTypeRequiredDescription
idstringYesUnique workflow ID. This is also the ID used to retrieve and run the workflow.
descriptionstringNoHuman-readable description
inputSchemaJsonSchemaYesJSON Schema for the workflow input
outputSchemaJsonSchemaYesJSON Schema for the workflow output
stateSchemaJsonSchemaNoJSON Schema for shared workflow state
requestContextSchemaJsonSchemaNoJSON Schema for values read from the request context
metadataRecord<string, unknown>NoArbitrary JSON metadata preserved through storage
graphSerializedStepFlowEntry[]YesStep entries that make up the workflow

Schemas use JSON Schema rather than Zod so the definition can round-trip through JSON. Mastra converts each schema to Zod when it registers the workflow.

{
"id": "greeting-workflow",
"description": "Returns a greeting for the supplied name",
"inputSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
},
"outputSchema": {
"type": "object",
"properties": { "message": { "type": "string" } },
"required": ["message"]
},
"graph": [
{
"type": "mapping",
"id": "create-greeting",
"mapConfig": "{\"message\":{\"template\":\"Hello, ${initData.name}!\"}}"
}
]
}

Graph entries
Direct link to Graph entries

Entries in the graph run in order. Each entry receives the previous entry's output, and the first entry receives the workflow input.

Entry typeDescription
agentInvoke a registered agent
toolInvoke a registered tool
mappingReshape data between steps
workflowInvoke a registered workflow as a nested step
parallelRun several steps concurrently and merge their outputs
conditionalRun every branch whose predicate is true, concurrently
foreachRun one step per item of an array input
loopRepeat a step while or until a predicate holds
sleepPause for a fixed duration
sleepUntilPause until a fixed date

Code-defined workflows that use .agent() and .tool() produce the same declarative entries when serialized.

Agent steps
Direct link to Agent steps

An agent entry invokes a registered agent by ID. Agent steps accept { prompt: string } as input and return { text: string } by default.

{
"type": "agent",
"id": "summarize",
"agentId": "support-agent"
}

The id identifies this call site within the workflow. Later steps address the result as stepResults.summarize, regardless of the agent's own ID.

Add an outputSchema to request structured output from the agent:

{
"type": "agent",
"id": "extract-subtopics",
"agentId": "support-agent",
"outputSchema": {
"type": "array",
"items": {
"type": "object",
"properties": { "title": { "type": "string" } },
"required": ["title"]
}
}
}

Use a mapping entry before an agent to build its { prompt } input from workflow data.

Agent entries accept an optional description and an options object:

{
"type": "agent",
"id": "summarize",
"agentId": "support-agent",
"description": "Summarize the incoming request",
"options": { "retries": 2, "metadata": { "team": "support" } }
}

Only retries and metadata persist. Function-valued options such as onFinish and function-valued toolChoice are rejected when a code-defined workflow is stored. Other agent call options don't persist.

Tool steps
Direct link to Tool steps

A tool entry invokes a tool by its registration key from the Mastra tools object. Mastra resolves the tool's input and output schemas from the registry when it registers the workflow.

{
"type": "tool",
"id": "lookup",
"toolId": "lookup-customer"
}

Tool entries accept the same optional description and options fields as agent entries. Only retries and metadata persist.

Mapping steps
Direct link to Mapping steps

A mapping entry reshapes data. Its mapConfig is a JSON string that encodes an object. Each key becomes a key in the step output, and each descriptor defines one source.

DescriptorDescription
{ "value": ... }A constant JSON value
{ "template": "..." }A string built from ${...} placeholders
{ "initData": true, "path": "a.b" }A value from the workflow input
{ "step": "step-id", "path": "a.b" }A value from a preceding step's output
{ "requestContextPath": "a.b" }A value from the request context

The step source also accepts an array of step IDs:

{ "step": ["escalate", "auto-reply"], "path": "text" }

The first listed step with a non-empty result supplies the value. This can select the branch that ran after a conditional entry.

Templates resolve placeholders against initData, inputData, state, requestContext, and stepResults.<step-id>:

{
"type": "mapping",
"id": "build-prompt",
"mapConfig": "{\"prompt\":{\"template\":\"Summarize this request: ${initData.request}\"}}"
}

Objects and arrays resolved by a template are stringified as JSON. A null value inside a present result renders as an empty string. A template that references a step without a successful output fails the run.

Mapping entries must be top-level graph entries. They can't be placed inside parallel, conditional, foreach, or loop containers.

Nested workflow steps
Direct link to Nested workflow steps

A workflow entry invokes another registered workflow. The target can be code-defined or stored.

{
"type": "workflow",
"id": "lookup-first",
"workflowId": "lookup-customer-workflow"
}

The id identifies the call site. The same nested workflow can appear several times under different call-site IDs, and later steps address each result as stepResults.<id>. A workflow entry also accepts an optional description.

Parallel entries
Direct link to Parallel entries

A parallel entry runs several single steps concurrently and merges their outputs into an object keyed by step ID.

{
"type": "parallel",
"steps": [
{ "type": "tool", "id": "first", "toolId": "lookup-customer" },
{ "type": "tool", "id": "second", "toolId": "lookup-customer" }
]
}

Each child must be an agent, tool, or workflow entry. All children receive the parallel entry's input directly.

Conditional entries
Direct link to Conditional entries

A conditional entry pairs each step with a declarative predicate and runs every branch whose predicate is true.

{
"type": "conditional",
"steps": [
{ "type": "agent", "id": "escalate", "agentId": "support-agent" },
{ "type": "agent", "id": "auto-reply", "agentId": "support-agent" }
],
"predicates": [
{ "op": "eq", "left": { "path": "inputData.priority" }, "right": { "literal": "urgent" } },
{ "op": "ne", "left": { "path": "inputData.priority" }, "right": { "literal": "urgent" } }
]
}

Each child must be an agent, tool, or workflow entry, and each child needs a predicate. All children receive the conditional entry's input directly.

Predicates
Direct link to Predicates

Conditional entries and loops use a JSON predicate DSL. Operands are { "path": "..." } references or { "literal": ... } values. Paths resolve against initData, inputData, stepResults, and state.

OperatorShape
eq, ne, lt, lte, gt, gte{ "op": "eq", "left": ..., "right": ... }
in, notIn{ "op": "in", "value": ..., "set": [...] }
exists, notExists{ "op": "exists", "path": "..." }
truthy, falsy{ "op": "truthy", "value": ... }
and, or{ "op": "and", "args": [...] }
not{ "op": "not", "arg": ... }

Missing paths don't throw. Path-based operators return false when the path can't be resolved. Use exists or notExists to distinguish a missing value from a falsy value.

Foreach entries
Direct link to Foreach entries

A foreach entry runs its body once for each item in an array input. The preceding entry must produce a raw array. Results preserve input order, and concurrency defaults to 1.

{
"type": "foreach",
"step": { "type": "workflow", "id": "write-blurb", "workflowId": "blurb-workflow" },
"opts": { "concurrency": 3 }
}

The body can be an agent, tool, or workflow entry, but not a mapping entry.

Loop entries
Direct link to Loop entries

A loop repeats one step while (dowhile) or until (dountil) a predicate holds.

{
"type": "loop",
"loopType": "dountil",
"step": { "type": "tool", "id": "poll", "toolId": "check-status" },
"predicate": {
"op": "eq",
"left": { "path": "inputData.status" },
"right": { "literal": "done" }
}
}

The loop body must be a single step, and stored loops require a declarative predicate.

Sleep entries
Direct link to Sleep entries

A sleep entry pauses for a fixed number of milliseconds. A sleepUntil entry pauses until a fixed date represented by an ISO date string. Stored definitions require literal values.

{ "type": "sleep", "id": "wait", "duration": 5000 }
{ "type": "sleepUntil", "id": "wait-for-launch", "date": "2027-01-01T00:00:00.000Z" }

Use a code-defined workflow when the duration or date must be calculated at runtime.

Validation
Direct link to Validation

Mastra validates definitions before it persists or registers them:

  • Structure: Entry shapes and required fields, including placement rules such as top-level-only mappings.
  • References: Each agentId and workflowId must resolve against the live registries or the same bundle. A toolId must match a tool registration key.
  • Schema flow: Each entry's input must be compatible with the preceding output, including inferred mapping outputs.

Validation errors include a dotted path, such as graph.2.steps.0, that identifies the invalid entry.