
Articles · Inbox · 17 min
How do I auto-triage email and Slack with an AI agent without a human in every thread?
You auto-triage email and Slack by giving each thread four exits: classify, file, reply, escalate. The agent writes a class, files the thread, replies only on scored classes, and hands the rest to a person with a packet. Guardrails sit on input, tools, and output. Stop rules sit in the runtime. People set policy and grants. They do not sit in every thread.
By Eric · Rome · Aug 29, 2026
A shared inbox plus a Slack channel is the desk. Done is junk archived, the customer thread in the CRM, a reply on the one class you have scored, and a packet for the rest. A summary in a chat window is not triage.
A person in every thread is not safety. It is a slow chatbot. How to run AI agents without a human is the same grant applied here: people sit at the edge, not inside every click.
Building the inbox agent, the connectors and the mailbox grants, is a separate job from this triage loop. I do not let a model wander the mailbox. I give it four exits and I make the runtime own the stop.
Auto-triage is a job with four exits: classify, file, reply, escalate. The agent reads a thread, writes a class, puts the thread where it belongs, sends only when policy allows, and hands the rest to a person with evidence. A human sets the policy and the grants, and they do not sit in every Slack thread.
Auto-triage is a finish line
Done is not a summary in a chat window. Done is a class on the thread, a place it lives, a reply only when the class allows it, and an escalation packet when it does not. If a script cannot see those four things, you still have a person in the loop.
Write the finish line first. A newsletter is class noise (label and archive), a paid invoice question is class money (file to finance, escalate, never send), and a password reset that matches a template is class customer_how_to (file to the ticket and send the template).
If you cannot name those artifacts, you do not have an agent. You have a chat about the inbox. Anthropic’s Building effective agents (19 December 2024) draws the line I use on every desk: a workflow is a predetermined path, and an agent is a model that directs its own tool use.
Inbox triage is mostly a workflow with a small agent at the edges. Known classes follow the path. Unknown classes, and replies that need a lookup, get a short loop inside the grants.
An open prompt over Gmail and Slack is how you double-send a customer and call it autonomy. Filters still win on the boring slice. A Gmail filter that archives receipts is a script, and you should keep it.
Save the model for threads a rule cannot see: mixed intent, a customer who switched from email to Slack, a vendor who changed the subject line. Anthropic’s same 2024 post says to find the simplest solution and add complexity only when it improves the outcome: a Zap for receipts, a router for the rest.
- A class written to the thread, with an id a script can read.
- A place: label, folder, channel pin, or CRM row.
- A reply only if the class is on the send or draft list.
- An escalation packet if the class is money, legal, incident, or unknown.
- A stop the runtime owns. The model does not get extra turns to be helpful.
Classify the thread before any tool writes
Routing is the pattern: you name the classes first, then you send each thread to the action that class owns. One prompt that tries to be inbox, support, legal, and sales will get worse as you tune it, so split the classes before the agent writes anything.
Anthropic names this the routing workflow in Building effective agents (19 December 2024). Routing classifies an input and directs it to a specialized followup task so you can separate concerns, and so that optimizing one kind of input stops hurting the others.
Routing classifies an input and directs it to a specialized followup task.
Their examples are customer service queries, and easy work on a small model versus hard work on a larger one. Mail and Slack are the same shape. I write eight classes on paper before I write a tool: noise, FYI, action internal, customer, money, legal, incident, and unknown.
A 21 Aug 2026 r/AI_Agents write-up on a local 9B model saw graph workflows burn fewer tokens than a ReAct loop on email triage, with no accuracy win. That is cost, not a close. I still classify in code and keep the loop off send.
If a thread could be two classes, the policy picks the higher blast radius, so money beats customer, legal beats FYI, and unknown beats a guess. Classification is a structured object, not an essay: class, confidence, evidence spans, language, who must act, and a deadline if the thread states one.
The runtime stores that object against the thread id, and the next step reads it instead of re-litigating the mail. Unknown is a first-class class, not a failure of the model: it is how you keep a person out of the easy threads without letting the agent bluff the hard ones.
Low confidence is unknown, mixed money and legal is unknown, and a new sender asking for a wire is unknown even if the prose looks friendly. Freeze real threads and gold-label them before the agent may apply a label in production: last month’s mail, last week’s Slack, and the ones you would not put in a demo.
Score class match, not how pretty the rationale is. If noise and customer swap, you are not ready to file, let alone send. Anthropic’s routing note is explicit: easy and common work can go to a cheaper model, hard and unusual work to a more capable one.
I do not spend a frontier model to decide that a newsletter is a newsletter, and I do spend one when the class is customer and the reply needs a lookup. Read enough of the thread to see the ask, then cap the read: a long incident thread gets a window and a search tool, not a paste of the whole history.
File it where the next person looks
Filing is the artifact people actually use: a label, a folder, a Slack pin, a CRM row. Summaries decay, and filed threads can be found on Monday. If the agent cannot write to the system of record, it is still a chatbot describing your mail.
File is a small write. apply_label is not send_mail, and add_reaction is not reply_in_thread, so I split those tools. The MCP spec for tools (modelcontextprotocol.io, 2024-2025 spec) treats tools as model-controlled: the model may pick them, and your runtime still decides which names exist.
Each class owns a place, and the table later in this piece is the map I actually run. An emoji is a useful Slack ack if your team already uses it that way, and it is not a system of record. If the next person looks in a channel, a list, or a ticket, write there.
If you file twice you will reply twice, so I key a thread to a person or a company when the connectors can see it and store the class on that key. The second surface inherits the first class unless the new message changes the blast radius. Filing should be idempotent on thread id: the tool either writes or returns already_filed, and the log is the record, not the model’s rationale.
Reply only on scored classes
Reply is a write with a blast radius, and draft and send are different grants. I let an agent draft on classes I have scored, and I let it send only on classes with a template, a freeze set, and a passing score. Everything else waits.
How to run AI agents without a human makes the same split: draft is not send, and a branch is not production. Inbox is the place teams skip that split because a reply feels small. It is your name on a customer thread.
Start send on the classes where done is obvious: a published how-to, office hours, we got the file. Template, placeholders from the thread, no invention. If the model wants to add a promise the template does not contain, that is a stop, not a better reply.
Customer classes that need a lookup can draft: the agent retrieves the allowed doc, writes a draft, and stops. A person sends until that class holds on the frozen set, then you promote the class, not the whole inbox. Autonomy is a grant, like a permission.
Money, legal, incident, and unknown never get the send tool, and neither does a first reply to a new vendor asking for a change of bank details. The runtime checks those rules before the send tool exists in the tool list for that run.
Match the surface: email reply stays on email, Slack reply stays in the thread. Quiet hours and rate limits sit in the runtime, so a scored class still waits after close of business, and it still waits if this thread already got an agent reply this run. The send tool returns sent, denied, already_sent, or timeout, and the model does not pick the retry.
Escalate with a packet, not a ping
Escalation is not a panic ping. It is a handoff packet: the original finish line, the evidence, the next action, and the limits. The person who picks it up should not re-read the whole thread to learn why they were tagged.
I use the same packet I wrote for How to hand off an agent to a human: goal, evidence, next action, limits. The outgoing party is the inbox agent. The incoming party is a person, or a specialist if you later split legal from support.
- Goal: the original finish line, unchanged. Close the thread, or get a human decision.
- Evidence: thread id, class, spans, attachments named by id, CRM id if it exists.
- Next action: the one thing the person must do. Approve a refund. Confirm a wire.
- Limits: spend left, what the agent already sent, deadline the thread stated, grants it did not have.
A chat dump is how fleets die, and it is how inbox escalations die. If you paste the model’s chain of thought into Slack, the next person skims it and misses the wire instruction in message four. Write the decision, link the thread, and stop.
Escalate on class, not on mood: money, legal, incident, unknown, a tripped send, and a customer who asked for a person. That last ask is a class, not a chance for the agent to prove it can still help. The tool is named open_escalation, with a thread id and a packet, and it writes to the queue the team already watches.
The packet is the product, and the ping is optional and narrow. If a specialist agent takes the packet later, it continues and does not re-ask the whole question. That rule is in how to hand off an agent to a human for a reason.
Put guardrails on input, tools, and output
Guardrails are checks the runtime runs on input, on each tool, and on the final output, not a nicer prompt. When a tripwire fires, the run stops. Use a cheap check before a send, and do not wait for the model to notice it was about to email a customer a guess.
OpenAI Agents SDK Guardrails docs (2026) describe three families: input guardrails on the initial user input (first agent only), output guardrails on the final output of the last agent, and tool guardrails around each custom function-tool. If your run includes a router and a writer, put the send check on the send tool, not on the writer’s input.
| Family | When it runs | Inbox use |
|---|---|---|
| Input | First agent only | Drop prompt injection in a pasted email, secret-looking content, asks that are out of policy |
| Tool | Every function-tool call | Block send without a class grant, reject secret-looking args, cap recipients |
| Output | Last agent only | Block promises, leaked tokens, off-policy legal language, a reply that ignores the template |
A guardrail returns whether a tripwire triggered. In the SDK that is tripwireTriggered, or tripwire_triggered in Python. True means the runner raises and the run stops, which is the contract I want on send.
Input guardrails have two execution modes in the same 2026 docs. Parallel, the default, starts the guardrail beside the model, which is faster, but if the tripwire later fires the model may already have spent tokens or called a tool. Blocking mode runs the guardrail first so the expensive model never starts, and I use blocking on the send path.
Tool guardrails in the SDK can allow, reject the content with a message, or throw. I throw on money, legal, and any attempt to add a recipient that was not on the thread. I reject when the draft still contains a placeholder the template engine missed, so the model can escalate instead of sending.
OpenAI Agents SDK guide on guardrails and human review (2026) puts the split in one line I keep: guardrails validate automatically, and human review pauses the run so a person or a policy can approve a sensitive action. Together they define continue, pause, or stop. Pause before side effects: sends, edits, and sensitive MCP actions.
Anthropic’s 2024 post describes parallelization as a way to run guardrails: one model processes the query while another screens it, a split they found tends to beat a single call that both answers and polices itself. I keep the classifier and the safety check as separate calls, and I put the output check on the tool before the HTTP call, because a tripwire after send does not unsend the mail.
Write stop rules the runtime can kill
Stop rules are owned by the runtime, not the model. Cap steps, spend, and sends. Unknown class and a repeated send on the same thread stop the reply path, and the model does not get a vote on those caps.
Anthropic’s Building effective agents (19 December 2024) is blunt about agents in a loop: the task often ends on completion, and it is also common to include stopping conditions, such as a maximum number of iterations, to keep control. They recommend testing in a sandbox and putting guardrails on. Inbox is not a sandbox, so the stop rule is how you keep production from becoming one.
- Unknown, money, legal, incident: no send tool in the list for that run.
- One agent reply per thread per run. A second send is a stop.
- Max tool calls per thread, and max threads per window, so a retry storm cannot clear the inbox.
- Spend cap for the mailbox. The runtime kills the trace.
- A person already replied after the agent started: stop and file.
- Send denied by a guardrail: escalate, do not rephrase unless the policy says one rewrite.
- The same tool error twice: stop.
Completion is a stop too. Class written, file written, reply sent or skipped, escalation written or skipped. If those four bits are set, the run is done, and the runtime should not ask the model whether it feels finished.
Retries are a stop rule in disguise: retry a dropped connection, do not retry a 403 by switching to Slack, and do not retry a customer who said stop. I keep the stop rules in code, next to the tool grants, because a prompt that says please stop after one reply is a wish, not a stop rule.
Run classify, file, reply, escalate in that order
The order is the product: classify, then file, then reply or escalate. Do not let the model pick a clever order per thread. Guardrails wrap the writes, the stop rule sits outside the prompt, and that is the loop I put on a desk.
That sequence is a workflow in Anthropic’s 2024 sense: a predetermined path with a model inside the boxes. I only open a free tool loop when the class is customer and the reply needs retrieval, or when unknown needs a tighter read of an attachment, and even then the agent may search and still may not send.
Do not invert the order. Send-then-classify is vandalism, reply-then-file is how CRM rows go missing, and classify-then-send that skips file is how the next search dies. Do the cheap write before the loud one.
Keep the tool list short and named
Name the tools the way you name permissions: search is not send, and Slack is not mail. Each tool has a schema, a timeout, and a grant. The MCP spec treats tools as model-controlled, so the runtime refuses calls the grant does not allow.
The Model Context Protocol specification for tools, published at modelcontextprotocol.io, says servers expose tools the model can discover and invoke. Clients list them with tools/list and call them with tools/call. A tool has a name, a description, an inputSchema, and an optional outputSchema, and that contract is the prompt, so be exact.
I expose a short list for inbox: list_unread, read_thread, apply_label, archive_thread, upsert_crm_note, draft_reply, send_reply, reply_in_thread, open_escalation. That is already nine, and I do not add browse_the_open_web or invite_to_channel by default.
The MCP tools spec is explicit about safety: there should always be a human in the loop with the ability to deny tool invocations, and applications should make the exposed tools clear, mark when tools fire, and confirm sensitive operations. Ability to deny is the edge I keep: confirm send, confirm a new recipient, and let classify run.
The same spec tells servers to validate inputs, enforce access control, rate limit, and sanitize outputs, and tells clients to show tool inputs before sensitive calls, validate results, time out calls, and log usage. I treat those as runtime requirements. An inbox agent without a tool log is a rumor.
Tool execution errors in MCP are returned with isError true so the model can correct itself, while protocol errors are for unknown tools and malformed calls. I pass execution errors through (invalid date, unknown thread id, denied), I do not pass a stack trace, and I do not let the model fix a deny by picking a different send tool.
Annotations on MCP tools are untrusted unless the server is trusted, so a tool marked read-only by a third-party server is not read-only in my grants. OpenAI Agents SDK tool guardrails wrap function tools you define, so do not assume a hosted mail connector inherits your send guardrail. Anthropic’s 2024 appendix on tools says to spend as much care on the tool interface as on the prompt: absolute ids, enums for class and action, not free text where you can avoid it.
| Class | File | Reply | Escalate |
|---|---|---|---|
| noise | Label and archive | No | No |
| fyi | Topic folder or pin | No | No |
| action_internal | Owner plus due if stated | Draft if asked | If blocked |
| customer | Ticket or CRM | Template send or draft | If intent is unclear |
| money | Finance folder | Never | Yes |
| legal | Hold folder | Never | Yes |
| incident | Incident channel | Status template or never | Yes |
| unknown | Holding | Never | Yes |
Score frozen threads before you leave the loop
Freeze real threads and gold-label the class and the action, then run the agent. Score the artifact, not the prose, and promote a class to unsupervised only when it holds. Inbox triage can leave the inner loop before legal ever should.
I do not publish volume numbers I cannot cite. I can tell you the mechanism: take last month’s mail and a week of the shared Slack channel, label class and expected action by hand, and keep that set frozen so this week can be compared to last week. Pretty traces that file the wrong thread are zeros.
Score four bits: class match, file destination match, reply decision match (send, draft, skip), and whether an escalation packet is present when the gold label says it should be. A fluent wrong reply is a miss. A correct archive with a long rationale is a hit.
Anthropic’s 2024 post says you add complexity only when it demonstrably improves outcomes. If a Gmail filter already archives the newsletters, do not put a model on that slice. If the classifier fails money versus customer, fix the classes and the examples, do not add a second agent.
Promote one class at a time: noise first, then FYI, then template customer, with money escalated until the score is boring. How to run AI agents without a human is this promotion rule, not a company-wide switch. The person at the edge reads the tool log, changes the policy when the score drops, and treats LLM-as-judge as a tone check, not the only score.
Leave people at the edge
People write the policy, grant the tools, and read the misses. They deny a send class rather than clicking archive on every newsletter. That is how you run an inbox agent without a human sitting in every mail and Slack thread.
The MCP spec’s human-in-the-loop language is about the ability to deny sensitive calls, not about a person in the inner loop. OpenAI’s 2026 human-review guide is about pause before side effects. Both fit: classify and file can run, send pauses until the class is promoted, and money never gets the send tool.
A person still owns first contact on a new policy, a new mailbox, and any class you have not scored, plus legal language and anything that moves money. Unsupervised does not mean no owner. It means the owner designed the runtime, the eval, and the grants, then got out of the easy threads.
If you cannot name the finish line, keep the human in the thread. If you can name it, and the frozen set holds, take them out of that class. Building the inbox agent is how you wire the tools, and this loop is how you use them without turning Slack into a second chat window for the model.
Questions
Yes, if the class, the grants, and the thread identity are shared. Keep send_mail and reply_in_thread as different tools. File once per person so you do not reply twice when they switch surfaces.
Draft until a class holds on a frozen set. Send only on classes with a template and a passing score. Money, legal, incident, and unknown never get the send tool.
A limit the runtime can kill: max steps, spend, one send per thread, no send on forbidden classes, stop if a person already replied. A prompt that asks the model to be careful is not a stop rule.
No. Start with one operator and a routing workflow: classify, then file, then reply or escalate. Split only when a specialist would need grants the inbox agent should never hold.
Keep the filters for the slice a rule can see. Use the agent for mixed intent, two surfaces, and replies that need a lookup. Anthropic’s 2024 guidance is to add that complexity only when it improves the outcome you measure.
Next

