@Workflow¶
Spec
Full specification: specs/workflow.md
· Item: AJ-6
Purpose¶
@Workflow is a class decorator that turns a Python class into a
multi-agent orchestrator. It accepts a list of @Agent
classes and either an LLM coordinator= model string (default mágico)
or an overridden route() method (escape hatch). The injected
run(message) / stream(message) methods mirror @Agent so the
primitive composes with @Stream and @Eval
without special-casing the host.
Reach for @Workflow the moment a single prompt stops fitting a single
agent — triage that hands off to billing, technical, or sales
specialists.
Signature¶
def Workflow(
*,
agents: list[type],
coordinator: str | None = None,
integrations: list[type] | None = None,
max_steps: int = 10,
) -> Callable[[type[T]], type[T]]: ...
Quick example¶
from ajolopy import Agent, Workflow
@Agent(model="claude-haiku-4-5", system="You triage support requests.")
class Triage:
"""Classify the user's message: billing, technical, or general."""
@Agent(model="claude-opus-4-7", system="You handle billing.")
class Billing:
"""Refunds, invoices, subscriptions."""
@Agent(model="claude-opus-4-7", system="You handle technical issues.")
class Technical:
"""Bugs, errors, integration help."""
@Workflow(
coordinator="claude-opus-4-7",
agents=[Triage, Billing, Technical],
)
class SupportTeam:
"""Route support requests to the right specialist."""
async def main() -> None:
team = SupportTeam()
answer = await team.run("my refund hasn't arrived, order #4392")
print(answer)
Kwargs¶
| Kwarg | Type | Default | Description |
|---|---|---|---|
agents |
list[type] |
required | Non-empty list of @Agent-decorated classes. Order is preserved. |
coordinator |
str \| None |
None |
Model string for the LLM coordinator. Required unless route() is overridden. |
integrations |
list[type] \| None |
None |
List of @MCP classes — tools are exposed to the coordinator AND each delegated agent. |
max_steps |
int |
10 |
Cap on the coordinator's tool-calling loop. Must be >= 1. |
Escape hatches¶
route()override. Overrideasync def route(self, message, context) -> typeto pick an agent class deterministically. The coordinator never runs;max_stepsis unused.- Per-invocation context. Forward kwargs through
run(message, **context)/stream(message, **context); they reachroute()as acontext: dictparameter (the LLM coordinator ignores them in v0.1). - Compose with
@Stream.@Workflowclasses can carry@Streammethods; the workflow event stream becomes SSE JSON events with no extra wiring. - Subclass
WorkflowRuntimefor unusual coordinator policies, span semantics, or hand-off accounting.
Common gotchas¶
- Exactly one of
coordinator=orroute()must be present. Passing both logs anINFOmessage (route()wins); passing neither raisesWorkflowConfigErrorat decoration time. - Each item in
agents=must itself carry@Agent. Plain classes are rejected with the class name in the error message. wf.stream(...)yields dicts (typediscriminator:handoff/agent_result/token/done), never bare strings. Clients must ignore unknowntypevalues forward-compatibly.- A delegated agent's
AgentErrordoes NOT propagate; it surfaces as anagent_resultevent withis_error=Trueon the tool result, and the coordinator decides whether to retry or abandon. WorkflowMaxStepsErrorcarriesmax_stepsandstep_count. If you see it in production, raise the cap — but inspect the trace first.
See also¶
@Agent— the building block this primitive orchestrates.@Stream— turn workflow events into SSE.@MCP— share MCP tools across coordinator and delegates.@Eval— regression-detect entire workflows.- Spec:
specs/workflow.md.