n4nAI

Building a support agent in CrewAI that escalates to a human

A hands-on practical guide to building a CrewAI support agent with human escalation logic, runnable Python code, and resilient LLM routing.

n4n Team2 min read506 words

Audio narration

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

A crewai support agent human escalation flow lets you automate tier-1 support while keeping a clean path to a person when the model hits its limits. This tutorial builds a minimal but production-shaped pipeline: a triage agent, a resolution agent, and an escalation tool that opens a human ticket. You will run it locally and see exactly where the human handoff triggers.

Prerequisites

  • Python 3.10 or newer
  • pip install crewai crewai_tools openai
  • An API key for an OpenAI-compatible LLM. If you want automatic provider fallback when a model is rate-limited, point at the n4n.ai OpenAI-compatible endpoint instead of a single vendor.
  • Familiarity with basic Python and environment variables

Set your key in the shell before running anything:

export OPENAI_API_KEY="sk-..."
# or for the gateway:
export N4N_API_KEY="sk-n4n-..."

Step 1: Define the escalation tool

The handoff to a human is just a tool call. In a real system this would POST to Zendesk or an internal queue; here we return a fake ticket ID so the logic is observable.

from crewai_tools import tool

@tool("HumanEscalation")
def open_human_ticket(query: str, transcript: str) -> str:
    """Use this when the customer issue cannot be resolved automatically.
    Pass the original query and the conversation transcript so far."""
    ticket_id = "HUM-" + str(abs(hash(query)) % 100000)
    # Imagine: requests.post("https://helpdesk.internal/tickets", json={...})
    return f"Escalated to human. Ticket {ticket_id} created from transcript: {transcript[:60]}..."

The decorator registers the function as a CrewAI tool with a name and description the LLM can reason about.

Step 2: Configure the LLM and agents

We instantiate one LLM and attach it to two agents. Using a gateway base URL means a degraded provider gets retried on a different one without code changes.

from crewai import LLM, Agent

llm = LLM(
    model="openai/gpt-4o-mini",
    base_url="https://api.n4n.ai/v1",  # OpenAI-compatible, 240+ models, automatic fallback
    api_key="sk-n4n-...",
    temperature=0.2,
)

triage_agent = Agent(
    role="Support Triage",
    goal="Decide if the request is auto-resolvable or must reach a human.",
    backstory="You enforce routing policy for a SaaS support desk.",
    llm=llm,
    tools=[open_human_ticket],
    verbose=True,
)

resolution_agent = Agent(
    role="Tier-1 Support",
    goal="Resolve common billing and login issues using concise answers.",
    backstory="You are a senior support rep who hates fluff.",
    llm=llm,
    verbose=True,
)

The verbose=True flag prints agent thoughts so you can watch the escalation decision happen.

Step 3: Triage task and checkpoint output

We give the triage agent a strict policy: escalate refunds over $500 or any reported outage. Otherwise it returns RESOLVABLE.

from crewai import Task, Crew, Process

triage_task = Task(
    description=(
        "Review this support request: '{query}'. "
        "If it mentions a refund over $500 or a service outage, call HumanEscalation. "
        "Otherwise reply with exactly the word RESOLVABLE."
    ),
    expected_output="A ticket confirmation string or the word RESOLVABLE.",
    agent=triage_agent,
)

crew = Crew(
    agents=[triage_agent],
    tasks=[triage_task],
    process=Process.sequential,
)

result = crew.kickoff(inputs={"query": "I was double charged $800 on my invoice"})
print("TRIAGE RESULT:", result)

Expected output (abbreviated):

TRIAGE RESULT: Escalated to human. Ticket HUM-12345 created from transcript: I was double charged $800...

If you change the input to "How do I reset my password?", the printed result is RESOLVABLE.

Step 4: Conditional resolution path

A real crewai support agent human escalation system should not run the resolution agent after escalation. Branch in code based on the triage string.

def run_support(query: str) -> str:
    triage_out = crew.kickoff(inputs={"query": query})
    if "RESOLVABLE" in str(triage_out):
        resolve_task = Task(
            description=f"Write a 2-sentence reply for: {query}",
            expected_output="A customer-facing response.",
            agent=resolution_agent,
        )
        resolve_crew = Crew(
            agents=[resolution_agent],
            tasks=[resolve_task],
            process=Process.sequential,
        )
        return resolve_crew.kickoff()
    return triage_out

