AI
Classify replies to the mail you send, handle detected opt-outs, and draft email text with AI.
Workwae has two optional AI features: classifying replies to the mail you send, and drafting email text for a person to edit. For connecting AI agents to mailboxes, see the MCP server.
What is sent to the model
Reply classification is off until your workspace turns it on; drafting runs only when someone asks for a draft. The text a feature needs goes to Gatewae, Workwae's AI gateway, which forwards it to a language model provider.
| Feature | Sent |
|---|---|
| Reply classification | The reply's subject, the first 2,000 characters of its own text, and whether it carries standard auto-reply headers. An out-of-office reply is read once more, with the date it was sent, for its return date. |
| Drafting | The instructions, the draft so far and, when answering a message, that message's sender, subject and the start of its own text. |
No training
Workwae uses only providers that do not train on the data it sends, and never trains a model on it.
Reply classification
Turn on aiReplyClassification in the workspace settings:
curl -X PATCH "https://api.workwae.com/v1/settings" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{ "aiReplyClassification": true, "optOutHandling": "review" }'What gets classified:
- New inbound mail in the inbox or junk that answers a message you sent through Workwae, matched by thread or by its
In-Reply-ToandReferencesheaders. - It is classified about a second after it is indexed.
- Mail more than seven days old when indexed is not classified.
- Turning the setting off drops queued replies unread.
The result
Each result is stored as the message's classification and sent as the message.classified event.
| Field | Meaning |
|---|---|
kind | opt_out, auto_reply, bounce or human_reply. probabilities holds each kind's probability, 0 to 1. |
optOutProbability | The probability the reply asks to stop receiving email, 0 to 1. |
autoReplyCategory | On auto-replies only: out_of_office, left_company, address_changed, ticket_acknowledgment, challenge_response or other; null otherwise. |
returnDate | For out-of-office replies, resolved against the day the reply was sent. null when the date is not real, falls before that day, or is more than a year after it. |
alternateContacts | Up to five addresses a left_company or address_changed reply names, read from its text as written, never from the model. |
outboxId | The send the reply answers. |
model, classifiedAt | The model that classified it, and when. |
React to it in your webhook handler:
import { parseWebhook } from "@workwae/sdk";
const event = await parseWebhook(secret, request.headers, await request.text());
if (event.known && event.type === "message.classified") {
const { classification: c } = event.data;
if (c.autoReplyCategory === "out_of_office" && c.returnDate) {
await pause(c.outboxId, c.returnDate);
}
if (c.kind === "human_reply") await stopFollowUps(c.outboxId);
}Opt-outs and the review queue
optOutHandling decides what a detected opt-out from the send's own recipient does, at an optOutProbability of 0.9 or more:
| Value | Effect |
|---|---|
off | Ignores it. |
review (default) | Holds the address in the review queue. |
suppress | Suppresses the address at once. |
- Replies saying the person left or changed address always wait for review unless the setting is
off; they never suppress at once. - Unsubscribe links always suppress, whatever the setting.
Work through the queue by approving or dismissing each review:
const reviews = workwae.suppressionReviews;
const { items } = await reviews.list({});
for (const review of items) {
const ok = await askOwner(review.address, review.source);
const input = { reviewId: review.id };
await (ok ? reviews.approve(input) : reviews.dismiss(input));
}Suppression lists covers each mode in detail.
Drafting
POST /v1/ai/draft writes an email body for a person to edit and returns { text }. Nothing is sent or saved. It needs an API key with the write scope and the AI module in the tenant's plan (plan_module_unavailable otherwise).
curl -X POST "https://api.workwae.com/v1/ai/draft" \
-H "Authorization: Bearer $WORKWAE_API_KEY" \
-H "content-type: application/json" \
-d '{
"instructions": "Shorter, and offer a call on Thursday",
"context": { "accountId": "'"$ACCOUNT_ID"'", "replyToMessageId": "'"$MESSAGE_ID"'" },
"format": "markdown"
}'| Field | Meaning |
|---|---|
instructions | What to write, in the user's words; up to 2,000 characters. |
format | text or markdown. |
context.replyToMessageId | Optional. The message being answered; needs context.accountId, the account holding it. |
context.currentDraft | Optional. The draft so far, to rewrite or continue; up to 20,000 characters. |
Limits and errors
| Limit or reason | Meaning |
|---|---|
| Daily classification cap | Each workspace has a daily cap on classified replies; past it, replies wait for the next UTC day. |
rate_limited | Drafts have a per-minute limit per workspace; past it, 429 with Retry-After. |
ai_refused | The model declines the instructions. |
ai_unavailable | The AI gateway fails or does not answer in time. |
ai_suspended | The AI service refuses Workwae's requests for now; try again later. |
ai_disabled | AI drafting is not configured on this deployment. |
unknown_account, unknown_message | context names an account the tenant does not have, or a message the account does not have. |
- If the gateway fails, classification retries a reply later, backing off to an hour, for up to seven days.
- Every answered call counts toward
aiCallsin your daily usage (GET /v1/usage; see Rate limits and usage).
Every route and its full list of errors is in the AI reference.