DocsWorkflowsData Flow

Data Flow Between Steps

In Mastra workflows, passing data between steps is essential for creating complex, interconnected processes. This guide explains the different methods for accessing data from previous steps and ensuring proper type safety throughout your workflow.

Overview of Data Flow Methods

Mastra provides several ways to pass data between steps:

  1. Context Object - Access step results directly through the context object
  2. Variable Mapping - Explicitly map outputs from one step to inputs of another
  3. getStepResult Method - Type-safe method to retrieve step outputs

Each approach has its advantages depending on your use case and requirements for type safety.

Using getStepResult Method

The getStepResult method provides a type-safe way to access step results. This is the recommended approach when working with TypeScript as it preserves type information.

Basic Usage

For better type safety, you can provide a type parameter to getStepResult:

src/mastra/workflows/get-step-result.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
const fetchUserStep = new Step({
  id: 'fetchUser',
  outputSchema: z.object({
    name: z.string(),
    userId: z.string(),
  }),
  execute: async ({ context }) => {
    return { name: 'John Doe', userId: '123' };
  },
});
 
const analyzeDataStep = new Step({
  id: "analyzeData",
  execute: async ({ context }) => {
    // Type-safe access to previous step result
    const userData = context.getStepResult<{ name: string, userId: string }>("fetchUser");
 
    if (!userData) {
      return { status: "error", message: "User data not found" };
    }
 
    return {
      analysis: `Analyzed data for user ${userData.name}`,
      userId: userData.userId
    };
  },
});

Using Step References

The most type-safe approach is to reference the step directly in the getStepResult call:

src/mastra/workflows/step-reference.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
// Define step with output schema
const fetchUserStep = new Step({
  id: "fetchUser",
  outputSchema: z.object({
    userId: z.string(),
    name: z.string(),
    email: z.string(),
  }),
  execute: async () => {
    return {
      userId: "user123",
      name: "John Doe",
      email: "john@example.com"
    };
  },
});
 
const processUserStep = new Step({
  id: "processUser",
  execute: async ({ context }) => {
    // TypeScript will infer the correct type from fetchUserStep's outputSchema
    const userData = context.getStepResult(fetchUserStep);
 
    return {
      processed: true,
      userName: userData?.name
    };
  },
});
 
const workflow = new Workflow({
  name: "user-workflow",
});
 
workflow
  .step(fetchUserStep)
  .then(processUserStep)
  .commit();

Using Variable Mapping

Variable mapping is an explicit way to define data flow between steps. This approach makes dependencies clear and provides good type safety.

src/mastra/workflows/variable-mapping.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
const fetchUserStep = new Step({
  id: "fetchUser",
  outputSchema: z.object({
    userId: z.string(),
    name: z.string(),
    email: z.string(),
  }),
  execute: async () => {
    return {
      userId: "user123",
      name: "John Doe",
      email: "john@example.com"
    };
  },
});
 
const sendEmailStep = new Step({
  id: "sendEmail",
  inputSchema: z.object({
    recipientEmail: z.string(),
    recipientName: z.string(),
  }),
  execute: async ({ context }) => {
    const { recipientEmail, recipientName } = context;
 
    // Send email logic here
    return {
      status: "sent",
      to: recipientEmail
    };
  },
});
 
const workflow = new Workflow({
  name: "email-workflow",
});
 
workflow
  .step(fetchUserStep)
  .then(sendEmailStep, {
    variables: {
      // Map specific fields from fetchUser to sendEmail inputs
      recipientEmail: { step: fetchUserStep, path: 'email' },
      recipientName: { step: fetchUserStep, path: 'name' }
    }
  })
  .commit();

For more details on variable mapping, see the Data Mapping with Workflow Variables documentation.

Using the Context Object

The context object provides direct access to all step results and their outputs. This approach is more flexible but requires careful handling to maintain type safety. You can access step results directly through the context.steps object:

src/mastra/workflows/context-access.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
const processOrderStep = new Step({
  id: 'processOrder',
  execute: async ({ context }) => {
    // Access data from a previous step
    let userData: { name: string, userId: string };
    if (context.steps['fetchUser']?.status === 'success') {
      userData = context.steps.fetchUser.output;
    } else {
      throw new Error('User data not found');
    }
 
    return {
      orderId: 'order123',
      userId: userData.userId,
      status: 'processing',
    };
  },
});
 
const workflow = new Workflow({
  name: "order-workflow",
});
 
workflow
  .step(fetchUserStep)
  .then(processOrderStep)
  .commit();

Workflow-Level Type Safety

For comprehensive type safety across your entire workflow, you can define types for all steps and pass them to the Workflow This allows you to get type safety for the context object on conditions, and step results on the final workflow output.

