CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Claude Code
Claude Code/2026-03-15Advanced

Building Production-Grade Multi-Agent Systems with Claude Code: Complete Implementation Guide

Master advanced multi-agent orchestration patterns using Claude Code and Agent SDK. Learn parallel execution, dependency management, automatic retries, and dynamic task distribution. Complete implementations for financial analysis, web scraping, and complex workflows.

claude-code129agent-sdk3orchestration7advanced11multi-agent6workflow37

Why Multi-Agent Orchestration Matters

Modern enterprise automation demands sophisticated coordination beyond what a single agent can handle:

  • Financial Analysis — Retrieve market data from multiple sources in parallel, analyze in real-time, assess risk
  • Web Scraping at Scale — Deploy specialized scrapers for different site architectures, aggregate results intelligently
  • Complex Workflows — Multi-step processes from sales proposals to customer onboarding to contract generation
  • Fault Tolerance — Continue processing when individual agents fail, with automatic recovery mechanisms

Traditional approaches suffer from bloated state management and error-handling code. Claude Code combined with Agent SDK provides systematic solutions to these challenges.

Foundational Concept: Master-Worker Pattern

// Master Orchestrator Agent Implementation
const Anthropic = require("@anthropic-ai/sdk");
 
class AgentOrchestrator {
  constructor() {
    this.client = new Anthropic();
    this.agents = [];
  }
 
  // Register worker agents
  registerAgent(name, role, capabilities) {
    this.agents.push({ name, role, capabilities });
  }
 
  // Core task distribution engine
  async distributeTask(task, context) {
    // Analyze task requirements
    const analysis = await this.analyzeTask(task);
    
    // Select appropriate agents
    const assignedAgents = this.selectAgents(analysis, context);
    
    // Execute in parallel
    const results = await Promise.all(
      assignedAgents.map(agent => this.executeAgent(agent, task, context))
    );
 
    return this.aggregateResults(results);
  }
 
  // Task analysis for skill matching
  async analyzeTask(task) {
    const response = await this.client.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 1024,
      messages: [
        {
          role: "user",
          content: `Analyze required skills for: "${task}". 
          Return JSON: {skills: [string], complexity: 1-10, estimatedTime: string}`
        }
      ]
    });
 
    const content = response.content[0];
    if (content.type === "text") {
      return JSON.parse(content.text);
    }
    return { skills: [], complexity: 5, estimatedTime: "unknown" };
  }
 
  // Select optimal agents
  selectAgents(analysis, context) {
    return this.agents.filter(agent => {
      const hasSkills = analysis.skills.some(skill => 
        agent.capabilities.includes(skill)
      );
      return hasSkills && (context.agentPreferences ? 
        context.agentPreferences.includes(agent.name) : true);
    });
  }
 
  // Execute on agent
  async executeAgent(agent, task, context) {
    try {
      const response = await this.client.messages.create({
        model: "claude-opus-4-6",
        max_tokens: 4096,
        messages: [
          {
            role: "user",
            content: `You are a ${agent.role} specializing in: ${agent.capabilities.join(", ")}.
            Task: ${task}
            Context: ${JSON.stringify(context)}
            
            Execute and provide detailed results with reasoning.`
          }
        ]
      });
 
      return {
        agentName: agent.name,
        result: response.content[0].type === "text" ? response.content[0].text : null,
        status: "success",
        tokensUsed: response.usage
      };
    } catch (error) {
      return {
        agentName: agent.name,
        error: error.message,
        status: "failed",
        tokensUsed: 0
      };
    }
  }
 
  // Aggregate results
  aggregateResults(results) {
    const successful = results.filter(r => r.status === "success");
    const failed = results.filter(r => r.status === "failed");
 
    return {
      successCount: successful.length,
      failureCount: failed.length,
      results: successful.map(r => ({
        agent: r.agentName,
        output: r.result
      })),
      errors: failed
    };
  }
}
 
// Usage
const orchestrator = new AgentOrchestrator();
orchestrator.registerAgent("data-analyst", "Financial Analyst", [
  "financial-analysis", "data-visualization", "trend-detection"
]);
orchestrator.registerAgent("web-expert", "Web Specialist", [
  "web-scraping", "html-parsing", "data-extraction"
]);
 
const result = await orchestrator.distributeTask(
  "Analyze market trends and extract competitor data",
  { agentPreferences: ["data-analyst", "web-expert"] }
);
 
console.log(result);

Pattern 1: Parallel Execution with Resilience

Use Case: Real-Time Multi-Source Data Fetching

Retrieve financial data (stocks, crypto, forex) from multiple sources in parallel. Survive individual source failures while maintaining overall workflow.

class ResilientDataFetcher {
  constructor(maxRetries = 3, timeoutMs = 30000) {
    this.client = new Anthropic();
    this.maxRetries = maxRetries;
    this.timeoutMs = timeoutMs;
  }
 
  // Per-source fetch with retry logic
  async fetchFromSource(source, query) {
    let lastError;
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await Promise.race([
          this.client.messages.create({
            model: "claude-opus-4-6",
            max_tokens: 2048,
            messages: [
              {
                role: "user",
                content: `Fetch from ${source}: "${query}". 
                Return: {source, timestamp, data, confidence}`
              }
            ]
          }),
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error("Timeout")), this.timeoutMs)
          )
        ]);
 
        return JSON.parse(response.content[0].text);
      } catch (error) {
        lastError = error;
        const backoffMs = Math.pow(2, attempt) * 1000;
        console.log(`Attempt ${attempt} failed. Backing off ${backoffMs}ms...`);
        await new Promise(resolve => setTimeout(resolve, backoffMs));
      }
    }
 
    return {
      source,
      status: "failed",
      error: lastError.message,
      confidence: 0
    };
  }
 
  // Parallel fetching
  async fetchMultipleSources(sources, query) {
    const fetchTasks = sources.map(source => 
      this.fetchFromSource(source, query)
    );
 
    const results = await Promise.allSettled(fetchTasks);
 
    return {
      timestamp: new Date().toISOString(),
      query,
      results: results.map((result, index) => ({
        source: sources[index],
        status: result.status,
        data: result.status === "fulfilled" ? result.value : { error: result.reason }
      })),
      successRate: results.filter(r => r.status === "fulfilled").length / sources.length
    };
  }
}
 
// Usage
const fetcher = new ResilientDataFetcher(3, 30000);
const data = await fetcher.fetchMultipleSources(
  ["AlphaVantage", "CoinGecko", "OANDA"],
  "AAPL stock price"
);
 
console.log(`Success rate: ${(data.successRate * 100).toFixed(1)}%`);

Pattern 2: Dependency Graph Execution

Use Case: Automated Sales Proposal Generation

Multiple steps with explicit dependencies (customer fetch → needs analysis → proposal → quote). Execute in correct order, passing results forward.

class DependencyGraphExecutor {
  constructor() {
    this.client = new Anthropic();
    this.executionLog = {};
    this.resultCache = {};
  }
 
  // Execute workflow with dependency graph
  async executeWorkflow(graph, inputs) {
    const executionOrder = this.topologicalSort(graph);
 
    for (const taskId of executionOrder) {
      const task = graph[taskId];
 
      // Collect dependency results
      const dependencyResults = {};
      for (const depId of task.dependencies || []) {
        dependencyResults[depId] = this.resultCache[depId];
      }
 
      console.log(`Executing: ${taskId}`);
      const result = await this.executeTask(
        taskId,
        task,
        { ...inputs, dependencies: dependencyResults }
      );
 
      this.resultCache[taskId] = result;
      this.executionLog[taskId] = {
        timestamp: new Date().toISOString(),
        status: "completed",
        result
      };
    }
 
    return this.resultCache;
  }
 
  // Topological sorting
  topologicalSort(graph) {
    const visited = new Set();
    const stack = [];
 
    const visit = (node) => {
      if (visited.has(node)) return;
      visited.add(node);
 
      const task = graph[node];
      for (const dep of task.dependencies || []) {
        visit(dep);
      }
 
      stack.push(node);
    };
 
    for (const node of Object.keys(graph)) {
      visit(node);
    }
 
    return stack;
  }
 
