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

# Hand a conversation over to a human agent

> Decide when a conversation needs a person, pick the queue from routing rules you control, and send the full history along with it.

Some conversations should end up with a person: a refund the model cannot approve, or a customer who asks for one.
This guide transfers such a conversation to a human queue with its full history, using routing rules your application owns.
It uses only existing platform features. The model decides or requests the transfer, the Conversations API supplies the history, and your application picks the queue and delivers the package.

API Reference:

* [Agents collection and run endpoints](https://docs.mka1.com/api-reference/agents/create-an-agent.md)
* [Conversation item endpoints](https://docs.mka1.com/api-reference/conversations/list-conversation-items.md)

## 1. Decide when to hand over

There are two ways to get the decision from the model. Use either or both.

**Triage after each customer message.** The console ships a "Customer care triage" agent template.
In the console, open Agents, choose New agent, pick Customer care triage, select Use template, then Create agent.
The agent returns one JSON object per run with `category`, `priority`, `sentiment`, `language`, `summary`, `suggested_reply`, `needs_human_handoff` and `handoff_reason`.
The template's instructions say when `needs_human_handoff` is true: the customer asks for a person, reports fraud or a security issue, threatens legal action or cancellation, is very upset, or needs a refund, account change or decision the model cannot make.
Edit the categories in the instructions to match your support teams before you route on them.
Copy the agent id; the sample below reads it from `TRIAGE_AGENT_ID`.

**Let the assistant ask for a person mid-conversation.** Declare a function tool on the agent or Responses request that handles the chat.
When the model calls it, the response contains a `function_call` item with the arguments. Your application builds the same handover package as below and answers with a `function_call_output` so the model can tell the customer what happens next.
See [Function calling](https://docs.mka1.com/docs/function-calling.md) for the loop.

```json Tool definition theme={null}
{
  "type": "function",
  "name": "transfer_to_human",
  "description": "Transfer this conversation to a human support agent. Call it when the customer asks for a person or when you cannot resolve the request.",
  "strict": true,
  "parameters": {
    "type": "object",
    "properties": {
      "reason": { "type": "string" },
      "queue_hint": { "type": "string", "enum": ["billing", "security", "retention", "general"] }
    },
    "required": ["reason", "queue_hint"],
    "additionalProperties": false
  }
}
```

## 2. Pause for a human decision

If the agent uses MCP tools that mutate external systems, set `require_approval: "always"` on the tool.
The response then stops at an `mcp_approval_request` item instead of calling the tool. Your application shows the human the server, tool name and arguments, and continues the response with an `mcp_approval_response` item that approves or denies the call.
This is the same approval flow as [Require end-user approval](https://docs.mka1.com/docs/mcp-tools.md#require-end-user-approval), with a support agent making the decision instead of the end user.

## 3. Collect the full history

Keep each support chat in a [conversation](https://docs.mka1.com/docs/conversations.md) so the history is stored on the platform.
When the handover fires, list the conversation's items and attach them to the package.
Items come back in the order they were added when you pass `order: "asc"`.

```ts TypeScript SDK theme={null}
const history = await sdk.llm.conversations.listItems({ conversationId: "conv_abc123", order: "asc" });
```

The list holds every message, function call and tool output the conversation contains, so the human agent sees what the customer and the assistant already said.
The list is paginated; pass `after` with the last item id when a conversation is longer than one page.

## 4. Route with rules you control

Routing is a lookup your application owns. Keep it as data so support leads can change it without a deploy.
The sample uses a list of rules, first match wins. A rule without `priority` matches every priority, and anything unmatched goes to a default queue.

```json Routing rules theme={null}
[
  { "category": "account_access", "priority": "urgent", "queue": "security-incidents" },
  { "category": "billing", "priority": "urgent", "queue": "billing-escalations" },
  { "category": "billing", "queue": "billing" },
  { "category": "complaint", "queue": "retention" }
]
```

`category` and `priority` come from the triage JSON. With the function tool, map `queue_hint` the same way.

## 5. Deliver to the queue

The handover package is plain JSON: the queue, the reason, the triage result, the conversation id and the history.
Post it to the system that holds your human queues, such as a help desk webhook or a ticketing API.
The sample posts to `HANDOVER_WEBHOOK_URL` when it is set and prints the package otherwise, so you can run it without a third-party account.

```json Handover package theme={null}
{
  "queue": "billing-escalations",
  "reason": "Customer demands a refund today and threatens a chargeback.",
  "triage": { "category": "billing", "priority": "urgent", "needs_human_handoff": true, "...": "..." },
  "conversation_id": "conv_abc123",
  "history": [{ "type": "message", "role": "user", "content": "..." }]
}
```

## Run the sample

The sample seeds a conversation so it runs on its own. In your application the conversation already exists and only steps 2 to 5 run.
It needs `MKA1_API_KEY` and the `TRIAGE_AGENT_ID` from step 1.

```bash theme={null}
bun add @meetkai/mka1
MKA1_API_KEY=mk-... TRIAGE_AGENT_ID=agt_... bun run handover.ts
```

```ts handover.ts theme={null}
import { SDK } from "@meetkai/mka1";

const sdk = new SDK({ bearerAuth: `Bearer ${process.env.MKA1_API_KEY}` });

const triageAgentId = process.env.TRIAGE_AGENT_ID;
if (!triageAgentId) throw new Error("Set TRIAGE_AGENT_ID to the agent created from the Customer care triage template");

// Routing rules: first match wins. Omit priority to match every priority.
const routingRules = [
  { category: "account_access", priority: "urgent", queue: "security-incidents" },
  { category: "billing", priority: "urgent", queue: "billing-escalations" },
  { category: "billing", queue: "billing" },
  { category: "complaint", queue: "retention" },
];

// 1. The conversation. In your app it already exists; this seeds one so the sample runs on its own.
const messages = [
  { role: "user", content: "I was charged twice for order 48213 and nobody answers my emails." },
  { role: "assistant", content: "I'm sorry about that. Can you confirm the last four digits of the card?" },
  { role: "user", content: "4412. I want a refund today or I'm disputing it with my bank." },
] as const;

const conversation = await sdk.llm.conversations.create({
  createConversationRequest: { items: [...messages], metadata: { channel: "web-chat" } },
});

// 2. Decide: run the triage agent over the transcript.
let run = await sdk.agentRuns.createAgentRun({
  agentId: triageAgentId,
  createAgentRunRequest: { input: messages.map((m) => `${m.role}: ${m.content}`).join("\n") },
});
while (run.status === "waiting" || run.status === "queued" || run.status === "submitting") {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  run = await sdk.agentRuns.getAgentRun({ agentId: triageAgentId, runId: run.id });
}
if (run.status !== "completed") throw new Error(`Triage run ended with status ${run.status}`);

const message = run.gatewayResponse.output.find((item: { type: string }) => item.type === "message");
const triage = JSON.parse(message.content[0].text);
if (!triage.needs_human_handoff) {
  console.log("No handover needed:", triage.summary);
  process.exit(0);
}

// 3. Route: category × priority → queue.
const rule = routingRules.find(
  (r) => r.category === triage.category && (r.priority === undefined || r.priority === triage.priority),
);
const queue = rule?.queue ?? "general-support";

// 4. Full history from the Conversations API.
const history = await sdk.llm.conversations.listItems({ conversationId: conversation.id, order: "asc" });

// 5. Hand over: post the package to your queue, or print it.
const handover = {
  queue,
  reason: triage.handoff_reason,
  triage,
  conversation_id: conversation.id,
  history: history.data,
};
const webhook = process.env.HANDOVER_WEBHOOK_URL;
if (webhook) {
  const posted = await fetch(webhook, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(handover),
  });
  console.log(`Handed over to ${queue}: ${posted.status}`);
} else {
  console.log(JSON.stringify(handover, null, 2));
}
```

## Example output

This is what the sample printed against production, trimmed to the first history item.

```json theme={null}
{
  "queue": "billing-escalations",
  "reason": "Customer is requesting a refund and threatening bank dispute, which requires specialized handling by our billing team.",
  "triage": {
    "category": "billing",
    "priority": "urgent",
    "sentiment": "negative",
    "language": "en",
    "summary": "Customer reports being charged twice for order 48213 and demands immediate refund or they will dispute with their bank.",
    "suggested_reply": "I understand you've been charged twice for order 48213 and need this resolved immediately. I've noted your concern about the double charge and your card ending in 4412. A refund specialist will contact you shortly to process this request and address your concerns.",
    "needs_human_handoff": true,
    "handoff_reason": "Customer is requesting a refund and threatening bank dispute, which requires specialized handling by our billing team."
  },
  "conversation_id": "conv_8a1688836dc64f51909a740bb2077d49",
  "history": [
    { "role": "user", "content": "I was charged twice for order 48213 and nobody answers my emails.", "id": "msg_ed756005c117425faca71c234b5b7368", "responseId": null, "metadata": null },
    "...",
    "..."
  ]
}
```

## Next steps

* [Create and run agents](https://docs.mka1.com/docs/managing-agents.md) for run status, history and versions
* [Conversations](https://docs.mka1.com/docs/conversations.md) for storing and continuing the chat after the handover
* [Use MCP tools](https://docs.mka1.com/docs/mcp-tools.md) for the approval flow in step 2
* [Chat connectors](https://docs.mka1.com/docs/agent-connectors.md) when the conversation arrives from a messaging channel


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.