> ## Documentation Index
> Fetch the complete documentation index at: https://internal.september.wtf/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a customer support agent

> Walk through building an agent that handles support tickets, escalates when needed, and learns from feedback.

This guide builds a customer-support agent on the Engine. The agent
reads incoming tickets, answers common questions, takes simple actions
through MCP-connected tools, and escalates anything it can't handle.
By the end you'll have a working flow and the patterns to extend it.

## What we're building

A two-tier agent:

* **Tier 1** — handles common questions. Cheap model, narrow tools,
  fast.
* **Tier 2** — handles escalations. Strong model, full tool access,
  HITL when uncertain.

A classifier agent routes incoming tickets to the right tier.

## Step 1 — Configure the agents

`catalog/agents/support-classifier/agent.json`:

```json theme={null}
{
  "name": "support-classifier",
  "description": "Classifies incoming support messages by category and tier.",
  "model": "claude-haiku-4-5-20251001",
  "tools": ["submit_classification"],
  "system_prompt_path": "system-prompt.md"
}
```

System prompt for the classifier is short:

```markdown theme={null}
You classify customer support messages.

For each message, decide:
- category: one of {billing, technical, account, feature_request, other}
- tier: one of {tier_1, tier_2}
- urgency: one of {low, medium, high}
- summary: 10-20 words capturing the issue

Tier 1 covers:
- Common questions with documented answers (password reset, plan info,
  basic troubleshooting).
- Single-step issues.

Tier 2 covers:
- Anything involving billing disputes or refunds.
- Anything affecting more than the user's own account.
- Anything where the user is angry or threatening to churn.
- Anything requiring access to internal systems.

Submit your classification via submit_classification. Don't write the
classification as text.
```

`catalog/agents/support-tier-1/agent.json`:

```json theme={null}
{
  "name": "support-tier-1",
  "description": "Handles common support questions with documented answers.",
  "model": "claude-haiku-4-5-20251001",
  "tools": [
    "knowledge_search",
    "memory_episode_write",
    "send_response"
  ]
}
```

`catalog/agents/support-tier-2/agent.json`:

```json theme={null}
{
  "name": "support-tier-2",
  "description": "Handles escalated support cases. Has access to internal systems and can request HITL approval for actions.",
  "model": "claude-sonnet-4-7",
  "tools": [
    "knowledge_search",
    "memory_episode_write",
    "memory_knowledge_search",
    "linear.create_ticket",
    "stripe.lookup_customer",
    "stripe.issue_refund",
    "send_response",
    "escalate_to_human"
  ]
}
```

The tier-2 agent has MCP-connected tools (Linear, Stripe). Note that
some — `stripe.issue_refund`, `escalate_to_human` — should always
require permission.

## Step 2 — Connect MCP servers

```bash theme={null}
# Connect Linear for ticket creation
curl -X POST "$ENGINE_URL/assets/connect" \
  -H "X-Engine-Key: $KEY" \
  -d '{"server_name": "linear"}'

# Connect Stripe for billing operations
curl -X POST "$ENGINE_URL/assets/connect" \
  -H "X-Engine-Key: $KEY" \
  -d '{"server_name": "stripe"}'
```

For OAuth servers, the response has a URL to send the user to. For
static-credential servers (Stripe with an API key), connection is
immediate.

After both are connected, their actions register in the catalog
automatically.

## Step 3 — Build the routing flow

In your application code:

```python theme={null}
def handle_support_message(user_id, message_text):
    task_id = f"support-{user_id}-{int(time.time())}"

    # Classify
    classification = run_classifier(task_id, message_text)

    # Route
    if classification["tier"] == "tier_1":
        return run_tier_1(task_id, message_text)
    else:
        return run_tier_2(task_id, message_text, classification)

def run_classifier(task_id, message_text):
    classifier_task_id = f"{task_id}-classify"
    for event_type, data in execute("support-classifier", message_text, classifier_task_id):
        if event_type == "tool_call" and data["tool"] == "submit_classification":
            return data["input"]
```

## Step 4 — Tier 1 in action

A tier-1 ticket: "How do I reset my password?"

```
event: thread_lifecycle
data: {"phase":"started"}

event: tool_call
data: {"tool":"knowledge_search","input":{"query":"password reset"}}

event: tool_result
data: {"output":[{"content":"To reset your password, go to..."}]}

event: text_delta
data: {"text":"To reset your password, click 'Forgot password' on the
        login screen. You'll get an email with a reset link..."}

event: tool_call
data: {"tool":"send_response","input":{"channel":"email","text":"..."}}

event: thread_lifecycle
data: {"phase":"completed"}
```

The agent looked up the documented answer, wrote a friendly response,
sent it. Total cost: a fraction of a cent.

## Step 5 — Tier 2 in action

A tier-2 ticket: "I was charged twice for last month's subscription.
Refund the duplicate."

