AI Integration  ·  BraivIQ AI Engineering Playbook

AI Automation Code Examples: Triage For An Operations Inbox With Claude, Measured Before It Routes Anything

These AI automation code examples build the kind of inbox triage Barclays described on 1 October 2026, when it said Claude sorts around 120,000 client emails a day in Global Markets. The agent classifies each email through one strict tool, enriches it from your own trade records and routes it to a queue where a person acts. Requests to change bank details always go to a call-back check, whatever the model says. Nothing routes automatically until a parallel run shows high enough precision for that category on enough emails.

Published  ·  Updated  ·  12 min read  ·  By BraivIQ Engineering

Person typing on a laptop at a white desk, illustrating AI automation code examples for operations email triage

Key takeaways

  • Barclays said on 1 October 2026 that Claude classifies, enriches and routes around 120,000 client emails a day in Global Markets so operations colleagues have what they need to act. The model sorts, and people act.
  • Classify through one strict tool with an enum of categories. Forced tool use returns a 400 error on Claude Opus 5.5 and Sonnet 5.5, so use tool_choice auto and send any email without a tool call to a person.
  • Some routes should never be the model's choice. Any request to change bank or settlement details goes to a call-back check against a known number.
  • Earn automatic routing per category: run in parallel with the team, measure precision and recall, and switch a category on only when it clears your bar on enough emails.

120,000 - Client emails a day that Barclays says Claude classifies, enriches and routes in Global Markets (1 October 2026)  ·  0.98 - Precision a category must reach in the sample before it routes automatically  ·  50 - Minimum labelled emails per category before automatic routing is considered  ·  0 - Bank detail changes the model may route on its own

Inbox triage is one of the safest first uses of AI in operations because the model never acts. It reads an email, picks a category from a fixed list, pulls out references, and the email lands in the right queue with context attached. A person still does the work. These AI automation code examples show the two parts that make it dependable: the classifier with fixed routing rules, and the measurement that decides which categories can skip the manual sort.

Both samples run on Python 3.14.4 and 3.11.14 and pass mypy 2.4.0 in strict mode. The triage sample uses anthropic 1.14.0 against a stub client that returns real SDK types, so it runs without an API key. All emails and references are made up.

What did Barclays describe on 1 October?

In Anthropic's announcement, Barclays said Claude classifies, enriches and routes around 120,000 incoming client emails a day in Global Markets “so operations colleagues receive the information needed to act.” Our blog covered the operations side of the Barclays news. For engineers the useful detail is the verb list: classify, enrich, route. None of those sends anything to a client.

What do AI automation code examples for inbox triage need?

Four parts. A classifier that can only answer with a category from your list. Enrichment from your own systems, so the model never invents a trade status. Routing rules where some categories are fixed by policy. Measurement that compares the agent's labels with the team's on the same emails.

AI automation code examples diagram for email triage: client emails arrive in a shared inbox, a Claude classifier uses a strict tool with tool_choice auto, routing rules send bank detail changes to a call-back check and unlabelled emails to ops triage, enrichment looks up trade references, ops queues mean a person acts on every email, measurement gives precision per label, and an audit log keeps the email ID, label, route, reviewer and any override.
Classification is the easy part. Routing is the design. Tap to open full size.

How do you classify and route an email with Claude?

