← Blog

How to Build a Claude-Powered Email Classifier

2026-11-04 · 5 min read · SubToAPI Team

Building a Claude-powered email classifier means sending incoming email content to Claude with a structured prompt that returns a category, priority, and any extracted fields, then using that output to route, tag, or trigger downstream actions. Unlike keyword-matching filters, Claude understands context, tone, and intent, so it can tell a frustrated cancellation request apart from a casual question even when both use similar words.

This guide walks through the full build: defining categories, writing a reliable classification prompt, enforcing structured output, handling edge cases, and wiring it into a pipeline you can run in production.

Step 1: Define your classification schema

Before writing any prompt, decide exactly what you need Claude to return. A vague schema produces vague results. A good starting schema for a support or sales inbox looks like:

{
  "category": "billing | bug_report | feature_request | spam | sales_lead | other",
  "priority": "low | medium | high | urgent",
  "sentiment": "positive | neutral | negative",
  "summary": "one sentence summary",
  "requires_human": true
}

Keep the category list short — 5 to 8 options max. Models (and humans) classify more consistently with fewer, well-separated buckets. If you need finer granularity later, add a second-level subcategory field rather than expanding the main list.

Step 2: Write a deterministic classification prompt

The prompt should be explicit about output format and give Claude no room to add commentary. A system prompt plus a user message containing the raw email works well:

System: You are an email classification engine. Analyze the email and
return ONLY a JSON object matching this schema, with no extra text:

{
  "category": "billing|bug_report|feature_request|spam|sales_lead|other",
  "priority": "low|medium|high|urgent",
  "sentiment": "positive|neutral|negative",
  "summary": "string, max 20 words",
  "requires_human": boolean
}

Rules:
- "urgent" priority only for account lockouts, security issues, or
  explicit threats to cancel/churn.
- "spam" overrides all other categories.
- If the email is ambiguous, pick the closest category and set
  requires_human to true.

The "Rules" section matters more than most people expect. Without explicit tie-breaking logic, Claude will make reasonable but inconsistent choices on edge cases — exactly the emails you most need consistency on.

Step 3: Enforce structured output with tool use

Asking for "JSON only" in plain text usually works, but it's not guaranteed — the model can occasionally wrap output in markdown fences or add a trailing note. The more robust approach is Claude's tool use feature: define a classify_email tool with a strict input schema, and force Claude to call it.

const response = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "claude-sonnet-4-5",
    max_tokens: 300,
    tools: [{
      name: "classify_email",
      description: "Classify an inbound email",
      input_schema: {
        type: "object",
        properties: {
          category: { type: "string", enum: ["billing","bug_report","feature_request","spam","sales_lead","other"] },
          priority: { type: "string", enum: ["low","medium","high","urgent"] },
          sentiment: { type: "string", enum: ["positive","neutral","negative"] },
          summary: { type: "string" },
          requires_human: { type: "boolean" }
        },
        required: ["category","priority","sentiment","summary","requires_human"]
      }
    }],
    tool_choice: { type: "tool", name: "classify_email" },
    messages: [{
      role: "user",
      content: `Classify this email:\n\n${emailBody}`
    }]
  })
});

Forcing tool_choice guarantees the response comes back as structured arguments rather than free text, which means you can parse it directly without regex or JSON-repair logic. This is the single biggest reliability improvement you can make over a plain-text prompt. See /docs/tools for the full tool-use reference.

Step 4: Handle multi-label and borderline cases

Real inboxes don't fit neatly into one category per email. A customer might report a bug and also threaten to cancel. Two options:

  1. Primary + secondary category: add an optional secondary_category field so the router can still take one primary action but downstream analytics keep the nuance.
  2. Confidence-gated review queue: when requires_human is true or the category is other, route to a human review queue instead of auto-processing. This keeps automation safe without needing a confidence score, which LLMs don't reliably self-report anyway.

Avoid asking the model for a numeric confidence score (e.g., "0.0–1.0") — these numbers tend to cluster near round values and don't correlate well with actual accuracy. A boolean requires_human flag driven by explicit rules is more trustworthy.

Step 5: Build the pipeline

A minimal production pipeline looks like:

  1. Inbound email hits your mail webhook (SendGrid, Postmark, SES, etc.)
  2. Strip HTML, signatures, and quoted reply chains — send only the new message content to reduce token usage and noise
  3. Call Claude with the forced tool call above
  4. Parse the tool call arguments into your schema
  5. Route based on category and priority: create a ticket, tag a CRM lead, auto-archive spam, or escalate to a human queue
const toolCall = response.content.find(c => c.type === "tool_use");
const result = toolCall.input;

if (result.category === "spam") return archiveEmail();
if (result.priority === "urgent" || result.requires_human) return escalateToHuman(result);
return routeToTeam(result.category);

For teams running this across hundreds or thousands of emails a day, running the classifier through SubToAPI gives you a single API key per environment, usage metadata per request so you can track classification volume and cost by team or project, and streaming support if you later add a reasoning step before the final classification. Set it up once following /docs/quickstart and reuse the same key across your support, sales, and ops inboxes.

Step 6: Monitor and iterate

Classification prompts drift in effectiveness as your email mix changes. Log every classification result alongside the original email (with PII handling as needed) and periodically sample 50–100 for manual review. Track disagreement rate between the model and a human reviewer by category — this tells you exactly which category's prompt wording needs tightening, rather than treating accuracy as one global number.

Questions

Can Claude classify emails in languages other than English? Yes. Claude handles multilingual input well; keep the schema and category labels in English for consistent downstream parsing, but the email body itself can be in any language.

How do I prevent the model from inventing new categories? Use tool use with an enum constraint on the category field, as shown above. This makes it structurally impossible for Claude to return a value outside your defined list.

Do I need a different model for high-volume classification? A smaller, faster Claude model is usually sufficient for classification since the task is narrow and well-defined. Reserve larger models for cases requiring deeper reasoning, like drafting a reply based on the classification result.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →