mastra-ai/mastra · error · Error

Foreach step cannot iterate a mapping: mappings project data

Error message

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.

What it means

In dynamic workflows, `foreach` executes a concrete step (agent, tool, or plain step) once per item, while a `mapping` entry is a pure data projection executed once. Iterating a mapping is semantically meaningless, so rehydrateWorkflow rejects it with this explanatory error instead of silently producing a broken graph.

Source

Thrown at packages/core/src/workflows/dynamic/rehydrate.ts:130

      const date = entry.date instanceof Date ? entry.date : new Date(entry.date);
      if (Number.isNaN(date.getTime())) {
        throw new Error(`Stored sleepUntil "${entry.id}" has an unparseable date: ${String(entry.date)}`);
      }
      const live: StepFlowEntry = { type: 'sleepUntil', id: entry.id, date };
      wf.__pushStepFlowEntry(live, { type: 'sleepUntil', id: entry.id, date });
      return;
    }
    case 'parallel': {
      const live: StepFlowEntry = {
        type: 'parallel',
        steps: entry.steps.map(s => rehydrateSingleEntry(s, mastra, schemaOpts)),
      };
      wf.__pushStepFlowEntry(live, entry);
      return;
    }
    case 'foreach': {
      if (entry.step.type === 'mapping') {
        throw new Error(
          `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.`,
        );
      }
      const live: StepFlowEntry = {
        type: 'foreach',
        step: rehydrateSingleEntry(entry.step, mastra, schemaOpts),
        opts: { concurrency: entry.opts?.concurrency ?? 1 },
      };
      wf.__pushStepFlowEntry(live, entry);
      return;
    }
    case 'step': {
      const live = rehydrateSingleEntry(entry, mastra, schemaOpts);
      wf.__pushStepFlowEntry(live, entry);
      return;
    }
    case 'workflow': {
      const nested = assertWorkflowExists(mastra, entry.workflowId);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Replace the foreach body with an agent, tool, or plain step that processes a single item.
  2. If a per-item transformation is needed, move the projection into the foreach step's own map/extract of the item, or use a mapping outside the foreach.
  3. Re-save the corrected graph to storage so the invalid combination is gone.

Example fix

// before
{ type: 'foreach', id: 'f', step: { type: 'mapping', id: 'm', mapConfig: '...' } }
// after
{ type: 'foreach', id: 'f', step: { type: 'tool', id: 't', toolId: 'processItem' } }
Defensive patterns

Strategy: validation

Validate before calling

function assertForeachBody(entry) {
  if (entry.type === 'foreach' && entry.step?.type === 'mapping') {
    throw new TypeError(`Foreach step "${entry.id}" cannot iterate a mapping; use an agent, tool, or plain step`);
  }
}

Type guard

function hasExecutableForeachBody(e: { type: string; step?: { type?: string } }): boolean {
  return e.type !== 'foreach' || (e.step?.type !== undefined && e.step.type !== 'mapping');
}

Try / catch

try {
  const wf = await rehydrateWorkflow(stored, mastra);
} catch (e) {
  if (e instanceof Error && e.message.includes('cannot iterate a mapping')) {
    // replace the foreach body with an agent/tool/plain step and re-save
  }
  throw e;
}

Prevention

When it happens

Trigger: A stored workflow graph contains a foreach entry whose `step` field is `{ type: 'mapping', ... }` — e.g. built via a UI that allowed nesting foreach around a map() projection, or a hand-edited stored definition.

Common situations: Builder UI bugs letting users pick a mapping as the foreach body; users confusing map (projection) with foreach (iteration) and modeling a per-item transformation as a mapping; imported definitions from other formats where map-over-items exists.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/160baf26ef9ae6df. Report an issue: GitHub.