triage.py
"""Operations inbox triage: classify, enrich and route. People act, the agent never replies.

The model classifies each email through one strict tool. Forced tool use (tool_choice "any" or "tool")
returns a 400 error on Claude Opus 5.5 and Sonnet 5.5, so this uses tool_choice "auto" and treats a
reply without the tool call as "needs a person". Requests to change settlement instructions always go to
manual verification by call-back, whatever the model says.

Requires anthropic 1.14.0. Runs against a stub client that returns real SDK types, so no API key is
needed. Tested on Python 3.14.4 and 3.11.14 with mypy --strict. Sample emails and references only.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from email import message_from_string
from email.message import Message as EmailMessage

from anthropic.types import Message, ToolParam, ToolUseBlock

CATEGORIES = ["trade_query", "ssi_change", "document_request", "valuation_request", "other"]
CLASSIFY: ToolParam = {
    "name": "classify_email",
    "description": "Classify one operations email and extract references. Use null when absent.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "category": {"type": "string", "enum": CATEGORIES},
            "trade_ref": {"type": ["string", "null"]},
            "account_ref": {"type": ["string", "null"]},
        },
        "required": ["account_ref", "category", "trade_ref"],
        "additionalProperties": False,
    },
}
QUEUES = {"trade_query": "middle-office", "document_request": "client-services",
          "valuation_request": "client-services", "other": "ops-triage"}


@dataclass(frozen=True)
class Routed:
    subject: str
    category: str
    queue: str
    context: dict[str, str]


def classify(create: Callable[..., Message], email: EmailMessage, model: str) -> dict[str, object] | None:
    body = email.get_payload()
    reply = create(model=model, max_tokens=512, tools=[CLASSIFY], tool_choice={"type": "auto"},
                   messages=[{"role": "user", "content": f"Subject: {email['Subject']}\n\n{body}"}])
    calls = [b for b in reply.content if isinstance(b, ToolUseBlock) and b.name == "classify_email"]
    return dict(calls[0].input) if calls else None


def route(create: Callable[..., Message], raw: str, trades: dict[str, dict[str, str]], model: str) -> Routed:
    email = message_from_string(raw)
    subject = str(email["Subject"])
    result = classify(create, email, model)
    if result is None or result.get("category") not in CATEGORIES:
        return Routed(subject, "unclassified", "ops-triage", {"reason": "model did not call the tool"})
    category = str(result["category"])
    if category == "ssi_change":  # fixed rule, never the model's choice
        return Routed(subject, category, "ssi-verification-callback",
                      {"rule": "verify by call-back to a known number before any change"})
    context: dict[str, str] = {}
    trade_ref = result.get("trade_ref")
    if isinstance(trade_ref, str):
        context = trades.get(trade_ref, {"lookup": f"{trade_ref} not found in reference data"})
    return Routed(subject, category, QUEUES[category], context)


class StubMessages:
    """Returns real anthropic.types.Message objects. Scripted by subject line for a repeatable test."""

    SCRIPT = {
        "Allocation query": {"category": "trade_query", "trade_ref": "TRD-88302", "account_ref": None},
        "Updated bank details": {"category": "ssi_change", "trade_ref": None, "account_ref": "ACC-11893"},
        "Valuation": {"category": "valuation_request", "trade_ref": None, "account_ref": "ACC-20417"},
    }

    def create(self, **kwargs: object) -> Message:
        messages = kwargs["messages"]
        assert isinstance(messages, list)
        text = str(messages[0]["content"])
        hit = next(((k, v) for k, v in self.SCRIPT.items() if k in text), None)
        content: list[dict[str, object]] = (
            [{"type": "tool_use", "id": "toolu_01", "name": "classify_email", "input": hit[1]}] if hit
            else [{"type": "text", "text": "I am not sure how to classify this email."}])
        return Message.model_validate({
            "id": "msg_stub", "type": "message", "role": "assistant", "model": "claude-haiku-5-5",
            "content": content, "stop_reason": "tool_use" if hit else "end_turn", "stop_sequence": None,
            "usage": {"input_tokens": 300, "output_tokens": 40}})


INBOX = [
    "Subject: Allocation query TRD-88302\n\nWe show 1,000 not 1,500 on the second fill, please confirm.",
    "Subject: Updated bank details\n\nPlease use our new account for settlement from Monday.",
    "Subject: Valuation for year end\n\nCould you send the 30 September valuation for ACC-20417?",
    "Subject: Lunch on Friday?\n\nAre you free?",
]
TRADES = {"TRD-88302": {"trade_ref": "TRD-88302", "counterparty": "Sample Broker LLP", "status": "unconfirmed"}}

if __name__ == "__main__":
    stub = StubMessages()
    for raw in INBOX:
        r = route(stub.create, raw, TRADES, model="claude-haiku-5-5")
        print(f"{r.subject[:28]:<28} {r.category:<18} -> {r.queue:<26} {r.context}")

Requires anthropic 1.14.0. Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict. Stub client, sample emails.

The allocation query goes to middle office with the trade's counterparty and status from reference data. The bank details email goes to the call-back queue under a fixed rule. The valuation request goes to client services. The lunch invitation gets no tool call and lands in ops triage for a person. The strict tool means a reply cannot contain a category outside the list.

How do you decide which categories can route automatically?

Run the agent beside the team for a few weeks. Each email gets the team's label and the agent's label. The script below computes precision and recall per category and switches automatic routing on only where precision is at least 0.98 on 50 or more emails. ssi_change never qualifies.

measure.py
"""Measure triage accuracy on labelled emails before anyone relies on it.

Computes precision and recall per category from a parallel run (the agent's label against the label the
team gave the same email), and decides which categories may be routed automatically. A category stays
with a person until it clears the precision bar on enough emails. ssi_change never auto-routes.