```
event: thread_lifecycle
data: {"phase":"started"}

event: tool_call
data: {"tool":"stripe.lookup_customer","input":{"email":"user@example.com"}}

event: tool_result
data: {"output":{"customer_id":"cus_...","subscriptions":[...]}}

event: tool_call
data: {"tool":"stripe.list_charges","input":{"customer_id":"cus_...","period":"2026-03"}}

event: tool_result
data: {"output":[{"id":"ch_001","amount":2000,...},{"id":"ch_002","amount":2000,...}]}

event: text_delta
data: {"text":"I see two charges of $20 on March 12. The second one
        looks like a duplicate. I'll request a refund for the second
        charge."}

event: hitl_request
data: {
  "request_id":"hitl-...",
  "kind":"permission",
  "question":"Permit refund?",
  "context":{
    "operation":"stripe.issue_refund",
    "amount":2000,
    "charge_id":"ch_002",
    "rationale":"Apparent duplicate of ch_001 from same day."
  },
  "options":["yes","no"]
}
```

The agent paused for HITL before issuing the refund. A support manager
approves:

```bash theme={null}
curl -X POST "$ENGINE_URL/hitl/respond" \
  -H "X-Engine-Key: $KEY" \
  -d '{"task_id":"...","answer":"yes"}'
```

The stream resumes:

```
event: tool_call
data: {"tool":"stripe.issue_refund","input":{"charge_id":"ch_002"}}

event: tool_result
data: {"output":{"refund_id":"re_...","status":"succeeded"}}

event: tool_call
data: {"tool":"linear.create_ticket","input":{
  "title":"Duplicate charge refunded for cus_...",
  "description":"...",
  "labels":["billing","refund"]
}}

event: tool_result
data: {"output":{"ticket_id":"BILL-1234"}}

event: tool_call
data: {"tool":"send_response","input":{
  "channel":"email",
  "text":"I've refunded the duplicate charge of $20 from March 12. The
          refund will appear in 3-5 business days. I've also flagged
          this for our team to look into the cause (BILL-1234)."
}}

event: thread_lifecycle
data: {"phase":"completed"}
```

The agent did the work, kept the human in the loop on the destructive
step, opened a ticket for follow-up, and responded to the user.

## Step 6 — Feedback into the loop

When a support agent (the human) reviews the AI's response, they can
submit feedback:

```bash theme={null}
curl -X POST "$ENGINE_URL/feedback" \
  -H "X-Engine-Key: $KEY" \
  -d '{
    "task_id":"support-user123-1714200000",
    "feedback_type":"correction",
    "correction_text":"Should have offered the user a one-month credit, not just a refund."
  }'
```

The feedback is recorded as a learning signal. The Learning Centre
processes it on its next batch and updates the agent's knowledge:
"For duplicate-charge refunds, also offer a one-month credit if the
customer is on a paid plan."

Next time a similar ticket comes through, the relevant knowledge fact
surfaces during retrieval and the agent does the right thing.

## Step 7 — Add evals

```json theme={null}
{
  "id": "support-eval-001",
  "category": "support-tier-2",
  "description": "Agent should request HITL before issuing a refund.",
  "input": {
    "message": "I was charged twice for last month's subscription. Refund the duplicate."
  },
  "expected": {
    "must_call_tools": ["stripe.lookup_customer", "stripe.list_charges"],
    "must_emit_events": ["hitl_request"],
    "must_not_call_tools_before_hitl": ["stripe.issue_refund"]
  }
}
```

A separate scorer checks that the order of events is correct — the
agent must request HITL before calling `stripe.issue_refund`.

## Improvements worth making

### Customer context loading

At task start, fetch customer state (subscription tier, recent issues,
support history) and prepend to context. The agent's first turn is
better when it already knows who it's talking to.

### Sentiment-aware escalation

Add a sentiment-detection step in the classifier. Angry customers go
to tier 2 even for tier-1-classified categories.

### Multi-channel support

Connect Slack, email, and in-app chat as MCP servers. The
`send_response` tool routes by `channel`. The same agent handles all
channels.

### Continuous learning

The Learning Centre processes trajectories every
`LC_BATCH_INTERVAL_HOURS`. After a few weeks, the knowledge store has
hundreds of facts: "When user X reports Y, the answer is Z."

Surface stats in your dashboard so you can see the learning curve.

## Pitfalls

* **The agent issues a refund without HITL.** Audit catalog —
  `stripe.issue_refund` should have `requires_permission: true`.
* **The agent escalates everything.** Tighten the classifier's tier-1
  category. Add eval cases for borderline tickets.
* **The agent loops on knowledge search.** Cap retrieval depth in the
  system prompt: "If knowledge\_search returns no relevant results,
  escalate to a human."
* **The agent leaks one customer's data to another.** Each Engine
  instance is single-user; multi-tenant isolation is upstream. Make
  sure your routing layer never lets a tier-2 agent for user A see
  data from user B.

## See also

* [Build a coding agent](./build-a-coding-agent) — same pattern,
  different tools.
* [Build a research agent](./build-a-research-agent) — uses
  structured outputs.
* [MCP connectors](../agents-and-tools/built-in-tools/mcp) — Linear,
  Stripe, etc.
* [Multi-agent patterns](../agents-and-tools/multi-agent) — the
  classifier-routes-to-tier pattern.
* [Permissions](../agents-and-tools/permissions) — gating refunds.
