Complex workflows often require multiple agents working together, each specialized for a specific task. This guide shows you how to effectively coordinate multiple agents and pass data between them.

Basic Agent Chaining

The most common pattern is to pass the result of one agent as input to another agent. Since agents use result schemas, you can safely type and validate the data being passed between them.

workflow.version(1).define(async (input, ctx) => {
  // First agent to analyze customer sentiment
  const sentimentAgent = ctx.agent({
    name: "analyzeSentiment",
    systemPrompt: [
      "You are a sentiment analysis expert.",
      "Given a customer message, determine the sentiment and key points.",
      "Be precise in your categorization.",
    ].join("\n"),
    resultSchema: z.object({
      sentiment: z.enum(["positive", "negative", "neutral"]),
      urgency: z.enum(["high", "medium", "low"]),
      keyPoints: z.array(z.string()),
    }),
  });

  // Second agent to generate a response
  const responseAgent = ctx.agent({
    name: "generateResponse",
    systemPrompt: [
      "You are a customer service expert.",
      "Given a sentiment analysis and key points,",
      "generate an appropriate response that addresses the customer's needs.",
    ].join("\n"),
    resultSchema: z.object({
      response: z.string(),
      suggestedActions: z.array(z.string()),
    }),
  });

  // Run the first agent
  const sentimentResult = await sentimentAgent.trigger({
    data: {
      customerMessage: input.message,
    },
  });

  // Pass the first agent's result to the second agent
  const responseResult = await responseAgent.trigger({
    data: {
      sentiment: sentimentResult.sentiment,
      urgency: sentimentResult.urgency,
      keyPoints: sentimentResult.keyPoints,
    },
  });

  console.log({
    analysis: sentimentResult,
    response: responseResult,
  });
});

Parallel Agent Execution

Sometimes you need multiple agents to work independently on different aspects of a task. You can run agents in parallel using programming primitives.

workflow.version(1).define(async (input, ctx) => {
  // Agent to analyze technical aspects
  const technicalAgent = ctx.agent({
    name: "analyzeTechnical",
    systemPrompt: "Analyze technical aspects of the bug report...",
    resultSchema: z.object({
      severity: z.enum(["critical", "high", "medium", "low"]),
      category: z.string(),
      technicalDetails: z.array(z.string()),
    }),
  });

  // Agent to analyze business impact
  const businessAgent = ctx.agent({
    name: "analyzeBusinessImpact",
    systemPrompt: "Analyze business impact of the reported issue...",
    resultSchema: z.object({
      impact: z.enum(["high", "medium", "low"]),
      affectedUsers: z.number(),
      businessRisks: z.array(z.string()),
    }),
  });

  // Run both agents in parallel
  const [technicalAnalysis, businessAnalysis] = await Promise.all([
    technicalAgent.trigger({ data: { bugReport: input.report } }),
    businessAgent.trigger({ data: { bugReport: input.report } }),
  ]);

  // Combine results for final decision
  console.log({
    technical: technicalAnalysis,
    business: businessAnalysis,
    priority:
      technicalAnalysis.severity === "critical" ||
      businessAnalysis.impact === "high"
        ? "immediate"
        : "normal",
  });
});

Dynamic Agent Creation

You can also create agents dynamically based on previous agent results:

workflow.version(1).define(async (input, ctx) => {
  // First agent determines what kind of specialists we need
  const routerAgent = ctx.agent({
    name: "routeRequest",
    systemPrompt: "Determine which specialists need to handle this request...",
    resultSchema: z.object({
      requiredSpecialists: z.array(
        z.enum(["technical", "billing", "security", "compliance"]),
      ),
    }),
  });

  const routingResult = await routerAgent.trigger({
    data: { request: input.customerRequest },
  });

  // Create and run specialist agents based on the routing
  const specialistResults = await Promise.all(
    routingResult.requiredSpecialists.map((specialist) => {
      const specialistAgent = ctx.agent({
        name: `${specialist}Specialist`,
        systemPrompt: `You are a ${specialist} specialist. Analyze the request...`,
        resultSchema: z.object({
          analysis: z.string(),
          recommendations: z.array(z.string()),
        }),
      });

      return specialistAgent.trigger({
        data: { request: input.customerRequest },
      });
    }),
  );

  console.log({
    routing: routingResult,
    specialistAnalyses: specialistResults,
  });
});

Best Practices

When working with multiple agents:

  1. Clear Responsibilities: Each agent should have a clear, focused responsibility.
  2. Type Safety: Use result schemas to ensure type safety when passing data between agents.
  3. Error Handling: Consider what happens if one agent fails in a chain of agents. These failure modes are covered in the workflow section.