  // Execute individual task
  async executeTask(taskId, task, context) {
    const response = await this.client.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 4096,
      messages: [
        {
          role: "user",
          content: `
            Task: ${task.description}
            Context: ${JSON.stringify(context, null, 2)}
            
            Output format: ${task.outputFormat || "natural language"}
          `
        }
      ]
    });
 
    return response.content[0].type === "text" ? response.content[0].text : null;
  }
}
 
// Workflow definition
const proposalWorkflow = {
  "fetch_customer": {
    description: "Get customer profile from CRM",
    dependencies: [],
    outputFormat: "JSON"
  },
  "analyze_needs": {
    description: "Analyze customer needs",
    dependencies: ["fetch_customer"],
    outputFormat: "JSON"
  },
  "create_proposal": {
    description: "Generate proposal document",
    dependencies: ["fetch_customer", "analyze_needs"],
    outputFormat: "markdown"
  },
  "generate_quote": {
    description: "Create pricing quote",
    dependencies: ["analyze_needs"],
    outputFormat: "JSON"
  }
};
 
const executor = new DependencyGraphExecutor();
const results = await executor.executeWorkflow(proposalWorkflow, {
  customerId: "CUST-001"
});

Pattern 3: Load-Balanced Task Distribution

Use Case: Intelligent Web Scraping Assignment

Distribute scraping tasks to specialized agents based on site type. Balance load, monitor performance, reassign underperforming agents.

class LoadBalancedTaskDistributor {
  constructor() {
    this.client = new Anthropic();
    this.agents = [];
    this.taskQueue = [];
    this.metrics = {};
  }
 
  // Register agent with capacity
  registerAgent(id, specialization, maxConcurrentTasks = 3) {
    const agent = {
      id,
      specialization,
      maxConcurrentTasks,
      currentTasks: 0,
      completedTasks: 0,
      averageExecutionTime: 0,
      successRate: 1.0
    };
    this.agents.push(agent);
    this.metrics[id] = agent;
  }
 
  // Enqueue task
  enqueueTask(task) {
    this.taskQueue.push({
      id: Math.random().toString(36),
      ...task,
      createdAt: Date.now(),
      attempts: 0
    });
  }
 
  // Select agent using scoring
  selectBestAgent(task) {
    let bestAgent = null;
    let bestScore = -Infinity;
 
    for (const agent of this.agents) {
      if (agent.currentTasks >= agent.maxConcurrentTasks) continue;
 
      const loadScore = (agent.maxConcurrentTasks - agent.currentTasks) / agent.maxConcurrentTasks;
      const performanceScore = agent.successRate * (1 / (agent.averageExecutionTime || 1));
      
      let compatibilityScore = 0;
      if (task.requiredSkills) {
        const matches = task.requiredSkills.filter(skill =>
          agent.specialization.includes(skill)
        ).length;
        compatibilityScore = matches / task.requiredSkills.length;
      }
 
      const totalScore = (loadScore * 0.3) + (performanceScore * 0.4) + (compatibilityScore * 0.3);
 
      if (totalScore > bestScore) {
        bestScore = totalScore;
        bestAgent = agent;
      }
    }
 
    return bestAgent;
  }
 
  // Main processing loop
  async processTaskQueue() {
    while (this.taskQueue.length > 0) {
      const task = this.taskQueue.shift();
      const agent = this.selectBestAgent(task);
 
      if (!agent) {
        this.taskQueue.push(task);
        await new Promise(resolve => setTimeout(resolve, 1000));
        continue;
      }
 
      agent.currentTasks++;
      const startTime = Date.now();
 
      this.executeTaskOnAgent(agent, task)
        .then(result => {
          const executionTime = Date.now() - startTime;
 
          agent.completedTasks++;
          agent.averageExecutionTime = 
            (agent.averageExecutionTime * (agent.completedTasks - 1) + executionTime) / 
            agent.completedTasks;
          agent.successRate = 0.95 * agent.successRate + 0.05;
 
          console.log(`✓ Task ${task.id} by ${agent.id} in ${executionTime}ms`);
        })
        .catch(error => {
          task.attempts++;
          if (task.attempts < 3) {
            this.taskQueue.push(task);
          }
          agent.successRate = 0.95 * agent.successRate;
        })
        .finally(() => {
          agent.currentTasks--;
        });
 
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }
 
  // Execute on agent
  async executeTaskOnAgent(agent, task) {
    const response = await this.client.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 2048,
      messages: [
        {
          role: "user",
          content: `Specializing in: ${agent.specialization.join(", ")}.
          
          Scrape task:
          URL: ${task.url}
          Target: ${task.targetData}
          
          Return as JSON.`
        }
      ]
    });
 
