Skip to Content
DocsWorkflowsControl Flow

Control Flow

When you build a workflow, you typically break down operations into smaller tasks that can be linked and reused. Steps provide a structured way to manage these tasks by defining inputs, outputs, and execution logic.

  • If the schemas match, the outputSchema from each step is automatically passed to the inputSchema of the next step.
  • If the schemas don’t match, use Input data mapping to transform the outputSchema into the expected inputSchema.

Chaining steps with .then()

Chain steps to execute sequentially using .then():

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const step1 = createStep({...}); const step2 = createStep({...}); export const testWorkflow = createWorkflow({...}) .then(step1) .then(step2) .commit();

This does what you’d expect: it executes step1, then it executes step2.

Parallel steps with .parallel():

Execute steps in parallel using .parallel():

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const step1 = createStep({...}); const step2 = createStep({...}); const step3 = createStep({...}); export const testWorkflow = createWorkflow({...}) .parallel([step1, step2]) .then(step3) .commit();

This executes step1 and step2 concurrently, then continues to step3 after both complete.

See Parallel Execution with Steps for more information.

Conditional branching (.branch())

Create conditional branches using .branch():

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const lessThanStep = createStep({...}); const greaterThanStep = createStep({...}); export const testWorkflow = createWorkflow({...}) .branch([ [async ({ inputData: { value } }) => (value < 9), lessThanStep], [async ({ inputData: { value } }) => (value >= 9), greaterThanStep] ]) .commit();

Branch conditions are evaluated sequentially, but steps with matching conditions are executed in parallel.

See Workflow with Conditional Branching for more information.

Looping commands

Workflows support two types of loops. When looping a step, or any step-compatible construct like a nested workflow, the initial inputData is sourced from the output of the previous step.

To ensure compatibility, the loop’s initial input must either match the shape of the previous step’s output, or be explicitly transformed using the map function.

.dowhile()

Executes a step repeatedly while a condition is true.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const counterStep = createStep({...}); export const testWorkflow = createWorkflow({...}) .dowhile(counterStep, async ({ inputData: { number } }) => number < 10) .commit();

.dountil()

Executes a step repeatedly until a condition becomes true.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const counterStep = createStep({...}); export const testWorkflow = createWorkflow({...}) .dountil(counterStep, async ({ inputData: { number } }) => number > 10) .commit();

.foreach()

Sequentially executes the same step for each item from the inputSchema.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const mapStep = createStep({...}); export const testWorkflow = createWorkflow({...}) .foreach(mapStep) .commit();

Early exit with .bail()

You can bail out of a workflow execution successfully by calling bail() in a step. This returns whatever payload is passed to the bail() function as the result of the workflow.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const step1 = createStep({ id: 'step1', execute: async ({ bail, inputData }) => { return bail({ result: 'bailed' }); }, inputSchema: z.object({ value: z.string() }), outputSchema: z.object({ result: z.string() }), }); export const testWorkflow = createWorkflow({...}) .then(step1) .commit();

Unsuccessful bails happen through throwing an error in the step.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const step1 = createStep({ id: 'step1', execute: async ({ bail, inputData }) => { throw new Error('bailed'); }, inputSchema: z.object({ value: z.string() }), outputSchema: z.object({ result: z.string() }), }); export const testWorkflow = createWorkflow({...}) .then(step1) .commit();

Example Run Instance

The following example demonstrates how to start a run with multiple inputs. Each input will pass through the mapStep sequentially.

src/test-workflow.ts
import { mastra } from "./mastra"; const run = await mastra.getWorkflow("testWorkflow").createRunAsync(); const result = await run.start({ inputData: [{ number: 10 }, { number: 100 }, { number: 200 }] });

To execute this run from your terminal:

npx tsx src/test-workflow.ts

Concurrency

Optionally, using concurrency allows you to execute steps in parallel with a limit on the number of concurrent executions.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const mapStep = createStep({...}) export const testWorkflow = createWorkflow({...}) .foreach(mapStep, { concurrency: 2 }) .commit();

Using a workflow as a step

For greater composability, you can re-use a workflow as a step in another workflow.

In the example below, nestedWorkflow is used as a step within testWorkflow. The testWorkflow uses step1 while the nestedWorkflow composes step2 and step3:

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; export const nestedWorkflow = createWorkflow({...}) export const testWorkflow = createWorkflow({...}) .then(nestedWorkflow) .commit();

When using .branch() or .parallel() to build more complex control flows, executing more than one step requires wrapping those steps in a nested workflow, along with a clear definition of how they should be executed.

Running nested workflows in parallel

Workflows themselves can also be executed in parallel.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep } from "@mastra/core/workflows"; import { z } from "zod"; const workflow1 = createWorkflow({...}); const workflow2 = createWorkflow({...}); export const testWorkflow = createWorkflow({...}) .parallel([workflow1, workflow2]) .commit();

Parallel steps receive previous step results as input. Their outputs are passed into the next step input as an object where the key is the step id and the value is the step output.

Cloning workflows

In the example below, clonedWorkflow is a clone of workflow1 and is used as a step within testWorkflow. The clonedWorkflow is run sequentially after step1.

src/mastra/workflows/test-workflow.ts
import { createWorkflow, createStep, cloneWorkflow } from "@mastra/core/workflows"; import { z } from "zod"; const step1 = createStep({...}); const clonedWorkflow = cloneWorkflow(step1, { id: "cloned-workflow" }); export const testWorkflow = createWorkflow({...}) .then(step1) .then(clonedWorkflow) .commit();