Skip to main content

Data Mapping with Workflow Variables

Workflow variables in Mastra provide a powerful mechanism for mapping data between steps, allowing you to create dynamic data flows and pass information from one step to another.

Understanding Workflow Variables

In Mastra workflows, variables serve as a way to:

  • Map data from trigger inputs to step inputs
  • Pass outputs from one step to inputs of another step
  • Access nested properties within step outputs
  • Create more flexible and reusable workflow steps

Using Variables for Data Mapping

Basic Variable Mapping

You can map data between steps using the variables property when adding a step to your workflow:

src/mastra/workflows/index.ts
import { LegacyStep, LegacyWorkflow } from "@mastra/core/workflows/legacy";

const workflow = new LegacyWorkflow({
name: "data-mapping-workflow",
triggerSchema: z.object({
inputData: z.string(),
}),
});

workflow
.step(step1, {
variables: {
// Map trigger data to step input
inputData: { step: "trigger", path: "inputData" },
},
})
.then(step2, {
variables: {
// Map output from step1 to input for step2
previousValue: { step: step1, path: "outputField" },
},
})
.commit();

// Register the workflow with Mastra
export const mastra = new Mastra({
legacy_workflows: { workflow },
});

Accessing Nested Properties

You can access nested properties using dot notation in the path field:

src/mastra/workflows/index.ts
workflow
.step(step1)
.then(step2, {
variables: {
// Access a nested property from step1's output
nestedValue: { step: step1, path: "nested.deeply.value" },
},
})
.commit();

Mapping Entire Objects

You can map an entire object by using . as the path:

src/mastra/workflows/index.ts
workflow
.step(step1, {
variables: {
// Map the entire trigger data object
triggerData: { step: "trigger", path: "." },
},
})
.commit();

Variables in Loops

Variables can also be passed to while and until loops. This is useful for passing data between iterations or from outside steps:

src/mastra/workflows/loop-variables.ts
// Step that increments a counter
const incrementStep = new LegacyStep({
id: "increment",
inputSchema: z.object({
// Previous value from last iteration
prevValue: z.number().optional(),
}),
outputSchema: z.object({
// Updated counter value
updatedCounter: z.number(),
}),
execute: async ({ context }) => {
const { prevValue = 0 } = context.inputData;
return { updatedCounter: prevValue + 1 };
},
});

const workflow = new LegacyWorkflow({
name: "counter",
});

workflow.step(incrementStep).while(
async ({ context }) => {
// Continue while counter is less than 10
const result = context.getStepResult(incrementStep);
return (result?.updatedCounter ?? 0) < 10;
},
incrementStep,
{
// Pass previous value to next iteration
prevValue: {
step: incrementStep,
path: "updatedCounter",
},
},
);

Variable Resolution

When a workflow executes, Mastra resolves variables at runtime by:

  1. Identifying the source step specified in the step property
  2. Retrieving the output from that step
  3. Navigating to the specified property using the path
  4. Injecting the resolved value into the target step's context as the inputData property

Examples

Mapping from Trigger Data

This example shows how to map data from the workflow trigger to a step:

src/mastra/workflows/trigger-mapping.ts
import { Mastra } from "@mastra/core";
import { LegacyStep, LegacyWorkflow } from "@mastra/core/workflows/legacy";
import { z } from "zod";

// Define a step that needs user input
const processUserInput = new LegacyStep({
id: "processUserInput",
execute: async ({ context }) => {
// The inputData will be available in context because of the variable mapping
const { inputData } = context.inputData;

return {
processedData: `Processed: ${inputData}`,
};
},
});

// Create the workflow
const workflow = new LegacyWorkflow({
name: "trigger-mapping",
triggerSchema: z.object({
inputData: z.string(),
}),
});

// Map the trigger data to the step
workflow
.step(processUserInput, {
variables: {
inputData: { step: "trigger", path: "inputData" },
},
})
.commit();

// Register the workflow with Mastra
export const mastra = new Mastra({
legacy_workflows: { workflow },
});

Mapping Between Steps

This example demonstrates mapping data from one step to another:

src/mastra/workflows/step-mapping.ts
import { Mastra } from "@mastra/core";
import { LegacyStep, LegacyWorkflow } from "@mastra/core/workflows/legacy";
import { z } from "zod";

// Step 1: Generate data
const generateData = new LegacyStep({
id: "generateData",
outputSchema: z.object({
nested: z.object({
value: z.string(),
}),
}),
execute: async () => {
return {
nested: {
value: "step1-data",
},
};
},
});

// Step 2: Process the data from step 1
const processData = new LegacyStep({
id: "processData",
inputSchema: z.object({
previousValue: z.string(),
}),
execute: async ({ context }) => {
// previousValue will be available because of the variable mapping
const { previousValue } = context.inputData;

return {
result: `Processed: ${previousValue}`,
};
},
});

// Create the workflow
const workflow = new LegacyWorkflow({
name: "step-mapping",
});

// Map data from step1 to step2
workflow
.step(generateData)
.then(processData, {
variables: {
// Map the nested.value property from generateData's output
previousValue: { step: generateData, path: "nested.value" },
},
})
.commit();

// Register the workflow with Mastra
export const mastra = new Mastra({
legacy_workflows: { workflow },
});

Type Safety

Mastra provides type safety for variable mappings when using TypeScript:

src/mastra/workflows/type-safe.ts
import { Mastra } from "@mastra/core";
import { LegacyStep, LegacyWorkflow } from "@mastra/core/workflows/legacy";
import { z } from "zod";

// Define schemas for better type safety
const triggerSchema = z.object({
inputValue: z.string(),
});

type TriggerType = z.infer<typeof triggerSchema>;

// Step with typed context
const step1 = new LegacyStep({
id: "step1",
outputSchema: z.object({
nested: z.object({
value: z.string(),
}),
}),
execute: async ({ context }) => {
// TypeScript knows the shape of triggerData
const triggerData = context.getStepResult<TriggerType>("trigger");

return {
nested: {
value: `processed-${triggerData?.inputValue}`,
},
};
},
});

// Create the workflow with the schema
const workflow = new LegacyWorkflow({
name: "type-safe-workflow",
triggerSchema,
});

workflow.step(step1).commit();

// Register the workflow with Mastra
export const mastra = new Mastra({
legacy_workflows: { workflow },
});

Best Practices

  1. Validate Inputs and Outputs: Use inputSchema and outputSchema to ensure data consistency.

  2. Keep Mappings Simple: Avoid overly complex nested paths when possible.

  3. Consider Default Values: Handle cases where mapped data might be undefined.

Comparison with Direct Context Access

While you can access previous step results directly via context.steps, using variable mappings offers several advantages:

FeatureVariable MappingDirect Context Access
ClarityExplicit data dependenciesImplicit dependencies
ReusabilitySteps can be reused with different mappingsSteps are tightly coupled
Type SafetyBetter TypeScript integrationRequires manual type assertions