Multi-agent systems involve multiple agents working together to solve complex problems. While other frameworks require you to find contrived ways to have agents spawn agents, the Agentica SDK makes this trivial: pass an agentic function or an agent to another agentic function or agent.That’s it. No special orchestration layers, no graphs, no composition primitives or DSL, no message-passing protocols, no global state management. It’s just code.
Because the Agentica SDK allows you to pass any runtime value into scope — functions, objects, classes, SDKs — this naturally includes:
The spawn() function itself
Existing agents
Agentic functions
Custom orchestration logic
When you pass these to an agent or agentic function, they can use them just like anything else in scope. No schemas, no special handling. It’s just code.
Let’s start with the simplest possible multi-agent pattern: calling one agentic function from another.
from agentica import agenticfrom dataclasses import dataclass@dataclassclass CodeAnalysis: bugs: list[str] style_issues: list[str] performance_issues: list[str]# Two separate agentic functions, each specialized@agentic()async def analyze_code(code: str) -> CodeAnalysis: """Analyze code for bugs, style issues, and performance problems.""" ...@agentic()async def fix_code(code: str, analysis: CodeAnalysis) -> str: """Fix the code based on the analysis.""" ...# Compose them togetheranalysis = await analyze_code(code)fixed = await fix_code(code, analysis)
This is a multi-agent system: two separate agent invocations, each specialized for its task, composed together. However the “workflow” here is very boring — it’s entirely serial, with no conditionality. Let’s take a look at what introducing control flow is like.
In this pattern, you orchestrate the agents from your code. You decide when to call agents, what tasks to give them, and how to combine their results. You write the control flow: loops, conditionals, error handling — all the logic that coordinates the agents.This is useful when:
The workflow involves iteration, conditionals, or complex branching
You want explicit control over execution order and conditions
You need to inject custom logic between agent calls
You’re building a specific, repeatable process with clear control flow
Example: Parallelization, Iteration, and Conditional Logic
from agentica import agentic... # other logic and classes@agentic()async def evaluate_candidate(cand: Candidate) -> EvaluationResult: """Evaluate if candidate meets our technical and cultural requirements.""" ...@agentic()async def create_interview_plan(cand: Candidate, eval: EvaluationResult) -> InterviewPlan: """Create a tailored interview plan focusing on the candidate's strengths and gaps.""" ...@agentic()async def create_rejection_letter(cand: Candidate, eval: EvaluationResult) -> RejectionLetter: """Generate a polite rejection letter with constructive feedback.""" ...async def pipeline(candidates: list[Candidate]): # Evaluate all candidates in parallel evaluations = await asyncio.gather(*[evaluate_candidate(c) for c in candidates]) to_be_interviewed: dict[Candidate, InterviewPlan] = dict() to_be_rejected: dict[Candidate, RejectionLetter] = dict() # Branch based on results async def pick_candidate(candidate: Candidate, evaluation: EvaluationResult): if evaluation.is_qualified: to_be_interviewed[candidate] = await create_interview_plan(candidate, evaluation) else: to_be_rejected[candidate] = await create_rejection_letter(candidate, evaluation) async with asyncio.TaskGroup() as tg: for candidate, evaluation in zip(candidates, evaluations): tg.create_task(pick_candidate(candidate, evaluation)) # Do something with the data ...
Notice how you write the parallelization, the conditional branching, the iteration logic. Agents are just functions you call — the control flow is yours. This is just regular programming with agents as building blocks.
Now here’s where the Agentica SDK’s design truly shines. Instead of you controlling the flow, let an agent control it. A coordinator agent manages multiple worker agents, each handling independent sub-tasks.This is useful when:
The number of sub-tasks isn’t known in advance
The workflow needs to adapt based on intermediate results
You want agents to decide how to parallelize work
Sub-tasks benefit from specialization
The problem is too complex for fixed orchestration logic
Let’s start with the most constrained approach: pass pre-configured agent objects with specific capabilities.
from agentica import spawn# Create specialized agents with permanent tool accessdata_analyst = await spawn( premise="You are a data analyst", scope={"query_database": query_database} # ← Functions at spawn time)market_analyst = await spawn( premise="You are a market analyst", scope={"get_market_data": get_market_data} # ← Functions at spawn time)coordinator = await spawn(premise="You are a research coordinator")report = await coordinator.call( ComprehensiveReport, # ← The rigid class defining the report type f"Research {topic} using the data analyst and market analyst", data_analyst=data_analyst, # ← Pass agent objects market_analyst=market_analyst # ← Just like anything else)
The coordinator can now invoke the specialist agents, pass data between them, and decide when to use which one — all dynamically based on the task.
For maximum flexibility, pass spawn itself so the coordinator can create any agents it needs:
from agentica import spawn... # other logic and classesasync def web_search(query: str) -> list[SearchResult]: ...# Create a coordinator with the ability to spawncoordinator = await spawn(premise="You are a research coordinator who can spawn sub-agents")# Give it spawn and tools - it decides how many sub-agents it needsreport = await coordinator.call( ResearchReport, # ← The rigid return type defining the report f"Research '{topic}' thoroughly using sub-agents for different aspects", spawn=spawn, # ← The interesting part happens here web_search=web_search # ← A function that may be passed to sub-agents)
What just happened? The coordinator agent can now:
Decide how many sub-agents it needs
Spawn them dynamically using spawn()
Assign them specific tasks
Wait for their results
Synthesize everything into a final report
You didn’t write any of that orchestration logic. The agent figured it out.
Give each agent a focused premise that defines its role:
researcher = await spawn( premise="You are a researcher who finds and verifies facts. Do not write content.")writer = await spawn( premise="You are a writer who creates engaging content from facts. Do not do research.")