n4nAI

Debugging CrewAI hierarchical process delegation

A step-by-step guide to diagnosing and fixing delegation failures in CrewAI hierarchical crews, with logging, instrumentation, and verification techniques.

n4n Team4 min read921 words

Audio narration

Coming soon — every post will get a voice note here.

Hierarchical crews in CrewAI introduce a manager agent that decides which specialist agent handles each task. When delegation breaks — tasks stall, wrong agents are picked, or the manager loops — you need systematic crewai hierarchical process delegation debugging rather than guesswork. This walkthrough gives you a reproducible process: enable the right logs, instrument the manager’s decision path, add guardrails, and verify the fix with a minimal test case.

Step 1: Reproduce with a minimal crew

Start by stripping your crew to the smallest configuration that still shows the symptom. Remove custom tools, memory, and external APIs. Keep one manager and two specialists with clearly distinct roles.

# minimal_crew.py
from crewai import Agent, Task, Crew, Process

researcher = Agent(
    role="Researcher",
    goal="Find one fact about the topic",
    backstory="You retrieve a single verifiable fact.",
    allow_delegation=False,
    verbose=True,
)

writer = Agent(
    role="Writer",
    goal="Summarize the fact in one sentence",
    backstory="You write concise summaries.",
    allow_delegation=False,
    verbose=True,
)

manager = Agent(
    role="Manager",
    goal="Decide which specialist handles the request",
    backstory="You route tasks to Researcher or Writer.",
    allow_delegation=True,
    verbose=True,
)

research_task = Task(
    description="Research the boiling point of water at sea level",
    expected_output="One sentence with the temperature and unit",
    agent=researcher,
)

write_task = Task(
    description="Summarize the research result",
    expected_output="One sentence summary",
    agent=writer,
)

crew = Crew(
    agents=[manager, researcher, writer],
    tasks=[research_task, write_task],
    process=Process.hierarchical,
    manager_agent=manager,
    verbose=True,
)

if __name__ == "__main__":
    result = crew.kickoff()
    print(result)

Run it and confirm the failure mode: does the manager pick the wrong agent, repeat the same delegation, or hang?

python minimal_crew.py

Verify success: the script finishes in under 30 seconds and prints a coherent result. If it loops or errors, you have a reproducible case.

Step 2: Enable full manager reasoning logs

CrewAI’s verbose=True shows task outputs but not the manager’s internal chain-of-thought. Patch the manager’s LLM call to capture the raw prompt and completion.

# debug_logging.py
import json
from crewai import Agent, Task, Crew, Process
from crewai.llm import LLM
from langchain_core.callbacks import BaseCallbackHandler

class ManagerLogger(BaseCallbackHandler):
    def on_llm_start(self, serialized, prompts, **kwargs):
        print("\n=== MANAGER PROMPT ===")
        for p in prompts:
            print(p[:2000])
    
    def on_llm_end(self, response, **kwargs):
        print("\n=== MANAGER RESPONSE ===")
        for gen in response.generations:
            for chunk in gen:
                print(chunk.text[:2000])

logged_llm = LLM(
    model="gpt-4o-mini",
    callbacks=[ManagerLogger()],
)

manager = Agent(
    role="Manager",
    goal="Decide which specialist handles the request",
    backstory="You route tasks to Researcher or Writer.",
    allow_delegation=True,
    verbose=True,
    llm=logged_llm,
)

Re-run the minimal crew. The manager’s prompt will show the exact context it receives: available agents, their descriptions, task history, and the current request. Look for:

  • Missing or truncated agent descriptions
  • Task history that grows without bound
  • Ambiguous delegation instructions in the system prompt

Verify success: you can see the manager’s reasoning for each delegation decision in the console output.

Step 3: Instrument delegation outcomes

Add a wrapper that records every delegation attempt — which agent was chosen, the task description, and whether the delegated task succeeded.

# instrumentation.py
from dataclasses import dataclass, field
from typing import List
from crewai import Agent, Task, Crew, Process

@dataclass
class DelegationRecord:
    step: int
    requested_agent: str
    actual_agent: str
    task_description: str
    success: bool
    error: str = ""

delegation_log: List[DelegationRecord] = []
step_counter = 0

class InstrumentedCrew(Crew):
    def _execute_task(self, task, agent, context=None):
        global step_counter
        step_counter += 1
        record = DelegationRecord(
            step=step_counter,
            requested_agent=agent.role,
            actual_agent=agent.role,
            task_description=task.description[:200],
            success=False,
        )
        try:
            result = super()._execute_task(task, agent, context)
            record.success = True
            return result
        except Exception as e:
            record.error = str(e)
            raise
        finally:
            delegation_log.append(record)

