Email Classification Agent
The Email Classification Agent turns a shared mailbox into a queue that sorts itself. On each run it reads every unread message in the inbox, decides which of your categories it belongs to, and moves it into that category's folder. Anything no category fits goes to a fallback folder rather than being guessed into a bucket.
Like the Email Agent, it has no chat interface. You configure it once in the Admin UI and trigger it programmatically — by another workflow, or via the API.
There is no built-in schedule yet
The agent has no recurring trigger you can set in the Admin UI. Every run has to be started from outside, so "triages itself" means "triages itself each time something asks it to". Until scheduling lands, drive it from whatever already runs on a timer in your environment.
It never sends email
The agent speaks IMAP only — there is no SMTP anywhere in it. It reads, it files, it creates folders. It cannot put a message on the wire. This is a design boundary, not a setting.
Categories are yours, not ours
There is no built-in taxonomy. You define the categories, and you can add or rename one at any time without a deployment. What makes classification work is the description you write for each — see below.
What it does
- List. Every unread message in the inbox, oldest sent first, up to Max Unread Messages.
- Fetch and archive. Each message is fetched in full. Its attachments and the original message are written to the platform's file storage, so the complete mail is preserved even after it has been moved.
- Classify. The configured model is shown your category names and descriptions and picks one per message — or declines, if none fits.
- File. Each message is moved into the folder for its category. If the folder does not exist, the agent creates it and subscribes it, so it shows up in your mail client.
- Report. The run records how many messages were filed and how many landed in each category.
Why re-running is safe
Filing is what prevents double work. Every message — categorised or not — leaves the inbox, so the next run's unread listing simply cannot see it again. There is no flag to get out of sync and nothing to clean up. If a run fails half-way, the messages it already filed stay filed and the rest are still sitting unread, ready for the next run.
The message stays unread
Mail is read with BODY.PEEK, so the agent never marks anything as seen. A person opening the Support folder still sees genuinely unread mail waiting for them — the agent sorted it, it did not handle it.
Configuration
Create a profile from the Email Classification Agent blueprint in the Admin UI.
Mailbox connection
The same fields as the Email Agent: host, port, username, password, TLS, inbox folder, and Max Unread Messages. There is no "processed folder" here — the classifier decides where each message goes.
Categories
A repeating list. Add one entry per category:
| Field | Description |
|---|---|
| Category | A short name, e.g. support_request. Must be unique. |
| Target Folder | Where mail in this category is filed. Created automatically if it does not exist. Must be unique. |
| What Belongs Here | A description of the kind of mail that belongs in this category. This is what the model reads. |
| Draft a Reply | When on, mail filed here also gets a drafted reply left in your Drafts folder. Off by default. |
Write the description for a new colleague, not for a search engine
This field does the real work. Folder names alone cannot separate an information request from a support request — but a description can: "we can resolve this by providing information" versus "this requires an action from our team". Describe the sender's intent and what handling the mail would involve. Listing keywords works far worse than one clear sentence about what the category is for.
Nested folder names use your server's separator
A target folder like Triage/Support builds a real folder tree only on servers whose hierarchy separator is / — Gmail among them. On a server that uses . you would get one flat folder literally named Triage/Support; write Triage.Support there instead. Mail is filed correctly either way, but only the matching separator gives you a tree. If you are unsure, use flat names like Support and Invoices.
Classifier
| Field | Default | Description |
|---|---|---|
| Fallback Folder | Uncategorised | Where mail the model is unsure about goes. Never guessed into a category, never left in the inbox. |
| Classification Model | (empty) | The model that classifies. Leave empty to use the agent's main model. |
| Classification Prompt | (a sensible default) | Instructions steering how the model chooses. |
How a message ends up in the fallback folder
The model is given one way out: it can say outright that none of the categories fit. When it does, the message goes to the fallback folder instead of a category folder.
An earlier version also asked the model to rate its own confidence and diverted anything below a threshold. That setting was removed. A self-reported score is written in the same breath as the answer rather than measured, so it adds no information the choice does not already carry — measured across the platform's chat models on a deliberately ambiguous message, the explicit decline caught it four times out of five while the threshold never once fired, and the one model that misfiled did so at 0.95 confidence.
The practical consequence: your category descriptions are the safety net, not a dial. If mail is landing in the wrong folder, sharpen the descriptions of the two categories being confused.
Schedule
The agent can run itself. Enable Schedule on the profile and give it the five cron positions plus a timezone; leave it disabled and the agent only runs when something triggers it.
| Field | Example | Description |
|---|---|---|
| Minute | 0 | 0-59. */15 means every fifteen minutes. |
| Hour | * | 0-23. |
| Day of month | * | 1-31. |
| Month | * | 1-12. |
| Day of week | * | 0-6, Sunday is 0. |
| Timezone | Europe/Zurich | The zone the positions are read in. |
The timezone is what makes "every day at 08:00" mean the same thing all year: occurrences are worked out in that zone, so a daily schedule keeps its wall-clock time across daylight-saving changes.
Every position has to be filled in. A blank or malformed one is rejected when you save, rather than being stored and quietly breaking the profile.
Start hourly, then tighten
0 * * * * — on the hour — is a good first schedule. It gives you a run an hour to inspect before you decide whether the mailbox needs draining more often.
What happens when a run overlaps the next occurrence
A large or slow mailbox can still be filing when the next scheduled time comes around. Only one run at a time may hold a mailbox: the second one reports that a previous run is still filing and stops without touching any mail. The occurrence is skipped rather than queued, and the next one runs normally.
This matters because a message is still unread right up until it moves. Without the guard, two overlapping runs would both read the same mail, both pay to classify it, and both try to file it.
A run keeps renewing its claim for as long as it is working, so how long a batch takes never causes it to lose the mailbox — the ten-minute expiry only starts counting down once the run has actually stopped. If a run fails outright, the claim lapses on its own within ten minutes and the next scheduled run proceeds, so a crash costs you at most one occurrence.
In the rare case where a run does lose its claim mid-flight — a mailbox or a model stalled for longer than ten minutes in a single step — it stops before filing anything rather than filing mail another run may already be moving. That shows up as a failed run in tracing, and it is worth investigating: it means something took far longer than it should.
Three things to know about scheduled runs
- They are invisible in the chat UI. A scheduled run belongs to no user, so it does not appear in anyone's conversation list. Watch them in tracing instead. Choosing who sees them is planned separately.
- They are not metered. Usage limits are enforced on requests that arrive over HTTP, and a scheduled run does not make one. A very frequent schedule against a busy mailbox can consume a lot of model budget unnoticed — the Max Messages cap bounds a single run, and the schedule bounds how often that happens.
- The agent has to be running. If the agent is offline when a scheduled time passes, that occurrence is skipped, not queued. Nothing is lost: the mail is still unread and the next run that does fire picks it up.
Reply drafting
Turn on Draft Reply to have the agent write a reply for the categories that warrant one, and leave it in your Drafts folder for a person to read, edit and send. Nothing is ever sent by the agent — there is no mechanism in the platform for it to send mail at all.
Drafting is chosen per category, on the Draft a Reply switch in the category list. That is the point of the feature: a complaint usually deserves a reply, a thank-you rarely does, and an invoice wants paying rather than answering. Mail that fitted no category and went to the fallback folder is never drafted — if the model could not say what a message was about, it is in no position to answer it.
| Field | Description |
|---|---|
| Draft Reply | The master switch for this section. Off by default. |
| Drafts Folder | Where drafts are appended. If the name is not found, your server's own Drafts folder is used, and failing that the name is created. |
| LLM Model | The model that writes the reply. Leave empty to use the agent's main model. |
| Draft Prompt | Instructions for tone and style. The shipped default is concise, polite, and forbids inventing facts. |
| Draft Input Tokens | How much of the message the model may be shown. See Long mail below. |
Each draft is threaded to the message it answers — Re: subject, correct In-Reply-To and References headers — so it appears inside the original conversation in your mail client rather than as a stray new message.
A draft is a first pass, not an answer
Read every draft before sending it. Even a draft grounded in your own documents (below) is only as good as the documents behind it, and an ungrounded one is written from the message alone — it has no access to your systems, your prices or your case history, and it will not know what it does not know.
Grounding drafts in your own documents
A reply written from the incoming message alone can acknowledge it, but it cannot answer it. Point a category at a Knowledge Collection and its replies are written from the documents in that collection instead — the agent asks a knowledge agent to answer the message from that collection, and uses the answer as the draft.
Grounding is chosen per category, on the same row as the folder and the description. This is what keeps retrieval precise: a message classified as support_request is answered from your support material and from nothing else, so the category verdict is what makes the lookup accurate. A category with no collection keeps writing from the message alone, so you can adopt this one category at a time.
The knowledge base layout this needs. A collection is a top-level folder in your knowledge database — ingestion creates one collection per folder automatically. So the setup is: one folder per category, holding the documents that answer that category.
support-kb/ ← knowledge database
├── support/ ← collection, for the support_request category
│ ├── troubleshooting-guide.pdf
│ └── known-issues.md
└── information/ ← collection, for the information_request category
├── price-list.pdf
└── opening-hours.mdConfigure it in three places:
| Field | Where | What it is |
|---|---|---|
| Knowledge Agent | Knowledge delegation | The agent that answers a message from a collection. Only agents that can be scoped to one are offered. |
| Knowledge Databases | Email classification | The databases your collections live in. A collection name alone does not identify one. |
| Knowledge Collection | On each category | The collection that category's replies are answered from. Leave empty for no retrieval. |
A category naming a collection that none of the configured databases holds fails the run before any mail is classified, rather than quietly answering from nothing. So does a grounded category whose Draft a Reply switch is off, which would otherwise have you looking for drafts that were never due.
Every message still gets a draft. When the lookup finds nothing that answers a message, the agent does not ask the model to write around an empty result — an ungrounded reply that reads like a grounded one is worse than an honest blank, because a reviewer skims it and sends it. Instead you get a draft carrying a fixed text you configure:
| Field | Used when |
|---|---|
| Draft When Nothing Was Found | Retrieval ran and found nothing relevant. A true statement about your knowledge base. |
| Draft When The Lookup Failed | The lookup itself broke or never answered. A statement about your deployment. |
| Knowledge Lookup Timeout | How long to wait before treating a lookup as failed. Defaults to 10 minutes. |
The two are kept apart deliberately: reporting an outage as an absence of knowledge is how a broken deployment gets read as a working one. And the reason there is always some draft is filing — a message that got no draft would be filed, unflagged and never looked at again, sitting in neither the drafted nor the untouched state.
Attachments
Turn on Read Attachments to let the drafter use what is inside the attached files, not just the message body. PDFs and images are read by the platform's document parser (with OCR); Word, PowerPoint and Excel files are converted directly. Anything else — archives, audio — is skipped.
Reading an attachment costs a parsing round trip, so three limits keep it bounded:
| Field | Description |
|---|---|
| Max Attachments Per Message | How many files are read per message, largest first. Default 3. |
| Minimum Attachment Size | Files smaller than this are skipped entirely. Default 8 KB. |
| Attachment Character Limit | How much text is kept from any single file. Default 20 000 characters. |
The size floor is there for a specific and very common case: the logo in an email signature arrives as an attachment just like a real document does. Without the floor, every ordinary business mail would pay to have its signature image parsed for nothing.
A file the parser finds no text in is still mentioned to the model — a photo, or a scan the OCR could not read. The model is told the file arrived and that no text could be read from it, so the draft can acknowledge "thank you for the photo" without inventing what the photo showed. It is never silently dropped: a sender who wrote "see attached" deserves a reply that at least notices the attachment.
The agent does not describe images
Attachment reading extracts text. A photo with no writing in it contributes its name and type, nothing more — the agent has no image understanding and will not tell you what is in a picture.
Long mail
A long forwarded thread with a 200-page PDF attached will not fit any model's input limit. Rather than fail, the agent trims — in a fixed order, so you can predict what the model saw:
- The headers and the list of attachments are always kept.
- Attachment text is dropped first, smallest file first.
- Only then is the message body shortened, at a sentence boundary, and marked as truncated.
The body is protected over the attachments because the body is where the sender's actual question is. Raise Draft Input Tokens if your model accepts more and you want less trimming.
Getting started
- Start with two or three categories, not fifteen. Broad, clearly-distinct buckets classify far more reliably than a long list of overlapping ones, and you can split them later once you see the traffic.
- Let it create the folders. Point the categories at folders that do not exist yet and let the first run create them — that way the names always match exactly.
- Watch the first runs in the event timeline. Every message shows the category chosen and the model's reason. That reason is the fastest way to find a description that needs rewording.
- Fix misfiling in the descriptions. They are the only lever there is, and they are the right one — nearly all misfiling traces back to two categories whose descriptions overlap.
- Then put it on a schedule (above), and the inbox drains itself.
- Turn on drafting last, and only for the categories that need it. Read the first few drafts before you trust the rest.
- Then ground the categories worth grounding. Load the documents that answer a category into its own collection and point the category at it. An ungrounded draft can only acknowledge a message; a grounded one can answer it.
What it does not do
- It never sends, and never deletes. Moving relocates a message; drafting writes into your Drafts folder. Nothing leaves the mailbox, and the platform has no way to send mail at all.
- It does not read attachments to classify. Classification uses the headers and the plain-text body only. Attachments can feed a drafted reply (above), but never the choice of category.
- It does not answer from a collection you did not point it at. Grounding is per category and opt-in; a category with no collection is drafted from the message and its attachments alone.
- It has no chat interface.
- It does not skip a message it cannot handle. A run is all-or-nothing: if one message fails to classify, nothing in that batch is filed and the whole batch is retried next run. That keeps a transient outage from scattering mail into the fallback folder, but it does mean one persistently unprocessable message blocks the mailbox until you move it out by hand. Watch for a run that reports an error every time with nothing filed.
Inbound mail is untrusted
Anyone can send your mailbox anything, and the body goes into the model's prompt. The agent is built so the worst case is bounded: the model chooses from your list of categories and can only ever return a position in that list, so a message containing instructions cannot invent a destination folder or make the agent do anything other than file mail. The platform's PII guard anonymises personal data at the LLM gateway. Even so, treat the folder a message landed in as a suggestion, not a verdict.
