Skip to main content
This guide walks you through building an orchestrator agent — an agent that calls other agents to complete complex workflows. By the end, you’ll have a working orchestrator that coordinates multiple leaf agents.
This is a hands-on tutorial. For reference documentation on orchestration concepts, see Orchestration. For SDK details, see SDK Reference.

Quick Start with Templates

The fastest way to scaffold an orchestrator is with orch init --template. Three orchestration patterns are available:
# Fan-out: call multiple agents in parallel, combine results
orch init my-scanner --template fan-out

# Pipeline: sequential processing — each step feeds into the next
orch init my-pipeline --template pipeline

# Map-reduce: split input, process in parallel, aggregate
orch init my-processor --template map-reduce
Each template generates a complete project with orchagent.json, main.py, schema.json, requirements.txt, and README.md. Update manifest.dependencies with your actual agents, edit the orchestration logic, and publish. All templates support JavaScript via --language javascript.
Templates are a great starting point. Read on to understand how orchestration works under the hood, or jump straight to Part 4: Publish and Run if you’ve already customized your template.

Prerequisites

  • orchagent CLI installed and authenticated (orch login)
  • At least one published agent to use as a dependency (or follow along to create both)
  • An LLM API key configured (Anthropic recommended for managed loop agents)

Choose Your Approach

orchagent supports two orchestration patterns. Choose based on how much control you need:
Managed LoopCode Runtime
How it worksLLM decides when to call dependencies via tool useYour Python or JavaScript code calls dependencies directly
You writeorchagent.json + prompt.md (no code)orchagent.json + code using the orchagent SDK
Best forLLM-driven decisions, flexible workflowsDeterministic pipelines, custom logic
Manifest fieldloop + custom_toolsruntime.command
Both approaches use the same dependency system, publishing flow, and security model.

Part 1: Build a Leaf Agent

Before building the orchestrator, you need at least one agent to call. If you already have published agents, skip to Part 2. Here’s a minimal tool agent that scans code for leaked secrets:

leak-finder/orchagent.json

{
  "name": "leak-finder",
  "type": "tool",
  "description": "Scans code for leaked secrets and API keys",
  "supported_providers": ["any"],
  "runtime": {
    "command": "python main.py"
  },
  "required_secrets": ["ANTHROPIC_API_KEY"]
}
Agents are callable by default (callable: true). You only need to set "callable": false to explicitly opt out (e.g., for always-on services like Discord bots).

leak-finder/main.py

import json
import sys
import re

def scan_for_secrets(code: str) -> list:
    patterns = {
        "AWS Key": r"AKIA[0-9A-Z]{16}",
        "GitHub Token": r"ghp_[a-zA-Z0-9]{36}",
        "Generic API Key": r"(?i)(api[_-]?key|secret[_-]?key)\s*[:=]\s*['\"][a-zA-Z0-9]{20,}['\"]",
    }
    findings = []
    for name, pattern in patterns.items():
        for match in re.finditer(pattern, code):
            findings.append({"type": name, "match": match.group()[:20] + "..."})
    return findings

input_data = json.loads(sys.stdin.read())
findings = scan_for_secrets(input_data.get("code", ""))
print(json.dumps({"findings": findings, "count": len(findings)}))
Publish it:
cd leak-finder
orch publish

Part 2: Build the Orchestrator

A managed loop orchestrator uses an LLM tool-use loop. The LLM reads your prompt, sees the available tools, and decides which dependencies to call and in what order. You don’t write code — just the prompt and tool definitions. Create a directory with three files:

security-review/orchagent.json

