Four kinds of reply
Each reply is an opt_out request, an
auto_reply, a bounce or a
human_reply, with a probability for each kind.
Two optional features: classifying replies to the mail you send, and drafting email text for a person to edit. Both are off until you turn them on, and send only the text a feature needs to our AI gateway.
// A reply to one of your sends, as your webhook gets it
{
"type": "message.classified",
"data": {
"classification": {
"kind": "auto_reply",
"autoReplyCategory": "left_company",
"optOutProbability": 0.02,
"returnDate": null,
"alternateContacts": ["sam.lee@example.com"],
"outboxId": "7b9d1f3a-5c7e-4a9b-8d1f-3a5c7e9b1d3f"
}
}
}
Classification runs on replies to your own sends; drafting runs only when someone asks for a draft. Nothing else calls the AI gateway.
Each reply is an opt_out request, an
auto_reply, a bounce or a
human_reply, with a probability for each kind.
Out of office, left the company, address changed, ticket acknowledgment, challenge-response, or other. Auto-reply headers are a hint to the classifier, never the verdict.
An out-of-office reply's return date is read from its text, resolved against the day it was sent in the sender's time zone. A date that isn't real, falls before the day it was sent, or is more than a year after it is left empty.
When your recipient left the company or changed address, up to five addresses the
reply names are kept as alternateContacts, read from the
text itself, never from the model.
A likely opt-out from your own recipient is ignored, held for review, or suppressed, per your opt-out setting. A person approves or dismisses each held review.
A reply is tied to the outbox entry it answers through its thread or its
In-Reply-To and References
headers, and carries that entry's outboxId.
Write an email body from instructions, in plain text or Markdown, optionally in reply to a message or reworking a draft. Nothing is sent or saved.
Every answered call, classification or draft, counts toward
aiCalls in your daily usage, next to sends and API
calls.
Reply classification waits for your workspace to turn it on, since reply text goes to a third party. Drafting runs only when someone asks for a draft.
Classification runs after a reply is indexed, takes about a second, and is announced
as message.classified.
New inbound mail in the inbox or junk that answers a message you sent through workwae is queued. Mail more than seven days old when it's indexed, such as from a widened sync window, is history and isn't classified.
One decision gives the kind, the opt-out probability and, for an auto-reply, its category. An out-of-office reply is read once more for its return date.
The result is stored as the message's classification.
Opt-outs and recipients who moved on go to your opt-out setting.
Your workspace's optOutHandling setting decides,
review by default. It applies to replies from the send's
own recipient; an opt-out counts at a probability of 0.9 or more.
| Setting | Reply asking to opt out | Left the company or changed address |
|---|---|---|
off |
Ignored. The reply is still classified. | Ignored. |
review |
Waits in the review queue with the message it came from and how sure the detection was. | Waits for review on the global list (source inactive), with the alternate contacts. |
suppress |
Suppressed at once. Where the send went to several people, it waits for review unless the receiving server shows DMARC, or an aligned SPF or DKIM, passing for the sender. | Waits for review. Never suppressed at once. |
When a feature is on, workwae sends only the text it needs to Gatewae, the AI gateway we use, which forwards it to a language model provider. The results are returned to your workspace.
| Feature | Sent | Not 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's subject and text are sent once more, with the date the reply was sent, to read its return date. | Quoted history, the rest of the thread, attachments, and anything about the account or the recipient. |
| Draft writing | The person's instructions, their draft so far and, when answering a message, that message's sender, subject and first 4,000 characters. | Anything else in the mailbox. Drafting runs only when a person asks for a draft. |
We use only providers that do not use the data we send to train their models, and the data is never used by us to train any model.
The same list is in the Privacy Policy, and Gatewae is listed among our subprocessors.
If the gateway or the provider fails, the reply is retried later, backing off to an hour, for up to seven days. Classification catches up on its own.
Each workspace has a daily cap on classified replies; past it, replies wait for the
next UTC day. Drafts have a per-minute limit, answered with
rate_limited and Retry-After.
Switch on classification for your workspace, pick how opt-outs are handled, and let a person decide on the held ones.
await workwae.settings.update({
aiReplyClassification: true,
optOutHandling: "review",
});
// Opt-outs and recipients who moved on, waiting for a person
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));
}
Pause a sequence until someone is back, suggest the new contact when someone left, and stop following up once a person answers.
parseWebhook checks the signature
and narrows the event on its type.
outboxId ties back to the send that got the reply, and
the event carries the message and its mailboxes.
autoReplyCategory is null for every other kind.
import { parseWebhook } from "@workwae/sdk";
const body = await request.text();
const event = await parseWebhook(secret, request.headers, body);
if (event.known && event.type === "message.classified") {
const { message, classification: c } = event.data;
switch (c.autoReplyCategory) {
case "out_of_office":
if (c.returnDate) await pause(c.outboxId, c.returnDate);
break;
case "left_company":
case "address_changed":
await suggest(message.threadId, c.alternateContacts);
break;
}
if (c.kind === "human_reply") await stopFollowUps(c.outboxId);
}
POST /v1/ai/draft writes an email body from instructions
and returns { text }. The same endpoint powers "Draft
with AI" in the console's compose and template editor.
accountId with replyToMessageId) and the draft so far
(currentDraft) to rewrite or continue.
text or markdown. Needs an API key
with the write scope.
ai_refused when the model declines,
ai_unavailable when the gateway fails or times out.
import { isApiError } from "@workwae/sdk";
try {
const { text } = await workwae.ai.draft({
instructions: "Shorter, and offer a call on Thursday",
context: {
accountId,
replyToMessageId: messageId,
currentDraft,
},
format: "markdown",
});
editor.setValue(text);
} catch (error) {
if (!isApiError(error, "ai_refused")) throw error;
showHint("Try rewording the request");
}
// AI calls per day, next to the rest of your usage
const { days } = await workwae.usage.get({ from: "2026-10-01" });
const aiCalls = days.map((d) => [d.day, d.aiCalls]);
Signed webhooks and change-feed entries, like every other event.
| Event | When |
|---|---|
message. |
A reply to one of your sends was classified. Carries the message, its mailboxes and the classification. |
suppression. |
A detected opt-out was suppressed, at once or on approval (source reply), or an approved review of a recipient who moved on put the address on the global list (source inactive). |
No. Reply classification is off until your workspace turns it on, and drafting runs only when someone asks for a draft. Nothing else calls the gateway.
No. We use only providers that do not use the data we send to train their models, and the data is never used by us to train any model.
New mail in the inbox or junk that answers a message your workspace sent through
workwae, matched by thread or by the Message-IDs its
In-Reply-To and References
name. Other mail is never classified.
Yes. With optOutHandling set to
off, replies are still classified and announced, and
detected opt-outs are ignored. Unsubscribe links still suppress.
No. The endpoint returns text for a person to edit. Saving it as a draft or sending it is a separate call you make.
Replies are retried later, backing off to an hour, until they are seven days old, so
an outage delays classification rather than losing replies. A draft request answers
ai_unavailable.
Give an agent new mail as it arrives, full threads, and drafts a person approves.
Log every rep's email to the deal, and know which replies are out-of-office notices.
The sends, outbox and suppression lists that reply classification builds on.
Signed, ordered and replayable delivery for message.classified and every other event.
workwae is in early access. Tell us what you are building, and read the docs in the meantime.