print(run_support("How do I reset my password?"))

Expected resolution output:

You can reset your password by clicking 'Forgot Password' on the login screen. A reset link will arrive in your email within a minute.

Step 5: Add a human-in-the-loop confirmation

Before sending the auto-reply, a support lead should approve it. CrewAI supports this natively with human_input=True on a task.

confirm_task = Task(
    description="Review the drafted reply for: {query}. Approve or rewrite.",
    expected_output="Final reply text.",
    agent=resolution_agent,
    human_input=True,
)

confirm_crew = Crew(
    agents=[resolution_agent],
    tasks=[confirm_task],
    process=Process.sequential,
)
final = confirm_crew.kickoff(inputs={"query": "How do I reset my password?"})
print("FINAL:", final)

When run, the process pauses and prompts in the terminal:

[Human input requested] Review the drafted reply for: How do I reset my password?
> approve
FINAL: You can reset your password by clicking 'Forgot Password' on the login screen...

Step 6: Full script

import os
from crewai import LLM, Agent, Task, Crew, Process
from crewai_tools import tool

@tool("HumanEscalation")
def open_human_ticket(query: str, transcript: str) -> str:
    """Use this when the customer issue cannot be resolved automatically."""
    ticket_id = "HUM-" + str(abs(hash(query)) % 100000)
    return f"Escalated to human. Ticket {ticket_id} created from transcript: {transcript[:60]}..."

llm = LLM(
    model="openai/gpt-4o-mini",
    base_url="https://api.n4n.ai/v1",
    api_key=os.environ["N4N_API_KEY"],
    temperature=0.2,
)

triage_agent = Agent(
    role="Support Triage",
    goal="Decide if the request is auto-resolvable or must reach a human.",
    backstory="You enforce routing policy for a SaaS support desk.",
    llm=llm,
    tools=[open_human_ticket],
    verbose=False,
)

resolution_agent = Agent(
    role="Tier-1 Support",
    goal="Resolve common billing and login issues using concise answers.",
    backstory="You are a senior support rep who hates fluff.",
    llm=llm,
    verbose=False,
)

def run_support(query: str) -> str:
    triage_task = Task(
        description=(
            "Review this support request: '{query}'. "
            "If it mentions a refund over $500 or a service outage, call HumanEscalation. "
            "Otherwise reply with exactly the word RESOLVABLE."
        ),
        expected_output="A ticket confirmation or RESOLVABLE.",
        agent=triage_agent,
    )
    triage_crew = Crew(agents=[triage_agent], tasks=[triage_task], process=Process.sequential)
    out = triage_crew.kickoff(inputs={"query": query})
    if "RESOLVABLE" not in str(out):
        return str(out)
    resolve_task = Task(
        description=f"Write a 2-sentence reply for: {query}",
        expected_output="A customer-facing response.",
        agent=resolution_agent,
    )
    return str(Crew(agents=[resolution_agent], tasks=[resolve_task], process=Process.sequential).kickoff())

if __name__ == "__main__":
    print(run_support("I was double charged $800 on my invoice"))
    print(run_support("How do I reset my password?"))

Production notes

The design above separates policy (triage) from execution (resolution) and makes the crewai support agent human escalation boundary explicit. Three things to harden before shipping:

  • Tool side effects: open_human_ticket must be idempotent. Include a customer ID and dedupe on your side so a retried agent call does not spawn ten tickets.
  • LLM reliability: A single provider will rate-limit you. Routing through an OpenAI-compatible gateway that honors client routing directives and forwards cache-control hints keeps p95 latency flat when one backend degrades.
  • Audit trail: Stream verbose logs to your observability stack. The transcript string passed to the escalation tool is your handoff context; capture it.

If you need per-token metering across multiple models, a gateway that reports usage on each response saves you from writing billing glue code. That is the only part where the inference layer should leak into your agent code.

Build the triage prompt around concrete thresholds ($500, “outage”) rather than vague “complex” language; the agent will follow rules better than vibes.

Tagscrewaicustomer-supportescalationagents

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 framework tutorials: customer support bots posts →