{
  "name": "security-review",
  "type": "agent",
  "description": "Comprehensive security review combining secret scanning and dependency auditing",
  "supported_providers": ["anthropic"],
  "loop": {
    "max_turns": 25
  },
  "timeout_seconds": 300,
  "manifest": {
    "manifest_version": 1,
    "dependencies": [
      { "id": "yourorg/leak-finder", "version": "v1" },
      { "id": "yourorg/dep-scanner", "version": "v1" }
    ],
    "orchestration_mode": "strict",
    "max_hops": 2,
    "timeout_ms": 300000,
    "per_call_downstream_cap": 100
  },
  "custom_tools": [
    {
      "name": "scan_secrets",
      "description": "Scan code for leaked secrets, API keys, and credentials",
      "command": "python3 /home/user/helpers/orch_call.py yourorg/leak-finder@v1",
      "input_schema": {
        "type": "object",
        "properties": {
          "code": { "type": "string", "description": "Code to scan" }
        },
        "required": ["code"]
      }
    },
    {
      "name": "scan_dependencies",
      "description": "Audit package dependencies for known vulnerabilities",
      "command": "python3 /home/user/helpers/orch_call.py yourorg/dep-scanner@v1",
      "input_schema": {
        "type": "object",
        "properties": {
          "path": { "type": "string", "description": "Project root to scan" }
        },
        "required": ["path"]
      }
    }
  ]
}
Key fields explained:
FieldPurpose
loop.max_turnsMaximum LLM iterations before forcing a result (25 is generous for most tasks)
manifest.dependenciesAgents this orchestrator is allowed to call — anything not listed is blocked
manifest.orchestration_mode"strict" (default) or "flexible". Strict removes bash and requires dependency calls. See Strict vs Flexible Mode.
manifest.max_hopsMaximum call chain depth (2 = this agent can call agents that don’t call other agents)
manifest.timeout_msTotal timeout for the entire execution including all dependency calls
manifest.per_call_downstream_capBudget passed to each dependency call (limits child agent spending, not this agent’s own calls)
custom_toolsTool definitions presented to the LLM — each command calls a dependency via the built-in orch_call.py helper
Strict mode is the default. Orchestrators with dependencies default to orchestration_mode: "strict" — bash is disabled and the agent must use its custom tools. To keep bash available, set "orchestration_mode": "flexible" in your manifest.
custom_tools is required for managed-loop orchestrators. If you declare manifest.dependencies but don’t add matching custom_tools, the LLM has no way to call your dependencies. It will waste all turns exploring the filesystem instead. Use orch scaffold orchestration to auto-generate the correct custom_tools from your dependencies.
The command field must use the exact format: python3 /home/user/helpers/orch_call.py org/agent@version. This helper script is pre-installed in every managed loop sandbox. It reads the tool input, calls the dependency through the gateway, and returns the result.

security-review/prompt.md

You are a security review agent. When given code or a repository, perform a thorough security audit.

## Process
1. Use scan_secrets to find any leaked credentials or API keys
2. Use scan_dependencies to check for vulnerable dependencies
3. Analyze the combined findings and assess overall risk

## Output
Return a JSON object with:
- severity: "low", "medium", "high", or "critical"
- findings: array of individual findings from both scans
- summary: one-paragraph risk assessment
- recommendations: array of specific actions to fix issues

security-review/schema.json

{
  "input": {
    "type": "object",
    "properties": {
      "code": { "type": "string", "description": "Code to review" },
      "repo_url": { "type": "string", "description": "Repository URL to scan" }
    }
  },
  "output": {
    "type": "object",
    "properties": {
      "severity": { "type": "string" },
      "findings": { "type": "array" },
      "summary": { "type": "string" },
      "recommendations": { "type": "array" }
    }
  }
}

Option B: Code Runtime

A code runtime orchestrator runs your code directly. You control the execution flow — which agents to call, in what order, and how to process results. Use this when you need deterministic logic, custom data transformation, or parallel fan-out. Code runtime orchestrators can be written in Python or JavaScript.

security-review/orchagent.json

{
  "name": "security-review",
  "type": "tool",
  "description": "Comprehensive security review combining secret scanning and dependency auditing",
  "supported_providers": ["any"],
  "runtime": {
    "command": "python main.py"
  },
  "timeout_seconds": 180,
  "manifest": {
    "manifest_version": 1,
    "dependencies": [
      { "id": "yourorg/leak-finder", "version": "v1" },
      { "id": "yourorg/dep-scanner", "version": "v1" }
    ],
    "max_hops": 2,
    "timeout_ms": 180000,
    "per_call_downstream_cap": 100
  }
}

security-review/main.py

import asyncio
import json
import sys

from orchagent import AgentClient, DependencyCallError

async def main():
    input_data = json.loads(sys.stdin.read())
    client = AgentClient()

    # Fan-out: call both scanners in parallel
    secrets_result, deps_result = await asyncio.gather(
        client.call("yourorg/leak-finder@v1", {"code": input_data.get("code", "")}),
        client.call("yourorg/dep-scanner@v1", {"path": input_data.get("path", ".")}),
        return_exceptions=True,
    )

    # Handle partial failures gracefully
    findings = []
    if isinstance(secrets_result, Exception):
        findings.append({"source": "leak-finder", "error": str(secrets_result)})
    else:
        findings.extend(secrets_result.get("findings", []))

    if isinstance(deps_result, Exception):
        findings.append({"source": "dep-scanner", "error": str(deps_result)})
    else:
        findings.extend(deps_result.get("vulnerabilities", []))

    severity = "critical" if len(findings) > 5 else "high" if findings else "low"

    print(json.dumps({
        "severity": severity,
        "findings": findings,
        "total_issues": len(findings),
    }))