# Use InstrumentedCrew instead of Crew
crew = InstrumentedCrew(
    agents=[manager, researcher, writer],
    tasks=[research_task, write_task],
    process=Process.hierarchical,
    manager_agent=manager,
    verbose=True,
)

if __name__ == "__main__":
    result = crew.kickoff()
    print("\n=== DELEGATION LOG ===")
    for r in delegation_log:
        status = "OK" if r.success else f"FAIL: {r.error}"
        print(f"Step {r.step}: {r.requested_agent} -> {r.actual_agent} | {status}")
        print(f"  Task: {r.task_description}")

Verify success: after run completion, the delegation log shows a clean sequence with no repeated steps and all success=True.

Step 4: Add explicit delegation rules to the manager

The most common failure is an underspecified manager backstory. Give the manager a structured decision framework in its system prompt.

manager = Agent(
    role="Manager",
    goal="Route each task to exactly one specialist",
    backstory=(
        "You are a routing manager. Follow these rules strictly:\n"
        "1. If the task asks for facts, data, or research -> delegate to Researcher\n"
        "2. If the task asks for writing, summarizing, or formatting -> delegate to Writer\n"
        "3. Never delegate to yourself.\n"
        "4. Never delegate the same task twice.\n"
        "5. After a specialist completes a task, consider the workflow done unless a new task appears."
    ),
    allow_delegation=True,
    verbose=True,
    llm=logged_llm,
)

Test again. The manager should now route deterministically.

Verify success: delegation log shows Researcher for the research task, Writer for the write task, no self-delegation, no repeats.

Step 5: Bound the manager’s context window

Hierarchical crews accumulate the full conversation history in the manager’s prompt. With many tasks, this exceeds the model’s context limit and truncates agent descriptions. Implement a sliding window that keeps only the last N interactions.

# context_window.py
from crewai import Crew
from typing import List, Dict

class BoundedContextCrew(Crew):
    MAX_HISTORY = 6  # keep last 3 task cycles (request + response)
    
    def _get_manager_prompt(self, task, agents, history):
        # history is a list of dicts with 'task', 'agent', 'output'
        trimmed = history[-self.MAX_HISTORY:]
        return super()._get_manager_prompt(task, agents, trimmed)

crew = BoundedContextCrew(
    agents=[manager, researcher, writer],
    tasks=[research_task, write_task],
    process=Process.hierarchical,
    manager_agent=manager,
    verbose=True,
)

For longer workflows, increase MAX_HISTORY or summarize older entries instead of dropping them.

Verify success: manager prompt size stays stable across 10+ task cycles (check via the ManagerLogger from Step 2).

Step 6: Handle rate limits and provider failures

When the manager’s LLM call fails — rate limit, timeout, or provider degradation — the crew can hang or retry indefinitely. Wrap the LLM with a retry policy and fallback.

# resilient_llm.py
from crewai.llm import LLM
from tenacity import retry, stop_after_attempt, wait_exponential_jitter

class ResilientLLM(LLM):
    @retry(
        wait=wait_exponential_jitter(initial=1, max=30),
        stop=stop_after_attempt(3),
        reraise=True,
    )
    def call(self, messages, **kwargs):
        return super().call(messages, **kwargs)

resilient_llm = ResilientLLM(model="gpt-4o-mini", callbacks=[ManagerLogger()])
manager = Agent(..., llm=resilient_llm)

If you route through a gateway that exposes multiple providers, you can also implement provider-level fallback. For example, n4n.ai forwards x-provider routing hints and returns x-fallback-provider headers when the primary is degraded — your client can read that header and switch automatically on the next request.

Verify success: simulate a rate limit (set a low RPM quota on your API key) and confirm the crew completes after retries without manual intervention.

Step 7: Write a regression test

Encode the expected delegation sequence as a test that fails if the manager deviates.

# test_delegation.py
import pytest
from minimal_crew import crew, delegation_log

def test_hierarchical_delegation_sequence():
    result = crew.kickoff()
    
    assert len(delegation_log) == 2, f"Expected 2 delegations, got {len(delegation_log)}"
    
    # Step 1: research task -> Researcher
    assert delegation_log[0].actual_agent == "Researcher"
    assert delegation_log[0].success is True
    
    # Step 2: write task -> Writer
    assert delegation_log[1].actual_agent == "Writer"
    assert delegation_log[1].success is True
    
    # No self-delegation
    for record in delegation_log:
        assert record.actual_agent != "Manager"
    
    print("Regression test passed")

Run with pytest test_delegation.py -v. This catches regressions when you upgrade CrewAI, change models, or modify agent descriptions.

Verify success: test passes consistently across 5 consecutive runs.

Step 8: Profile token usage per delegation

Delegation loops often stem from the manager re-explaining the same context. Track tokens per manager turn to spot bloat.