src/mastra/workflows/workflow-typing.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
 
// Create steps with typed outputs
const fetchUserStep = new Step({
  id: "fetchUser",
  outputSchema: z.object({
    userId: z.string(),
    name: z.string(),
    email: z.string(),
  }),
  execute: async () => {
    return {
      userId: "user123",
      name: "John Doe",
      email: "john@example.com"
    };
  },
});
 
const processOrderStep = new Step({
  id: "processOrder",
  execute: async ({ context }) => {
    // TypeScript knows the shape of userData
    const userData = context.getStepResult(fetchUserStep);
 
    return {
      orderId: "order123",
      status: "processing"
    };
  },
});
 
const workflow = new Workflow<[typeof fetchUserStep, typeof processOrderStep]>({
  name: "typed-workflow",
});
 
workflow
  .step(fetchUserStep)
  .then(processOrderStep)
  .until(async ({ context }) => {
    // TypeScript knows the shape of userData here
    const res = context.getStepResult('fetchUser');
    return res?.userId === '123';
  }, processOrderStep)
  .commit();

Accessing Trigger Data

In addition to step results, you can access the original trigger data that started the workflow:

src/mastra/workflows/trigger-data.ts
import { Step, Workflow } from "@mastra/core/workflows";
import { z } from "zod";
 
// Define trigger schema
const triggerSchema = z.object({
  customerId: z.string(),
  orderItems: z.array(z.string()),
});
 
type TriggerType = z.infer<typeof triggerSchema>;
 
const processOrderStep = new Step({
  id: "processOrder",
  execute: async ({ context }) => {
    // Access trigger data with type safety
    const triggerData = context.getStepResult<TriggerType>('trigger');
 
    return {
      customerId: triggerData?.customerId,
      itemCount: triggerData?.orderItems.length || 0,
      status: "processing"
    };
  },
});
 
const workflow = new Workflow({
  name: "order-workflow",
  triggerSchema,
});
 
workflow
  .step(processOrderStep)
  .commit();

Accessing Workflow Results

You can get typed access to the results of a workflow by injecting the step types into the Workflow type params:

src/mastra/workflows/get-results.ts
import { Workflow } from "@mastra/core/workflows";
 
const fetchUserStep = new Step({
  id: "fetchUser",
  outputSchema: z.object({
    userId: z.string(),
    name: z.string(),
    email: z.string(),
  }),
  execute: async () => {
    return {
      userId: "user123",
      name: "John Doe",
      email: "john@example.com"
    };
  },
});
 
const processOrderStep = new Step({
  id: "processOrder",
  outputSchema: z.object({
    orderId: z.string(),
    status: z.string(),
  }),
  execute: async ({ context }) => {
    const userData = context.getStepResult(fetchUserStep);
    return {
      orderId: "order123",
      status: "processing"
    };
  },
});
 
const workflow = new Workflow<[typeof fetchUserStep, typeof processOrderStep]>({
  name: "typed-workflow",
});
 
workflow
  .step(fetchUserStep)
  .then(processOrderStep)
  .commit();
 
const run = workflow.createRun();
const result = await run.start();
 
// The result is a discriminated union of the step results
// So it needs to be narrowed down via status checks
if (result.results.processOrder.status === 'success') {
  // TypeScript will know the shape of the results
  const orderId = result.results.processOrder.output.orderId;
  console.log({orderId});
}
 
if (result.results.fetchUser.status === 'success') {
  const userId = result.results.fetchUser.output.userId;
  console.log({userId});
}

Best Practices for Data Flow

  1. Use getStepResult with Step References for Type Safety

    • Ensures TypeScript can infer the correct types
    • Catches type errors at compile time
  2. *Use Variable Mapping for Explicit Dependencies

    • Makes data flow clear and maintainable
    • Provides good documentation of step dependencies
  3. Define Output Schemas for Steps

    • Validates data at runtime
      • Validates return type of the execute function
    • Improves type inference in TypeScript
  4. Handle Missing Data Gracefully

    • Always check if step results exist before accessing properties
    • Provide fallback values for optional data
  5. Keep Data Transformations Simple

    • Transform data in dedicated steps rather than in variable mappings
    • Makes workflows easier to test and debug

Comparison of Data Flow Methods

MethodType SafetyExplicitnessUse Case
getStepResultHighestHighComplex workflows with strict typing requirements
Variable MappingHighHighWhen dependencies need to be clear and explicit
context.stepsMediumLowQuick access to step data in simple workflows

By choosing the right data flow method for your use case, you can create workflows that are both type-safe and maintainable.