asyncio.run(main())

security-review/requirements.txt

orchagent-sdk
orchagent-sdk must be in your requirements.txt. It is installed automatically inside the sandbox at runtime, but the install takes approximately 55 seconds — factor this into your timeout budget (see Timeout Budgeting).

Part 3: Workspace Secrets

If your agent (or any agent in the chain) needs external API keys or credentials at runtime, use workspace secrets combined with the required_secrets field.

Adding Secrets

  1. Go to your orchagent DashboardSettingsSecrets
  2. Add each secret (e.g., EXTERNAL_API_KEY, DATABASE_URL)

Declaring Required Secrets

In your orchagent.json, list which secrets your agent needs:
{
  "name": "my-agent",
  "required_secrets": ["EXTERNAL_API_KEY", "DATABASE_URL"]
}
At runtime, these secrets are injected as environment variables into your agent’s sandbox. Secrets not listed in required_secrets will not be available, even if they exist in your workspace.
import os
api_key = os.environ["EXTERNAL_API_KEY"]  # Available because it's in required_secrets
Do not add ORCHAGENT_SERVICE_KEY to required_secrets. The gateway automatically injects a temporary service key for agents that have manifest dependencies. Adding your own workspace secret named ORCHAGENT_SERVICE_KEY will override the auto-injected key and break orchestration with confusing billing errors.

Part 4: Publish and Run

Publishing Order

Always publish bottom-up — leaf agents before orchestrators. The gateway validates that all declared dependencies exist at publish time. The easiest way is --all from the parent directory — it auto-detects the dependency graph and publishes in the correct order:
# Publish all agents in dependency order (recommended)
orch publish --all

# Preview the plan without publishing
orch publish --all --dry-run
Manual alternative:
# 1. Publish leaf agents first
cd leak-finder && orch publish
cd ../dep-scanner && orch publish

# 2. Then publish the orchestrator
cd ../security-review && orch publish
If you try to publish the orchestrator before its dependencies, you’ll get a “Dependency not found” error.

Running Your Orchestrator

# Run on the cloud (default)
orch run yourorg/security-review --data '{"code": "api_key = \"sk-abc123\""}'

# Run with JSON output
orch run yourorg/security-review --json --data '{"code": "api_key = \"sk-abc123\""}'

Timeout Budgeting

Sandboxes need time to start up and install dependencies. Plan your timeouts accordingly:
PhaseApproximate Time
Sandbox startup~5s
SDK installation (Python orchagent-sdk in requirements.txt)~55s
npm install (JavaScript orchagent-sdk in package.json)~15-30s
Your agent’s executionvaries
Each dependency call (includes its own startup + install)~60-70s overhead + execution
Rules of thumb:
  • Managed loop orchestrators: Set timeout_seconds to at least 300 (5 minutes). The LLM loop, sandbox setup, and dependency calls all share this budget.
  • Code runtime orchestrators: Set timeout_seconds to at least 180 (3 minutes) to account for SDK installation + dependency call overhead.
  • manifest.timeout_ms should match or exceed timeout_seconds × 1000.
  • Leaf agents with no SDK dependency are fast (~5s startup). Set their timeouts lower.
The platform adds a 120-second buffer to your timeout for sandbox lifecycle management. A timeout_seconds: 300 agent gets a sandbox that lives for 420 seconds. You don’t need to account for this buffer yourself.

Multi-Level Orchestration

Orchestrators can call other orchestrators, creating multi-level chains. Each level runs in its own isolated sandbox.
User → security-suite (orchestrator, max_hops: 3)
         ├→ code-reviewer (orchestrator, calls linter + formatter)
         └→ vuln-scanner (leaf agent)
For multi-level chains:
  • The top-level agent’s max_hops must be at least equal to the deepest chain depth
  • Each level decrements the hop count by 1
  • Timeouts propagate down — ensure the top-level timeout is large enough for the entire chain
  • Publishing order is strictly bottom-up: deepest leaf agents first, then mid-level orchestrators, then the top-level agent

Troubleshooting

”Dependency not found” on publish

All dependencies must be published before the orchestrator. Publish leaf agents first.

Agent runs but dependency calls return empty results

Agents are callable by default. If the dependency has "callable": false set explicitly, remove it or set it to true. The gateway blocks agent-to-agent calls to non-callable agents.