# token_tracker.py
from crewai.llm import LLM
from dataclasses import dataclass

@dataclass
class TokenSnapshot:
    step: int
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int

token_history: List[TokenSnapshot] = []

class TokenTrackingLLM(LLM):
    def call(self, messages, **kwargs):
        response = super().call(messages, **kwargs)
        usage = response.usage if hasattr(response, 'usage') else None
        if usage:
            token_history.append(TokenSnapshot(
                step=len(token_history) + 1,
                prompt_tokens=usage.prompt_tokens,
                completion_tokens=usage.completion_tokens,
                total_tokens=usage.total_tokens,
            ))
        return response

tracking_llm = TokenTrackingLLM(model="gpt-4o-mini", callbacks=[ManagerLogger()])
manager = Agent(..., llm=tracking_llm)

After a run, print the history:

for t in token_history:
    print(f"Step {t.step}: {t.prompt_tokens} prompt + {t.completion_tokens} completion = {t.total_tokens} total")

Verify success: prompt tokens stay roughly constant across steps (within 20%). Growth indicates accumulating context that needs the sliding window from Step 5.

Step 9: Validate with a multi-turn scenario

Extend the minimal crew to three sequential tasks that require alternating specialists. This exposes handoff bugs that single-task runs miss.

# multiturn_crew.py
from crewai import Agent, Task, Crew, Process

tasks = [
    Task(description="Research the capital of France", expected_output="City name", agent=researcher),
    Task(description="Write a one-sentence travel blurb for that city", expected_output="One sentence", agent=writer),
    Task(description="Research the population of that city", expected_output="Number with year", agent=researcher),
    Task(description="Summarize both facts in a tweet", expected_output="Under 280 chars", agent=writer),
]

crew = InstrumentedCrew(
    agents=[manager, researcher, writer],
    tasks=tasks,
    process=Process.hierarchical,
    manager_agent=manager,
    verbose=True,
)

if __name__ == "__main__":
    result = crew.kickoff()
    print("\n=== FINAL RESULT ===")
    print(result)
    print("\n=== DELEGATION LOG ===")
    for r in delegation_log:
        status = "OK" if r.success else f"FAIL: {r.error}"
        print(f"Step {r.step}: {r.actual_agent} | {status} | {r.task_description[:80]}")

Expected delegation sequence: Researcher, Writer, Researcher, Writer.

Verify success: log matches expected sequence exactly, no repeated research or write steps, total runtime under 60 seconds.

Step 10: Document the debug checklist

Create a DEBUGGING.md in your repo so the next engineer (or future you) follows the same path.

# CrewAI Hierarchical Delegation Debug Checklist

1. **Reproduce minimally**`minimal_crew.py` with 2 specialists, 2 tasks
2. **Capture manager reasoning**`ManagerLogger` callback on manager's LLM
3. **Log every delegation**`InstrumentedCrew` records agent, task, success
4. **Explicit routing rules** — manager backstory with numbered decision rules
5. **Bound context window**`BoundedContextCrew` keeps last N history entries
6. **Resilient LLM** — retry with exponential backoff, provider fallback
7. **Regression test**`test_delegation.py` asserts exact delegation sequence
8. **Token profiling**`TokenTrackingLLM` watches for prompt bloat
9. **Multi-turn validation** — 4+ task alternating sequence
10. **CI gate** — run regression test on every PR

Common failure patterns and fixes

Symptom Root cause Fix
Manager delegates to itself Backstory doesn’t forbid self-delegation Add rule “Never delegate to yourself”
Same task delegated repeatedly Manager doesn’t see completion in history Ensure task outputs are appended to history; bound window doesn’t drop the latest
Wrong specialist chosen Agent descriptions overlap or are vague Rewrite role/goal/backstory with disjoint keywords; add routing rules
Crew hangs on rate limit No retry logic on manager LLM Wrap LLM with tenacity; use gateway with automatic fallback
Prompt tokens grow unbounded No context window management Implement sliding window or summarization
Non-deterministic routing Temperature > 0 on manager Set temperature=0 on manager’s LLM

When to escalate beyond the crew

If the manager consistently misroutes despite explicit rules, the model may lack the reasoning capacity for your routing complexity. Options:

  • Use a stronger model for the manager only (keep specialists on cheaper models)
  • Replace hierarchical process with a deterministic router function that selects the agent, then call Crew(process=Process.sequential) with that agent
  • Add a classification step before the crew: a lightweight LLM call that tags the task type, then map tags to agents in code

The hierarchical process is convenient but not magic. Treat the manager as a component you instrument, test, and version like any other service.

Tagscrewaihierarchical-processtroubleshootingdelegation

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All crewai sequential vs hierarchical crews posts →