Standard library only. Tested on Python 3.14.4 and 3.11.14 with mypy --strict. Sample labels only.
"""

from __future__ import annotations

import csv
import io
from collections import Counter
from dataclasses import dataclass

MIN_PRECISION = 0.98
MIN_SUPPORT = 50  # labelled emails per category before auto-routing is even considered
NEVER_AUTO = {"ssi_change"}


@dataclass(frozen=True)
class CategoryResult:
    category: str
    support: int
    precision: float
    recall: float
    auto_route: bool


def evaluate(rows: list[tuple[str, str]]) -> list[CategoryResult]:
    """rows: (team_label, agent_label) pairs from the parallel run."""
    tp: Counter[str] = Counter()
    predicted: Counter[str] = Counter()
    actual: Counter[str] = Counter()
    for team, agent in rows:
        actual[team] += 1
        predicted[agent] += 1
        if team == agent:
            tp[team] += 1
    results = []
    for cat in sorted(actual):
        precision = tp[cat] / predicted[cat] if predicted[cat] else 0.0
        recall = tp[cat] / actual[cat]
        auto = (cat not in NEVER_AUTO and actual[cat] >= MIN_SUPPORT and precision >= MIN_PRECISION)
        results.append(CategoryResult(cat, actual[cat], round(precision, 3), round(recall, 3), auto))
    return results


SAMPLE_CSV = (
    "team_label,agent_label\n"
    + "trade_query,trade_query\n" * 118 + "trade_query,other\n" * 2
    + "document_request,document_request\n" * 61 + "document_request,valuation_request\n" * 3
    + "valuation_request,valuation_request\n" * 40
    + "ssi_change,ssi_change\n" * 12
    + "other,other\n" * 30 + "other,trade_query\n" * 1
)

if __name__ == "__main__":
    reader = csv.DictReader(io.StringIO(SAMPLE_CSV))
    rows = [(r["team_label"], r["agent_label"]) for r in reader]
    print(f"{'category':<18} {'n':>4} {'precision':>9} {'recall':>7}  route")
    for r in evaluate(rows):
        print(f"{r.category:<18} {r.support:>4} {r.precision:>9.3f} {r.recall:>7.3f}  "
              f"{'automatic' if r.auto_route else 'person'}")

Standard library only. Tested on Python 3.14.4 and 3.11.14 with mypy 2.4.0 --strict. Sample labels.

In the sample, trade queries (precision 0.992 on 120 emails) and document requests (1.000 on 64) route automatically. Valuation requests stay manual at 0.930 precision, because the agent sent three document requests there. other stays manual at 0.938, and ssi_change stays manual by rule despite a perfect score.

What breaks in production?

  • **Emails with two requests.** One category per email loses the second request. Allow a secondary category or route mixed emails to a person.
  • **Instructions inside emails.** An email can say “ignore your rules”. The classifier has no tools that act, so the worst case is a wrong queue, and the fixed rules still apply.
  • **Drift.** New products and counterparties change the mix. Keep sampling routed emails for review and recompute precision monthly.
  • **Reference data gaps.** If a trade is not found, say so in the context rather than letting the model guess, as the sample does.

What are the alternatives to an LLM classifier?

OptionGood fitTrade-off
Keyword and sender rulesFew, stable categoriesBreaks on new wording and needs constant upkeep
A small trained classifierHigh volume with lots of labelled historyNeeds training data and retraining as language changes
LLM with a strict tool, as aboveVaried wording and reference extractionCost per email and the need to measure before trusting it
Decision models that score a closed setTeams that want calibrated probabilitiesA newer approach with fewer production references

Decision models are covered in our LLM classification playbook.

Where should an operations team start?

Export a month of emails with the queue each one ended up in, use that as your labelled set, and run the classifier in parallel without routing anything. Our Workflow Automation London team can set this up as a 14-day Proof Run, and our services page lists the operations workflows we build as a Workflow Automation Agency and AI Agency UK team.

Frequently asked questions

Can AI reply to client emails in operations?

It can draft replies, but in the pattern shown here it does not send anything. It classifies, enriches and routes, and a person acts. If you add drafting later, put a named approval in front of every outgoing message.

Why not use forced tool use for classification?

Anthropic's release notes say tool_choice set to any or to a named tool returns a 400 error on Claude Opus 5.5 and Sonnet 5.5. Use tool_choice auto with a strict tool and treat any reply without a tool call as needing a person.

How accurate does email triage need to be before it routes automatically?

Set the bar per category from the cost of a wrong route. The sample uses 0.98 precision on at least 50 labelled emails, and keeps bank detail changes manual whatever the score.

References

  1. Anthropic, "Barclays scales Claude", 1 October 2026. https://www.anthropic.com/news/barclays-scales-claude
  2. Anthropic, "Claude Developer Platform release notes", September and October 2026. https://platform.claude.com/docs/en/release-notes/overview
  3. PyPI, "anthropic 1.14.0", 14 October 2026. https://pypi.org/project/anthropic/1.14.0/