”MISSING_BILLING_ORG” error

This usually means ORCHAGENT_SERVICE_KEY was overridden by a workspace secret. Remove ORCHAGENT_SERVICE_KEY from your required_secrets — the gateway injects it automatically.

Secrets are empty inside the sandbox

Your agent must declare required_secrets in orchagent.json. Secrets added to the dashboard are only injected into sandboxes that explicitly request them.

Timeout / “peer closed connection”

The SDK install (~55s) plus sandbox startup (~5s) consumes budget before your code runs. Increase timeout_seconds to give your agent more time. For orchestrators, 300s is a safe starting point.

Dependency returns error but no details

Wrap SDK calls in try/catch and log the error:
from orchagent import DependencyCallError

try:
    result = await client.call("org/agent@v1", data)
except DependencyCallError as e:
    print(f"Dependency failed: status={e.status_code} body={e.response_body}", file=sys.stderr)

Complete File Checklist

Managed Loop Orchestrator

my-orchestrator/
  orchagent.json    # manifest + loop + custom_tools + manifest.dependencies
  prompt.md         # LLM system prompt
  schema.json       # input/output schemas

Code Runtime Orchestrator (Python)

my-orchestrator/
  orchagent.json    # manifest + runtime.command + manifest.dependencies
  main.py           # your Python code using orchagent-sdk
  requirements.txt  # must include orchagent-sdk
  prompt.md         # prompt (optional, used if agent also has direct LLM features)
  schema.json       # input/output schemas

Code Runtime Orchestrator (JavaScript)

my-orchestrator/
  orchagent.json    # manifest + runtime.command + manifest.dependencies
  main.js           # your JavaScript code using orchagent-sdk
  package.json      # must include orchagent-sdk in dependencies
  prompt.md         # prompt (optional)
  schema.json       # input/output schemas

Testing Your Orchestrator

Orchestrators are hard to test because they depend on live sub-agents. Mocked fixtures solve this — they run the full LLM agent loop but return deterministic mock responses for sub-agent calls instead of hitting the network.

Step 1: Create a mocked fixture

Create tests/fixture-mock-basic.json:
{
  "description": "Orchestrator combines scan results into a security report",
  "input": {
    "code": "import os; os.getenv('DB_PASSWORD')"
  },
  "mocks": {
    "scan_secrets": {
      "findings": [{"type": "env_access", "variable": "DB_PASSWORD", "severity": "high"}]
    },
    "scan_deps": {
      "vulnerabilities": []
    }
  },
  "expected_contains": ["DB_PASSWORD", "high"]
}
The mocks keys must match the custom tool name fields in your orchagent.json. Each value is the JSON response the LLM will see when it calls that tool.

Step 2: Run the tests

orch test --verbose
The output shows the full agent loop — which tools the LLM chose to call, the mock responses injected, and whether the final output matches your expectations:
Validating agent...
  Type: agent (agent loop)
  Name: security-review
  Configuration valid

Running mocked orchestration tests...

  Provider: anthropic (claude-sonnet-4-5-20250929)
  Custom tools: scan_secrets, scan_deps
  Max turns: 25

  fixture-mock-basic.json (Orchestrator combines scan results): PASS

What gets tested

Mocked tests exercise the full reasoning pipeline:
  1. The LLM reads your prompt and the user’s input
  2. It decides which tools to call (and in what order)
  3. Custom tools return your mock responses instead of calling real sub-agents
  4. The LLM processes the results and calls submit_result
  5. The final output is validated against expected_output or expected_contains
Built-in tools (bash, read_file, write_file, list_files) still execute normally — only custom tools listed in mocks are intercepted.

File structure with tests

my-orchestrator/
  orchagent.json
  prompt.md
  schema.json
  tests/
    fixture-mock-basic.json       # mocked: tests tool selection + aggregation
    fixture-mock-edge-case.json   # mocked: tests error handling in results
    fixture-prompt-only.json      # non-mocked: tests prompt quality (LLM call only)
You can mix mocked and non-mocked fixtures in the same tests/ directory. orch test automatically routes each fixture to the correct runner based on whether it has a mocks field.
Mocked tests are ideal for CI/CD — they verify the LLM’s reasoning without requiring deployed sub-agents. See the CI/CD Guide for GitHub Actions setup.

Next Steps

Orchestration Reference

Concepts, patterns, rate limits, and troubleshooting reference

SDK Reference

Complete API reference for the orchagent-sdk package

Manifest Format

All orchagent.json fields and validation rules

Agent Types

Understand execution engines and when to use each