In CrewAI Flows, how does @router decide which branch runs next?
answer
- a listener whose return value is a decision
- returns a name, not data
- downstream steps subscribe by string
- unselected branches cost nothing
- unmatched label ends that path quietly
basics
~20 sA @router method is triggered like a listener but returns a string label instead of data. Steps decorated with @listen of that exact label run next; branches whose label was not returned simply never execute.
solid answer
~50 s`@router(previous_step)` marks a method that fires when `previous_step` completes, exactly like a listener — the difference is in what its return value means. A router returns a **string label**, and that label is the event other steps subscribe to with `@listen("label")`. So the router is where the branch decision lives, and the labels are the branch names. Because the router is ordinary Python, the decision can be a deterministic check on `self.state` (score above threshold, validation failed, record already exists) or the parsed output of a crew you ran in a previous step. Only the branch matching the returned label runs; the others are never invoked, which is the whole point — you get skipped work rather than an agent politely deciding not to act. If the returned label matches no listener, that path simply ends.
go deeper
Know that @router returns a string naming the branch, and that steps join a branch by listening for that exact string. Be able to sketch a two-label approve/reject split.
Explain that the router is triggered like a listener but its return value is a label rather than data, that branch inputs must therefore come from state, and that unselected branches never execute.
Show that you route on deterministic state where possible, validate model-produced labels against a closed set, and know the failure mode where a skipped branch starves a downstream join and the flow ends silently.
Argue about how much control flow belongs in routers at all: a handful of structural decisions is a design, a dozen labels is a state machine someone will have to maintain, and past that an agent choosing tools is the better abstraction.
## The problem routers solve `@listen` gives you edges that always fire. Real workflows need edges that fire *conditionally*: retry or give up, escalate to a human or auto-approve, run the expensive research crew or return the cached answer. `@router` is the construct that turns a runtime value into a choice of edge. ## Mechanics A router is declared like a listener — `@router(previous_step)` — and it is triggered the same way, when `previous_step` completes. What differs is the contract on its return value. A listener's return value is *data* handed to whoever listens to it. A router's return value is a **label**: a plain string naming the branch to take. Downstream steps subscribe to that label by string, `@listen("approved")` or `@listen("rejected")`. The consequence is that a router does not pass data down the branch. Anything the branch needs must already be on `self.state`. In practice a router body reads state, decides, and returns a short constant — it is not the place to do work. ## Where the decision comes from Three flavours show up in real code, and interviewers care that you can tell them apart: 1. **Deterministic** — `if self.state.confidence < 0.6: return "needs_review"`. No model involved. This is the cheapest and most testable branch and should be the default. 2. **Model-informed but code-decided** — an earlier step runs a small crew or a single LLM call that classifies something onto state; the router branches on that classified value. The model produces a fact, your code makes the decision. 3. **Model-decided** — the router returns whatever label an LLM emitted. This works, but you have moved control flow back into the model, which is what Flows exist to avoid. Constrain it: validate the returned string against the known label set and fall back to a safe branch, because a model that returns `"Approved."` instead of `"approved"` will silently match nothing. ## What happens to the branches not taken Nothing. They are never invoked. This is materially different from letting a hierarchical crew decide: there, an agent still consumes tokens reading the task and reasoning about whether to act. Router branches that are not selected cost zero tokens and zero latency, which is why moving a coarse decision from the manager agent into a router is often the single biggest cost win in a CrewAI system. It is also the main foot-gun. Because a skipped branch produces no event, any join that was waiting on all branches of a router will never complete — the flow just ends quietly with the join step never running. When a flow "stops early for no reason", a router whose unselected branch fed a fan-in condition is the usual culprit. The second foot-gun is the unmatched label. If the router returns `"escalate"` and no step listens for `"escalate"`, there is no error — that path simply terminates. Keep the label set small, define it as constants rather than inline literals, and assert that every label has a listener. ## Composing routers Routers chain like anything else: a branch step can itself be routed, giving nested decisions. A branch can also be a no-op step that exists only so several labels converge on a common continuation. And routers pair naturally with the fan-in conditions — the safe pattern is that branches which may be skipped are joined with an "any of" condition rather than an "all of" condition, so a skipped branch does not stall the join. ## Testing A router is the most testable part of a flow because it is a pure function of state in the deterministic case: construct the flow, set state, call the router method directly, assert the returned string. Doing that removes an entire class of production surprises and costs nothing in tokens. If your router is not unit-testable, it is doing too much work inside itself. ## When a router is the wrong tool If the branch condition is really "which of twelve specialist skills does this request need", enumerating twelve labels in a router is worse than letting an agent pick a tool. Routers are for a handful of structural decisions in a known workflow, not for open-ended dispatch.
- What happens if the router returns a label no step listens for?That path simply ends — there is no error and no default branch. The flow finishes with everything downstream of the intended branch unexecuted. Because a model-generated label is easy to get slightly wrong, validate the returned string against a known set and fall back to an explicit safe label instead of returning free text.
- How can a router make a downstream join never fire?A branch that is not selected produces no completion event. Any step waiting for *all* of several triggers, one of which sits on the unselected branch, will never be satisfied, and the flow ends with that step never running. Join branches that a router may skip with an any-of condition, or route into a shared continuation step instead.
- Should the routing decision itself come from an LLM?Prefer not. The cheapest and most debuggable routers read a value off state and branch in plain Python. If a model must classify, do that in a separate step whose only job is to produce a constrained value on state, then let the router branch on that value deterministically — it keeps the decision testable and the label set closed.
saying these in an interview costs you the question
- Saying @router returns data for the branch to consume
- Thinking unselected branches still run and just do nothing
- Assuming an unmatched label raises an error or hits a default
- Confusing @router with picking which agent handles a task
- Putting the branch's real work inside the router method