In CrewAI Flows, when do listeners using or_() and and_() fire?
answer
- one is a race, one is a join
- wrapped around triggers inside @listen
- any-of can fire more than once
- all-of dispatches exactly once
- a skipped branch starves the join
basics
~20 sor_() fires the listener each time any of the named steps finishes, so it can run more than once. and_() holds the listener until every named step has finished and then fires it once. or_() is a race, and_() is a join.
solid answer
~40 sBoth are condition helpers you wrap around the triggers in a `@listen(...)` decorator: `@listen(or_(path_a, path_b))` and `@listen(and_(crew_one, crew_two))`. With `or_()`, the listener is dispatched on **each** matching completion — if both branches eventually run, the listener runs twice, so its body must be idempotent or must guard on `self.state`. With `and_()`, CrewAI waits until all named steps have completed and dispatches the listener a single time, which is the fan-in for parallel work: two `@start` methods or two branches each running a crew, joined by one step that combines their results from state. The trap is combining `and_()` with a router: a branch that the router did not select never emits, so the join never becomes satisfiable and the flow ends with that step silently unexecuted.
go deeper
Remember the plain meaning: any-of runs the listener when any named step finishes, all-of waits for every named step. Both are written inside the @listen decorator around the trigger names.
Explain that the any-of form can dispatch more than once and the all-of form dispatches exactly once, and that joined branches communicate their results through state rather than through return values.
Demonstrate the failure you have actually hit: a router arm that was not selected starves an all-of join, the flow ends quietly, and nothing errors. Talk about idempotent listeners and one state field per parallel branch.
Judge when flow edges stop being the right place to express concurrency. There are no timeouts, retries or run history on these joins, so past a few of them the workload deserves a real orchestrator rather than a deeper graph.
## Why conditions exist A bare `@listen(step)` is a single edge: one trigger, one listener. Real graphs need two other shapes — *converge from alternatives* and *wait for everything* — and `or_()` and `and_()` are those shapes. Both are imported alongside the decorators (`from crewai.flow.flow import Flow, start, listen, router, or_, and_`) and both are used inside the `@listen` decorator rather than as decorators themselves. ## or_() — converge from alternatives `@listen(or_(a, b, c))` subscribes the method to *any* of the named steps. Semantically it is "whichever of these happens, run me". The critical detail, and the one interviews probe, is that it is not a one-shot: if two of the named steps both complete during the run, the listener is dispatched twice. That is fine when the branches are genuinely exclusive — the classic case is joining the arms of a router back onto a common continuation, where only one arm ever ran. It is a bug when the branches are parallel: a "send the summary email" step wired with `or_()` to two crews that both run will send two emails. The defences are the usual idempotency ones: check a flag on `self.state` at the top of the listener and return early if it is already set, or make the effect naturally idempotent (upsert rather than insert). ## and_() — wait for everything `@listen(and_(a, b))` holds the listener until every named step has completed, then dispatches it exactly once. This is the fan-in that makes parallel work useful. The canonical shape is two entry points, or two branches, each running its own crew and writing its result to state, joined by a single step that reads both results off state and produces the combined answer. Note what the join does *not* give you: it does not pass you both return values. A joined listener gets no meaningful upstream payload, so the parallel branches must communicate through `self.state`. Design the state schema for that up front — a field per branch, written by exactly one branch — because two branches writing the same field is a race you will not enjoy debugging. ## The interaction that breaks flows The single most common production failure with these helpers is combining `and_()` with a router. A router branch that is not selected never runs, and a step that never runs never emits a completion event. If that step is one of the triggers inside an `and_()`, the join's condition can never be satisfied. There is no timeout and no error: the flow simply finishes with the join step and everything downstream of it unexecuted, which looks exactly like "the flow stopped early for no reason". The fixes are structural. Either join the possibly-skipped branches with `or_()` and reconcile inside the listener by reading state, or make every router arm terminate in the same continuation step so the arms converge before the join, or ensure the `and_()` only names steps that are unconditionally reached. ## Choosing between them Ask what you want when only one trigger fires. If the answer is "run anyway with whatever we have", that is `or_()`. If the answer is "wait — the result is meaningless without both", that is `and_()`, and you have accepted that a missing branch stalls the path. Neither is a scheduler: there is no timeout knob on a join and no retry on a starved one. If you need "wait for both, but proceed after 30 seconds", that is code you write inside a step, typically by running the parallel work yourself with async primitives rather than by expressing it as flow edges. ## Testing and inspection Because the wiring is declarative, `flow.plot()` renders the graph including the join and race edges, and it is the fastest way to confirm that the branch you thought feeds a join actually does. Beyond that, the meaningful tests are behavioural: run the flow with the router forced down each arm and assert that the terminal step actually executed in every case. A join that is only exercised on the happy path is the one that will strand you. ## Keeping the graph small These helpers make it tempting to express arbitrarily rich concurrency as flow edges. Resist past a point: a graph with several nested joins is a workflow engine you now maintain without one's tooling — no visual run history, no retries, no per-step timeouts. Two or three joins is expressive; a dozen means the workload wanted a real orchestrator.
- Why does a listener wired with or_() sometimes run twice?Because or_() dispatches on each matching completion, not on the first one. If two of the named triggers both complete during a run — parallel branches rather than exclusive router arms — the listener is invoked once per completion. Guard the body with a flag on self.state, or make its effect idempotent, before wiring any side effect this way.
- How do the two branches joined by and_() actually hand their results to the join step?Through self.state. The joined listener receives no combined payload of the branch return values, so each branch must write its own result to its own state field and the join reads them back. Give every parallel branch a distinct field; two branches writing the same field is a race with no ordering guarantee.
- What do you do when a join must proceed even if one branch never finishes?Not express it as and_(), because there is no timeout on a join and a starved one just stalls that path silently. Either join with or_() and reconcile inside the listener by checking which results are present on state, or run the parallel work explicitly inside one async step where you control the timeout yourself.
saying these in an interview costs you the question
- Saying or_() fires only once, on the first trigger
- Assuming and_() times out or errors if a branch never runs
- Expecting the join step to receive both branches' return values
- Using and_() across router arms that are mutually exclusive
- Treating these helpers as a general concurrency scheduler