CrewAI Process.hierarchical
CrewAI's hierarchical process is a supervisor pattern. Here it is in one file.
Swapping one enum value changes who decides what runs next. The mechanism behind it is a manager model choosing among workers, and it predates every framework that ships it.
· 8 min read
In CrewAI you change one enum value, from a sequential process to a hierarchical one, and the behaviour of the whole crew changes. Tasks stop running in the order you wrote them. A manager appears. It is presented as a configuration flag, which makes it easy to miss that you just swapped one architecture for a different one.
What you had before
Sequential is a loop over a list you wrote. The routing decision was made by you, at the time you wrote the code, and it is free.
// Sequential: you decided the order at design time.
async function sequential(tasks: Task[], workers: Worker[]) {
const results: string[] = [];
for (const task of tasks) {
const worker = workers.find((w) => w.name === task.assignedTo)!;
results.push(await worker.run(task.description, results));
}
return results;
}What the flag gives you
The manager is not an orchestrator object. It is a model call whose answer is the name of the next worker.
// Hierarchical: a model decides the order at run time.
interface Worker {
name: string;
description: string; // the manager reads this, not your code
run: (instruction: string, context: string[]) => Promise<string>;
}
async function supervise(
goal: string,
workers: Worker[],
decide: (prompt: string) => Promise<{ worker: string; instruction: string } | { done: true }>,
maxSteps = 8,
) {
const transcript: string[] = [];
for (let step = 0; step < maxSteps; step++) {
const roster = workers.map((w) => `- ${w.name}: ${w.description}`).join("\n");
const decision = await decide(
`Goal: ${goal}\n\nWorkers:\n${roster}\n\nWork so far:\n${
transcript.join("\n") || "(nothing yet)"
}\n\nPick the next worker and the instruction, or say done.`,
);
if ("done" in decision) return transcript;
const worker = workers.find((w) => w.name === decision.worker);
if (!worker) {
transcript.push(`manager picked unknown worker: ${decision.worker}`);
continue; // let it read its own mistake and retry
}
transcript.push(`${worker.name}: ${await worker.run(decision.instruction, transcript)}`);
}
return transcript;
}Read the two together and the trade is obvious. Sequential routing costs nothing and cannot adapt. Supervised routing adapts and costs one model call per step, before any worker has done anything.
The parts that decide whether it works
The worker descriptions are the program. The manager routes on those strings and nothing else. Two workers with overlapping descriptions produce a manager that picks between them by coin flip, and the fix is a prose edit, not a code change. This is the single biggest difference from a router you wrote yourself, where the routing logic is inspectable.
The transcript is the context, and it grows. Every worker output is appended and fed to the next manager decision. Long runs bury the goal under intermediate output, and the manager starts routing on the noise instead of the objective. Summarising worker results before they enter the transcript is usually the difference between eight useful steps and eight expensive ones.
It serialises work that could be parallel. One decision, one worker, repeat. If three of your workers are independent lookups, a supervisor will run them one after another while a fixed pipeline would have run them at once. Adaptive routing is worth paying for when the path genuinely varies; it is pure overhead when it does not.
The step budget is a safety feature. A manager with no cap that keeps picking a worker returning an unhelpful result will keep picking it. The unknown-worker branch above is deliberate for the same reason: a hard failure there ends the run, while feeding the mistake back gives the model one chance to correct itself.
Why the name matters more than the flag
Once you can see it as a supervisor, you recognise it everywhere: a router node in a graph framework, an orchestrator agent handing off to sub-agents, a planner that emits a worker name. Those are the same mechanism with four names, and each will be renamed again.
The decision underneath never changes: does the routing need to adapt to what earlier steps found, and is that worth a model call per step. That question is answerable without knowing any framework at all.
The same mechanism, with its tradeoffs
Every pattern page runs its code on the page, in TypeScript, Python and Rust.