    return JSON.parse(response.content[0].text);
  }
}
 
// Usage
const distributor = new LoadBalancedTaskDistributor();
distributor.registerAgent("scraper-1", ["ecommerce", "json"], 2);
distributor.registerAgent("scraper-2", ["news", "article"], 3);
 
[
  { url: "shop.com", targetData: "prices", requiredSkills: ["ecommerce"] },
  { url: "news.com", targetData: "articles", requiredSkills: ["news"] }
].forEach(task => distributor.enqueueTask(task));
 
await distributor.processTaskQueue();

Token Optimization

Caching and Batch Processing

class TokenEfficientOrchestrator {
  constructor() {
    this.client = new Anthropic();
    this.responseCache = new Map();
  }
 
  // Cache identical queries
  async getCachedResponse(cacheKey, generator) {
    if (this.responseCache.has(cacheKey)) {
      return this.responseCache.get(cacheKey);
    }
 
    const response = await generator();
    this.responseCache.set(cacheKey, response);
    return response;
  }
 
  // Batch multiple tasks
  async batchExecute(tasks) {
    const batchPrompt = tasks.map((task, i) => 
      `Task ${i + 1}: ${task.description}`
    ).join("\n\n");
 
    const response = await this.client.messages.create({
      model: "claude-opus-4-6",
      max_tokens: 4096,
      messages: [
        {
          role: "user",
          content: `Process all tasks:\n\n${batchPrompt}`
        }
      ]
    });
 
    return response.content[0].text;
  }
}

Surviving Across Sessions — Externalizing Orchestrator State and Turning Failures into Learning

Every pattern so far assumes a single run. Parallelism, dependency graphs, load balancing — all of it lives between the moment the orchestrator starts and the moment it finishes.

Real pipelines rarely finish in one pass. You run them overnight and check the rest in the morning. You let the first wave of agents go, then run the rest only after reviewing results. The moment a run spans that gap, the model forgets everything from before. Start the orchestrator again and it has no idea which tasks are already done, so it redoes them from scratch.

Running the Dolice Labs article generation through agents as an indie developer, this is exactly where I got stuck: a nightly job stalled partway, and the next morning I spent longer reconstructing "how far did it get" from the logs than the run itself had taken.

Push orchestrator state out to files

The fix is plain. Write the orchestrator's progress out of the run and into files. Keep two: a human-readable summary and a machine-readable structured state.

from datetime import datetime
from pathlib import Path
import yaml
 
def save_orchestrator_state(run_dir: str, state: dict) -> None:
    """Write the orchestrator's progress out to two files."""
    run_path = Path(run_dir)
    run_path.mkdir(parents=True, exist_ok=True)
 
    # 1) A summary a human can scan at a glance
    summary = f"""# Run state (updated: {datetime.now():%Y-%m-%d %H:%M})
 
## Current phase
{state.get("current_phase", "unset")}
 
## Finished agents
{", ".join(state.get("done_agents", [])) or "none"}
 
## Pending tasks
{", ".join(state.get("pending_tasks", [])) or "none"}
 
## Key decisions
{state.get("decisions", "none")}
"""
    (run_path / "run_state.md").write_text(summary, encoding="utf-8")
 
    # 2) Structured state the machine reads on resume
    machine = {
        "updated_at": datetime.now().isoformat(),
        "current_phase": state.get("current_phase"),
        "done_agents": state.get("done_agents", []),
        "pending_tasks": state.get("pending_tasks", []),
    }
    with open(run_path / "run_state.yaml", "w", encoding="utf-8") as f:
        yaml.dump(machine, f, allow_unicode=True, default_flow_style=False)
 
 
