DEV Community

Cover image for Mastra Multi-Agent Pipeline: Coordinating Four TypeScript Agents Without Losing State
mech.app
mech.app

Posted on Originally published at mech.app

Mastra Multi-Agent Pipeline: Coordinating Four TypeScript Agents Without Losing State

Building a single-agent demo is straightforward. Coordinating four specialized agents, persisting intermediate state, skipping completed work on retries, and keeping the API responsive while models think is where the plumbing gets interesting.

Clause AI analyzes rental and lease agreements using a four-agent pipeline built with Mastra, a TypeScript orchestration framework. Each agent has a focused responsibility: parsing structure, summarizing clauses, flagging risk, and answering user questions via RAG. The implementation exposes real orchestration challenges: state handoff, partial failure recovery, and type-safe communication across agent boundaries.

Why Four Agents Instead of One Prompt

A monolithic prompt that tries to extract entities, summarize clauses, score risk, and answer questions in one pass creates brittle failure modes:

  • Worker crashes mid-pipeline force full re-runs, burning API quota on already-completed steps
  • Partial writes leave the database inconsistent
  • Multiple uploads compete for rate-limited model endpoints
  • Debugging failures requires parsing megabyte-sized prompt contexts

The four-agent design isolates failure domains and allows independent tuning:

Agent Responsibility Model Config Output Schema
Parser Extract entities, dates, parties, payments, clause structure Low reasoning, temp 0.2 Structured JSON with typed fields
Summary Convert legal jargon to plain-English bullets Low reasoning, temp 0.6 Markdown list per clause
Risk Flag risky or unfair clauses with severity scoring Medium reasoning, temp 0.4 Risk array with severity enum
Query Answer user questions via RAG with tool use Medium reasoning, temp 0.7 Natural language response

Each agent runs only when its input dependencies are satisfied, and its output is persisted before the next agent starts.

Mastra's Orchestration Primitives

Mastra provides three core abstractions for multi-agent coordination:

Workflows define the execution graph. Each step declares its dependencies, input schema, and output schema. The framework handles topological sorting and parallel execution where possible.

State persistence happens automatically between steps. Mastra writes each agent's output to a state store (Postgres, Redis, or in-memory) before invoking the next agent. If a step fails, the workflow resumes from the last successful checkpoint.

Agent boundaries are enforced through explicit message passing. Agents cannot call each other directly. The orchestrator mediates all communication, passing typed messages between steps.

Here's the workflow definition for the document analysis pipeline:

import { Workflow, Agent } from '@mastra/core';

const documentWorkflow = new Workflow({
  name: 'analyze-contract',
  steps: [
    {
      id: 'parse',
      agent: parserAgent,
      input: z.object({ documentUrl: z.string() }),
      output: z.object({ 
        entities: z.array(entitySchema),
        clauses: z.array(clauseSchema)
      })
    },
    {
      id: 'summarize',
      agent: summaryAgent,
      dependsOn: ['parse'],
      input: z.object({ clauses: z.array(clauseSchema) }),
      output: z.object({ 
        summaries: z.array(z.string())
      })
    },
    {
      id: 'risk-analysis',
      agent: riskAgent,
      dependsOn: ['parse'],
      input: z.object({ clauses: z.array(clauseSchema) }),
      output: z.object({ 
        risks: z.array(riskSchema)
      })
    },
    {
      id: 'embed',
      dependsOn: ['summarize'],
      handler: async (ctx) => {
        const { summaries } = ctx.getStepOutput('summarize');
        return embedAndStore(summaries);
      }
    }
  ]
});
Enter fullscreen mode Exit fullscreen mode

The dependsOn array creates the execution graph. summarize and risk-analysis both depend on parse, so they run in parallel once parsing completes. The embed step waits for summaries before generating vector embeddings.

State Handoff and Retry Behavior

When a workflow step completes, Mastra writes its output to the state store with a step ID and execution ID. If the workflow crashes, the next invocation reads the state store and skips completed steps.

This creates an important constraint: step outputs must be deterministic or idempotent. If the parser agent returns different results on retry, the workflow will skip it but use stale data for downstream steps.

The framework provides three retry strategies:

  • Skip completed: Resume from the last successful step (default)
  • Rerun failed: Retry only the failed step, reusing upstream outputs
  • Full rerun: Ignore state and execute all steps

For Clause AI, the default skip-completed strategy works because parsing and summarization are deterministic given the same document. Risk analysis uses temperature 0.4, which introduces some variance, but the team accepts minor scoring differences across retries.

Error Propagation Across Agent Boundaries

When an agent throws an error, Mastra propagates it up the workflow graph and marks dependent steps as blocked. The framework does not provide automatic retry logic or circuit breakers. You implement those in the agent handler:

const parserAgent = new Agent({
  name: 'parser',
  model: openai('gpt-4o-mini'),
  instructions: 'Extract entities and clauses...',
  handler: async (input) => {
    const maxRetries = 3;
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await parseDocument(input.documentUrl);
      } catch (err) {
        if (i === maxRetries - 1) throw err;
        await sleep(1000 * Math.pow(2, i)); // exponential backoff
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

This puts retry logic inside the agent boundary, where it has context about transient vs. permanent failures. The workflow sees a single success or failure signal.

Type Safety at Agent Boundaries

Mastra uses Zod schemas to enforce input and output contracts. The workflow definition declares what each step expects and produces. The TypeScript compiler checks that upstream outputs match downstream inputs.

This catches mismatches at build time:

// This fails type checking because 'summarize' expects
// an array of clauseSchema, but 'parse' returns entitySchema
{
  id: 'summarize',
  dependsOn: ['parse'],
  input: z.object({ clauses: z.array(clauseSchema) }),
  // Error: Type 'entitySchema[]' is not assignable to 'clauseSchema[]'
}
Enter fullscreen mode Exit fullscreen mode

The type safety breaks down when agents use tool calls or dynamic output shapes. If the risk agent decides to return a different schema based on document type, the workflow has no way to enforce that at compile time. You need runtime validation inside the agent handler.

Observability Gaps

Mastra logs step start and completion events but does not provide structured trace IDs or execution graphs. To debug a failed workflow, you correlate timestamps in application logs with step IDs in the state store.

The framework does not expose metrics for step duration, retry counts, or queue depth. You instrument those manually:

const startTime = Date.now();
const result = await workflow.run({ documentUrl });
metrics.histogram('workflow.duration', Date.now() - startTime, {
  workflow: 'analyze-contract',
  status: result.status
});
Enter fullscreen mode Exit fullscreen mode

For production deployments, you need an external observability layer. The team uses Axiom to collect structured logs and Grafana to visualize workflow metrics.

Deployment Shape

The Clause AI pipeline runs in three environments:

  • API server: Accepts document uploads, enqueues workflow runs, returns execution IDs
  • Worker pool: Pulls workflow runs from a queue, executes steps, writes state
  • Query service: Handles real-time chat requests using the Query agent

The API server does not execute workflows synchronously. It returns immediately with an execution ID, and the client polls for status. This keeps the API responsive while models think.

Workers scale horizontally. Each worker pulls from a shared queue (Redis Streams) and claims exclusive ownership of a workflow run. If a worker crashes, the run becomes available for another worker after a timeout.

The Query agent runs in a separate service because it needs low latency for chat interactions. It reads from the same state store to access parsed entities and embedded summaries.

Likely Failure Modes

State store unavailability: If Postgres goes down mid-workflow, the worker cannot persist step outputs. The run fails, and retry logic depends on whether the state store was unreachable or corrupted.

Non-deterministic agent outputs: If an agent returns different results on retry, the workflow skips it but uses stale data downstream. This creates subtle bugs that only appear after failures.

Queue backlog: If workers cannot keep up with upload volume, the queue grows unbounded. The team sets a max queue depth and returns 429 errors when it is exceeded.

Model rate limits: OpenAI rate limits apply per API key, not per workflow. If multiple workflows hit the same endpoint simultaneously, they compete for quota. The team uses separate API keys for each agent type.

Technical Verdict

Use Mastra when you need multi-step agent coordination in TypeScript with automatic state persistence and parallel execution. The framework handles the orchestration plumbing so you can focus on agent logic.

Avoid Mastra when you need fine-grained observability, automatic retry policies, or circuit breakers. The framework provides basic workflow execution but lacks production-grade reliability primitives. You will build those yourself.

The type-safe agent boundaries are valuable for catching schema mismatches at build time, but they do not prevent runtime failures from non-deterministic outputs or tool call errors. Treat Mastra as a coordination layer, not a reliability layer.

Source Links

Top comments (1)

Collapse
 
triumph1701 profile image
Triumph

I’d separate conversational context from workflow state and make the latter durable and versioned. A handoff should carry an artifact reference plus a small summary, not the whole history; that keeps parallel branches from silently overwriting each other and makes retries idempotent.