Claude Architect WorkshopPDFEPUBlearnwithparam.comkaas.team

Foundations, lesson 4

Escalation and handoff

Rules decide who takes over, and the handoff carries the case

Your support agent escalates when a customer sounds angry, and keeps going when a customer asks for a person. The people who do get a ticket receive one line and start from zero. This is the agent at Lumen Supply, an online electronics shop. You will watch it fail, then fix it with three small rules.

I doWatch the failure and its fix.

Rules run first, then Claude finishes the rest

Three parties split the work. Code routes the ticket first. Claude resolves what the agent can solve. A person takes the rest.

Two triggers send a ticket to a person. The customer asks for one, or the shop has no policy for the request.

Rules decide who takes a ticket. A person receives a full handoff.Customerwrites a ticketRulesroute and matchClaudefinishes the restPersontakes the handoffthe rules run in code, before ClaudeRules decide who takes a ticket. A person receives a full handoff.Customerwrites a ticketRulesroute and matchClaudefinishes the restPersontakes the handoffthe rules run in code, before Claude

A solvable ticket stays with the agent

The customer wants a refund the shop allows. No rule sends it to a person, so Claude calls get_customer. The tool call carries the email it read from the ticket. The result carries the customer and the orders.

Claude reads that result and writes the reply. The code never wrote the reply and Claude never chose the route.

  1. CustomerHi, I'm dana@example.com. The keyboard from order 1042 arrived with keys missing. Can I…
    Show the result
    Hi, I'm dana@example.com. The keyboard from order 1042 arrived with keys missing. Can I return it for a refund?
  2. Claudelooks up: dana@example.com
    Show the result
    {
      "input": {
        "query": "dana@example.com"
      },
      "output": {
        "customer": {
          "name": "Dana Cho",
          "email": "dana@example.com",
          "orders": [
            {
              "id": "1042",
              "item": "USB keyboard",
              "amount": 89.0
            }
          ]
        }
      }
    }
  3. Rules{'customer': {'name': 'Dana Cho', 'email': 'dana@example.com', 'orders': [{'id': '1042', 'item'…
    Show the result
    {
      "customer": {
        "name": "Dana Cho",
        "email": "dana@example.com",
        "orders": [
          {
            "id": "1042",
            "item": "USB keyboard",
            "amount": 89.0
          }
        ]
      }
    }
  4. ClaudeI found order 1042 for a USB keyboard ($89.00) on the account for Dana Cho, but the lookup doesn't include our return…
    Show the result
    I found order 1042 for a USB keyboard ($89.00) on the account for Dana Cho, but the lookup doesn't include our return policy or the status of a refund. I can't issue a refund from here, so please reply with a photo of the missing keys and the packing slip, and our team will review the return and tell you the next steps.

Claude finished the ticket.

Three rules as written

route in the starting code escalates on tone: two exclamation marks or the word "ridiculous". Tone is not difficulty. An angry customer with a simple refund is easy, and a calm customer who wants a policy the shop lacks is hard.

Sentiment does not measure difficulty, and neither does the model rating its own confidence. A rule needs a signal that names the ticket, such as a request for a person.

A prompt line that says not to invent policies only lowers the rate. When the shop has no policy, route to a person in code before the agent answers.

starter/handoff.py
1"""EDIT THIS FILE. Three rules decide what happens to a ticket.2Run `./workshop check escalation-handoff` until it is green, then3`./workshop watch escalation-handoff`.4"""56from kit.api import Json789def route(text: str) -> str:10 """Send a ticket to `human` or leave it with the `agent`."""11 # TODO(workshop): escalate on an explicit request for a person and on12 # a policy the shop does not have. Never on tone.13 upset = "!!" in text or "ridiculous" in text.lower()1415 return "human" if upset else "agent"161718def match(query: str, customers: list[Json]) -> Json:19 """Find the customer a query names, or ask for one more identifier."""20 # TODO(workshop): with two or more matches, return {"ask": ...}.21 found = [22 c23 for c in customers24 if query.lower() in f"{c['name']} {c['email']}".lower()25 ]2627 return {"customer": found[0]} if found else {"error": "No customer."}282930def handoff(case: Json) -> Json:31 """What a person receives when the ticket is escalated."""32 # TODO(workshop): send customer, order, issue, tried and ask.33 return {"summary": case["issue"]}

What happens when the customer asks for a person?

Take the ticket that says: "I have asked three times about order 1043. I want to speak to a person." The starting rules only look at tone.

Predict: Does the ticket reach a person?

  1. AYes. The agent understands the request and hands it over.
  2. BNo. The starting rule looks for tone, so the agent carries on.
Show the answer

B. The agent looked up the customer and wrote a reply. The customer asked for a person and got a bot again.

The customer asks and is ignored

This is the same ticket with the starting rules. Claude looks the customer up and answers. The result card is red: the ticket needed a person.

This is a real Claude Haiku 5.5 run, recorded once and replayed here. The rules changed nothing, so the ticket stayed with the agent.

  1. CustomerI'm omar@example.com. I have asked three times about order 1043. I want to speak to a…
    Show the result
    I'm omar@example.com. I have asked three times about order 1043. I want to speak to a person.
  2. Claudelooks up: omar@example.com
    Show the result
    {
      "input": {
        "query": "omar@example.com"
      },
      "output": {
        "customer": {
          "name": "Omar Reyes",
          "email": "omar@example.com",
          "orders": [
            {
              "id": "1043",
              "item": "4K monitor",
              "amount": 640.0
            }
          ]
        }
      }
    }
  3. Rules{'customer': {'name': 'Omar Reyes', 'email': 'omar@example.com', 'orders': [{'id': '1043', 'item'…
    Show the result
    {
      "customer": {
        "name": "Omar Reyes",
        "email": "omar@example.com",
        "orders": [
          {
            "id": "1043",
            "item": "4K monitor",
            "amount": 640.0
          }
        ]
      }
    }
  4. ClaudeI found your account, Omar Reyes, with order 1043 for a 4K monitor totaling $640.00. I can't transfer you to a person…
    Show the result
    I found your account, Omar Reyes, with order 1043 for a 4K monitor totaling $640.00. I can't transfer you to a person from here, but I can see your order details, so tell me what's wrong with order 1043 and I'll do my best to help.

The ticket needed a person and the agent carried on.

Three rules replace tone with criteria

Escalate on an explicit request for a person, or on a policy the shop does not have. Ask when two customers match. The text matches in route stand in for a real classifier.

When the model must judge the gray cases, write two to four worked examples, each a sample ticket with the right call and its reason. A reason teaches the boundary, which a bare label cannot. "Be conservative" is not a criterion, because it names no case.

Split a message that holds several asks into items, and decide each one. The routing is plain code, so the same ticket goes the same way every time.

Messages between the customer, Claude, the rules and a person, in order.CustomerClaudeRulesPersontickethandoff
handoff.pygreen lines are what the fix adds
1"""Three rules: who decides, who the customer is, and what a person gets."""23from kit.api import Json45ASK_FOR_PERSON = ("speak to a person", "talk to a human", "real person")6POLICY_GAPS = ("price match", "match the price", "match their price")789def route(text: str) -> str:10 """Escalate on a request for a person or a missing policy."""11 lowered = text.lower()12 if any(p in lowered for p in (*ASK_FOR_PERSON, *POLICY_GAPS)):13 return "human"1415 return "agent"161718def match(query: str, customers: list[Json]) -> Json:19 """Find one customer, or ask for one more identifier."""20 found = [21 c22 for c in customers23 if query.lower() in f"{c['name']} {c['email']}".lower()24 ]25 if len(found) > 1:26 return {27 "ask": "More than one customer has that name. Ask for the email."28 }2930 return {"customer": found[0]} if found else {"error": "No customer."}313233def handoff(case: Json) -> Json:34 """Everything a person needs, so nobody asks the customer again."""35 return {36 "customer": case["customer"],37 "order": case["order"],38 "issue": case["issue"],39 "tried": case["tried"],40 "ask": case["ask"],41 }

The handoff carries the case

The shop has no price match policy, so a person must decide. With the starting rules the handoff is one line. Press the second button: the handoff holds five fields, which are customer, order, issue, what was tried, and the ask.

A handoff is a data structure, not a sentence. The person who opens it should not have to ask the customer again.

The same care applies to alert flags that tell staff a ticket needs a look. If one flag category fires on 40 percent of tickets, staff stop reading every flag. Switch that category off until its criteria are fixed, and keep the others on.

This part is interactive in the lab: run ./workshop watch escalation-handoff.

We doChange one setting at a time and predict.

The same ticket now goes to a person

Use the switch to run the ticket with the starting rules, then the escalation rules. On the second button, the card marked "This is where the runs split" is where the ticket now goes to a person.

Try the angry ticket too. The starting rules escalate it with a one line summary. The fixed rules leave it with the agent, which can solve it.

Examples earn their place on the ambiguous tickets, so do not fill the prompt with easy ones. Vary the ticket layouts in them, so the model learns the rule and not one channel's format.

This part is interactive in the lab: run ./workshop watch escalation-handoff.

Try it: escalation rules

Change one setting at a time. Pick the price match ticket: the shop has no such policy, so a person must decide. Pick the Two Sam Lees ticket: the shop has two customers with that name, the lookup finds both, and the fixed rules ask for an email.

Every combination is a recorded run, so this works with no internet.

This part is interactive in the lab: run ./workshop watch escalation-handoff.

The same lookup runs in the Agent SDK

The Claude Agent SDK serves tools from an in-process MCP server. The route rule still runs in code first. A ticket for a person gets the tool hand_to_person. Its result is the handoff, and Claude reads that result to write a short reply.

This is a real Claude Haiku 5.5 run through the SDK. With the starting rules, the same ticket goes to the lookup and the agent answers a question only a person can decide.

app/sdk_agent.pyfile: 133 lines
···1 line not shown23`route` still runs in code first. A ticket it sends to a person gets one4tool, `hand_to_person`, whose result is the handoff Claude reads before it5replies. Any other ticket gets `get_customer`, backed by the learner's `match`.6"""78import json9from types import ModuleType1011from claude_agent_sdk import (12 McpSdkServerConfig,13 SdkMcpTool,14 create_sdk_mcp_server,15 tool,16)1718from kit import sdk19from kit.api import Json20from .agent import FIELDS, LOOKUP, escalate, lookup21from .prompt import ESCALATED22from .tickets import Ticket2324SERVER = "support"25PREFIX = f"mcp__{SERVER}__"26HANDOFF = "hand_to_person"···6 lines not shown33 """Serve the one tool this ticket needs, in-process."""3435 @tool(LOOKUP["name"], LOOKUP["description"], LOOKUP["input_schema"])36 async def find(args: Json) -> Json:37 return reply(lookup(rules, args))3839 @tool(HANDOFF, "Hand this ticket to a person.", HANDOFF_SCHEMA)40 async def hand(args: Json) -> Json:41 return reply(escalate(ticket, rules, ["route rule"]))4243 found: SdkMcpTool[Json] = hand if human else find4445 return create_sdk_mcp_server(SERVER, tools=[found])464748def reply(output: Json) -> Json:49 """One tool result for the SDK."""50 return {"content": [{"type": "text", "text": json.dumps(output)}]}515253def request_for(54 ticket: Ticket, rules: ModuleType, system: str, tag: str55) -> Json:56 """The plain request a recording is keyed by."""57 human = rules.route(ticket.text) == "human"···76 lines not shown
  1. CustomerI'm dana@example.com. I paid $89 for order 1042 and a competitor sells it for $69. Can…
    Show the result
    I'm dana@example.com. I paid $89 for order 1042 and a competitor sells it for $69. Can you price match?
  2. Rulessent to a person
    Show the result
    {
      "kind": "route",
      "to": "human"
    }
  3. Claudehands the ticket to a person
    Show the result
    {}
  4. Person receivesfields: customer, order, issue, tried, ask
    Show the result
    {
      "customer": "Dana Cho",
      "order": "1042",
      "issue": "I'm dana@example.com. I paid $89 for order 1042 and a competitor sells it for $69. Can you price match?",
      "tried": [
        "route rule"
      ],
      "ask": "Decide on a price match. The shop has no such policy."
    }
  5. ClaudeThanks, Dana. A member of our team will follow up with you about your price match request for order 1042. I've passed…
    Show the result
    Thanks, Dana. A member of our team will follow up with you about your price match request for order 1042. I've passed along your order number, the $89 price you paid, the $69 competitor price, and your question about whether we can match it.

A person took over, with customer, order, issue, tried, ask.

You doFix the starting code, then test yourself.

You do: fix the three rules

Copy the starting code and open work/escalation-handoff/handoff.py. It has three planted defects. route() escalates on tone, match() answers about the first of two customers with one name, and handoff() sends a one-line summary.

Terminal
./workshop start escalation-handoff
./workshop check escalation-handoff
./workshop watch escalation-handoff

The checks send a request for a person and a price match to a human, and keep a solvable angry ticket with the agent. They expect a question when two customers match, and five fields in every handoff.

Check your understanding

Answer each question, then read why every option is right or wrong. The wrong options are the mistakes engineers make on the job.

1. A team escalates every ticket with an angry tone. Of 100 escalations last week, 34 were simple refunds the agent could have finished. What is the right change?

  1. AEscalate on named criteria, such as a request for a person or a policy the shop lacks.
  2. BHire more people for the escalation queue, so the extra 34 are handled quickly.
  3. CAdd more angry words to the rule, so the tone check is more exact.
  4. DKeep the tone rule and add a sentiment score, escalating only when the score is very low.
Show the answer

Answer: A

  • A Correct. A criterion says something about the ticket, such as what is asked or whether a policy exists.
  • B More staff clears the queue and leaves the rule unchanged. The 34 simple refunds still reach a person.
  • C A longer word list misses calm hard tickets and still catches angry easy ones.
  • D A sentiment score is still tone. A calm ticket can be hard, and an angry one can be a simple refund.

2. Select two valid triggers for sending a ticket to a person.

  1. AThe customer asks for a person.
  2. BThe sentiment score of the message is below a threshold.
  3. CThe shop has no policy for what the customer asks.
  4. DThe model rates its own confidence under 0.8.
Show the answer

Answer: A, C

  • A Correct. An explicit request is honoured at once.
  • B Sentiment measures tone, not difficulty. A calm ticket can need a person and an angry one may not.
  • C Correct. When the policy is silent the agent would have to invent one, so a person decides.
  • D A model's rating of itself does not track whether it is right, so it cannot route tickets.

3. A lookup for Sam Lee returns two customers, and the agent answers about the first one. In 50 such tickets, 19 were about the wrong person. What should the lookup do?

  1. AReturn the first match, because the first record is right more often than not.
  2. BReturn the account with the newest order, since it is more likely to be active.
  3. CReturn a question asking for one more identifier, such as the email.
  4. DSend every name collision to a person, so nobody has to choose.
Show the answer

Answer: C

  • A The first record is an arbitrary pick. In the tickets above it was wrong 19 times in 50.
  • B Recency is a guess. The customer who wrote in may be the one with the older account.
  • C Correct. One more identifier settles it, and the agent never has to guess.
  • D A collision can be settled by one question. Sending it to a person spends a person on a simple case.

4. A routing rule sends a ticket to a person when the model rates its own confidence under 0.7. The model rated 0.9 on an answer that was wrong. What does this show?

  1. AThe threshold is too low, so raising it to 0.95 will catch this kind of miss.
  2. BThe model should rate its confidence a second time, and the second rating is the one to use.
  3. CAveraging three ratings will give a number that follows whether the answer is right.
  4. DSelf-rated confidence does not measure difficulty, so route on explicit criteria.
Show the answer

Answer: D

  • A A higher threshold still relies on a number that can be 0.9 on a wrong answer.
  • B A second rating comes from the same source and has the same flaw.
  • C An average of numbers that do not track correctness does not track it either.
  • D Correct. Name the triggers, and check accuracy against labelled tickets instead of trusting a score.

5. A customer writes "I want a person" after three replies from the agent. The agent writes a fourth answer. What should happen instead?

  1. AThe agent tries one more answer first, since it might solve the ticket.
  2. BThe ticket goes to a person at once.
  3. CThe agent asks the customer to confirm that they really want a person.
  4. DThe agent waits to see whether the next message sounds angry.
Show the answer

Answer: B

  • A The customer has already asked. A fourth attempt repeats what they are tired of.
  • B Correct. An explicit request is a rule with no judgement in it, so it is honoured at once.
  • C A confirmation turns a clear request into one more step the customer did not ask for.
  • D Tone is not the trigger. The customer has already said what they want.

6. A customer asks for a price match, which the shop has never offered. The agent quotes a price match policy it made up, in 12 of 40 such tickets. What is the right fix?

  1. AAdd a line to the prompt that says not to invent policies.
  2. BLet the agent offer the nearest policy it has, such as a return window.
  3. CLower the temperature so the agent stays closer to the facts it has.
  4. DEscalate to a person in code whenever the policy is silent.
Show the answer

Answer: D

  • A A prompt line lowers the rate but is still a request, and 12 in 40 shows how often it slips.
  • B A different policy is still not the one asked for, and the customer is misled.
  • C Temperature changes variety, not whether the agent knows a policy exists.
  • D Correct. A named criterion sends the ticket to a person, and no policy gets invented.

7. The prompt says "be conservative about escalating". On 60 borderline tickets, 18 go the wrong way. What should change?

  1. AMake it stronger, for example "be very conservative about escalating".
  2. BHave the model rate its confidence and escalate below a threshold.
  3. CName the categories, what to escalate in each and what to handle.
  4. DEscalate every borderline ticket, so none are handled wrongly.
Show the answer

Answer: C

  • A A stronger word is still not a criterion. The model still has to guess what conservative means.
  • B A confidence rating does not track whether the ticket is hard.
  • C Correct. Named categories with a clear action each leave the model nothing to guess.
  • D This fixes the 18 by sending all 60 to people, which is the cost the agent was built to avoid.

8. One flag category is raised on 40 percent of tickets, and staff have stopped reading any flags. What is the best response?

  1. ASwitch that category off until its criteria are fixed.
  2. BAsk staff to read every flag, since the noisy category may hide real ones.
  3. CReword the flag so it sounds less urgent, so staff take it less seriously.
  4. DRemove all flagging, so nobody has to sift through them.
Show the answer

Answer: A

  • A Correct. Trust is restored by removing the noise, and the category comes back once its criteria are sound.
  • B Staff already stopped. A flag nobody trusts costs time and protects no one.
  • C A quieter flag on 40 percent of tickets is still noise.
  • D This drops the useful flags along with the noisy one.

9. An escalation note reads "customer unhappy". The person who takes the ticket has to ask the customer everything again, and handling time is 11 minutes longer. What is the fix?

  1. AWrite a longer free-text summary of the conversation.
  2. BAttach the full transcript and let the person find what matters in it.
  3. CHand over a structured case: customer, order, issue, tried, ask.
  4. DAdd a mood score, so the person knows how careful to be.
Show the answer

Answer: C

  • A A longer summary can still leave out the order or the ask, and a person has to read it all.
  • B A transcript holds the answer but buries it, so the person still starts from zero.
  • C Correct. Each field answers a question the person would otherwise put to the customer.
  • D A mood score says how the customer feels and not what to do.

10. Select two fields a handoff needs when a person takes over a refund case.

  1. AThe model's confidence score for its last answer.
  2. BWhat the agent already tried.
  3. CA label for the customer's mood.
  4. DWhat the person is asked to do.
Show the answer

Answer: B, D

  • A The score does not say what happened or what to do, and it is not reliable.
  • B Correct. It stops the person from repeating a step the agent already took.
  • C A mood label does not help the person act on the case.
  • D Correct. The ask is the reason the ticket reached a person.

11. One message asks for a refund and an address change. The agent answers the refund and ignores the address in 14 of 30 such messages. What should change?

  1. ATell customers to send one request per message.
  2. BAnswer the longer request first, since it is likely the main one.
  3. CTreat the second request as out of scope, so the agent only takes the first.
  4. DSplit the message into separate items and handle each one.
Show the answer

Answer: D

  • A This moves the work to the customer, and many will not read it.
  • B Length does not show which request matters, and the other is still dropped.
  • C The address change is in scope. Dropping it means a second ticket and a second wait.
  • D Correct. A list of items makes each request visible, so none is dropped.

12. An escalation must carry the case history. The model writes a free summary, and 9 of 30 leave out the order number. How should the handoff be fixed?

  1. AAdd a sentence to the prompt that says to include the order number.
  2. BFix the fields in a schema, so a handoff with no order fails.
  3. CLet the person who takes the ticket ask the model for what is missing.
  4. DAsk for a summary of exactly 60 words, so the length is always the same.
Show the answer

Answer: B

  • A A prompt line lowers the misses but does not stop them, and nothing flags a missing field.
  • B Correct. A schema makes a missing field an error the harness can see and reject.
  • C The person now has to spot what is missing, which is the work the handoff should save.
  • D A word count says nothing about which facts are in the summary.

13. On borderline tickets the same input is escalated on one run and handled on the next, in 15 of 60 repeats. What is the most reliable change?

  1. AAdd two to four borderline examples, each with action and reason.
  2. BWrite a longer list of rules that covers every case in words.
  3. CAdd a stricter instruction, since examples only change the style of the reply.
  4. DRaise the temperature so the model explores more before it settles.
Show the answer

Answer: A

  • A Correct. Examples with reasons settle the cases the words leave open, the same way every time.
  • B A long list still leaves the boundary cases to judgement, and the prompt grows.
  • C Examples show the model where a line falls, which is more than style.
  • D More variety makes the runs differ more, not less.

14. Select two things a few-shot example for a borderline ticket should show.

  1. AThe ticket as it arrives.
  2. BThe action chosen and the reason for it.
  3. CThe probability the model gave to each action.
  4. DWho wrote the example and when.
Show the answer

Answer: A, B

  • A Correct. The model matches new tickets against the inputs it has seen.
  • B Correct. The reason tells the model what to weigh, so it can apply it to a new ticket.
  • C Probabilities are not part of the pattern the model learns from.
  • D Authorship does not help the model decide anything.

15. The prompt holds four examples, and all four are easy tickets. The agent fails on 7 of 20 hard tickets. What is the fix?

  1. AAdd more easy examples, since more examples help.
  2. BReplace them with ambiguous cases, each with a reason.
  3. CMove to a larger model that copes better with the hard tickets.
  4. DRemove the examples and rely on the written rules alone.
Show the answer

Answer: B

  • A More easy cases teach the model the part it already gets right.
  • B Correct. Examples earn their place on the tickets where the wording alone is not enough.
  • C A larger model can still have no example of where the line falls.
  • D This throws away the one part of the prompt that shows the boundary.

16. All the prompt's examples use one ticket layout. A new layout from another channel breaks the routing on 11 of 40 tickets. What is the fix?

  1. AAdd more examples in the same layout, so the pattern is stronger.
  2. BReplace the examples with a pattern match on the new channel's header.
  3. CAdd a special rule that handles only the new layout.
  4. DVary the layouts, so the model learns the rule and not the format.
Show the answer

Answer: D

  • A More of one layout repeats the format and teaches the model nothing new.
  • B A header match is a rule for one channel, and the routing still breaks on the next.
  • C The next channel needs another special rule, and the list grows with every layout.
  • D Correct. Different layouts with the same outcome show what stays fixed and what changes.

Play: spot the bug

This rule sends a ticket to a person when the customer asks for one.

python
def route(text: str) -> str:
    return "human" if "person" in text.lower() else "agent"


route("Please update the personal address on my account.")

Predict: Where does this ticket go?

  1. ATo a person, because "personal" holds the word person.
  2. BTo the agent, because nobody asked for a person.
Show the answer

A. A single word matches inside other words and other meanings. Match whole requests such as "speak to a person", and test the rule on tickets that use the word without asking.

References

Conclusions

  • Escalate on explicit criteria: a request for a person, or a policy the shop does not have.
  • Ask for one more identifier when a lookup matches more than one customer.
  • Give the model two to four examples with reasons for the gray cases.
  • A handoff carries customer, order, issue, what was tried and the ask.

You can now write escalation rules that send the right tickets to a person, with the whole case.

Escalation criteria

Rules that name when a person takes over, such as a request or a policy gap

Handoff

The case a person receives: customer, order, issue, tried and ask

Ambiguous match

A lookup that finds more than one customer, so Claude asks

Policy gap

A request for something the shop has no rule for