def resume_orchestrator_state(run_dir: str) -> dict:
    """Load last run's state on resume; start empty if none exists."""
    state_file = Path(run_dir) / "run_state.yaml"
    if not state_file.exists():
        return {"current_phase": None, "done_agents": [], "pending_tasks": []}
    with open(state_file, encoding="utf-8") as f:
        return yaml.safe_load(f) or {}

Stack each finished agent into done_agents and keep only the next tasks in pending_tasks. On resume, read resume_orchestrator_state first and skip anything already done. This is where the earlier idea — tagging every agent with a unique ID in the logs — connects: logs give you visibility during a run, the state file gives you memory between runs.

Carry failures into the next run

Once state lives outside the run, you can go one step further: stop throwing away the corrections a human hands back, and stop letting the same mistake reappear next time.

import re
from collections import Counter
from datetime import datetime
from pathlib import Path
 
def collect_recurring_corrections(run_dir: str, threshold: int = 3) -> dict:
    """Pull only the corrections that recur at or above the threshold."""
    log_dir = Path(run_dir) / "_feedback"
    if not log_dir.exists():
        return {}
 
    corrections = []
    for log in log_dir.glob("*.md"):
        text = log.read_text(encoding="utf-8")
        corrections += re.findall(r"##\s*Correction\s*\n(.*?)(?=##|\Z)", text, re.DOTALL)
 
    normalized = [c.strip()[:100] for c in corrections]
    counts = Counter(normalized)
    return {k: v for k, v in counts.items() if v >= threshold}
 
 
def stage_improvements(run_dir: str, recurring: dict) -> None:
    """Record recurring corrections into pending_improvements.md as proposals.
    Never write them straight into the agent instructions; a human reviews first."""
    if not recurring:
        return
    pending = Path(run_dir) / "pending_improvements.md"
    stamp = datetime.now().strftime("%Y-%m-%d")
    lines = [f"- [{stamp}] x{count}: {issue[:80]}" for issue, count in recurring.items()]
    body = pending.read_text(encoding="utf-8") if pending.exists() else ""
    pending.write_text(body + f"\n\n## {stamp}\n" + "\n".join(lines), encoding="utf-8")
    print(f"Staged {len(recurring)} proposal(s). Review before applying.")

The deliberate pause is the point. A detected problem never gets written straight into the agent's instructions. It is staged as a proposal, reviewed by a human, and only then applied. An orchestrator quietly rewriting its own blueprint is the failure mode I fear most. Make it reversible before you automate it — that ordering is the one thing I never bend.

On the next run, the approved "known pitfalls" get prepended to each agent's prompt. No correction lands a third time. That alone visibly cut how often work bounced back to review.

Wrapping up

Claude Code + Agent SDK multi-agent orchestration solves:

  1. Complex Logic — Parallel execution, dependencies, error handling
  2. Scalability — Grows with agent count
  3. Maintainability — Centralized orchestration
  4. Cost Efficiency — Optimized token consumption

Master these patterns to build AI-driven enterprise workflows at scale.

Automating the Dolice Labs operations as an indie developer, I keep relearning that the more agents you add, the harder it gets to see who is waiting on whom. I skipped the orchestrator layer early on and paid for it; now I always funnel state sharing and error propagation through a single place. Complexity is something you contain by design, not by adding features.

See Claude Code Agent Guide and Agent SDK Intro for deeper learning.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-03-27
Claude Code Mastery — Hooks, Orchestration & Multi-Agent Operations
Master Hooks and Orchestration in Claude Code. Learn 21 lifecycle events, 4 handler patterns, and multi-agent coordination strategies inspired by DevMoses' 198-agent parallel execution.
Claude Code2026-06-18
Claude Code Adds /cd — Carrying Your Warm Cache Across Repositories
Claude Code's new /cd moves a running session to another working directory without rebuilding the prompt cache. Here are the design calls and pitfalls when you sweep across several repositories in one sitting.
Claude Code2026-06-02
Two Weeks of Splitting iOS Work Between Claude on Xcode and Claude Code
I ran Claude on Xcode, which lives in the Xcode sidebar, alongside Claude Code in the terminal across two weeks of real wallpaper-app work. Here is how I ended up dividing the tasks, and the simple rule I use to decide which one to open.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →