Docs

Message Optimizer

messages.optimize rewrites one outbound draft in the sender own voice and returns it on the same call. Needs no connected data. Graded on style, and against your own customer conversations when you pass evidence: workspace.

messages.optimize takes one outbound draft and hands back a stronger version of it, in the voice of the person sending it. It scores your text, tries better versions, and returns whichever graded best, which is sometimes the one you submitted.

It needs no connected data, so it is the first result a new workspace can get. The cookbooks put it to work: one draft read end to end, a campaign from a spreadsheet, and the call in front of your send queue.

OperationRESTMCPScope
Rewrite a draftmessages.optimizePOST /messages/optimizemessages → optimizemessages:execute

A Customer agent key carries messages:execute (amdahl keys create --preset agent). A key created before the optimizer went public does not: create a new one. A Read only key is refused with 403 forbidden.

What it grades

That depends on evidence, which you choose per call, and then on whether the workspace has synced conversations:

  • evidence omitted or "off" (the default): style alone. How the message reads and how likely it is to get a reply, judged against how you write. This is the fast pass, and it is what a brand-new workspace gets either way.
  • evidence: "workspace", on a workspace with synced conversations: style, and the draft against your own customers' words. It is slower. Each grading reads about 20 quotes from the workspace's own conversations, and also scores four dimensions against them: relevant_positioning (is the offer positioned for this customer's situation rather than as a generic pitch), grounding (are its claims backed by what your customers actually said), verified_specifics (are its specific details ones your customers actually stated) and differentiation (does it make a distinct, evidence-backed point rather than boilerplate). The rewriter reads up to 8 of those quotes too, to ground the rewrite in what your customers actually say; it never quotes them or presents them as the recipient's words. The blind re-grade behind lift reads the same quotes, and lift.quotes counts them.

With evidence off, those four dimensions are not graded at all: they drop out of the score rather than counting as zero, and nothing is read from your conversations. With "workspace" on a workspace that has no conversations yet, or in an environment that does not read them, the run is graded as with "off" and still succeeds; the response's evidence field says which happened. Pass "workspace" to keep the behaviour this operation had before evidence became a per-call choice.

The ask. On a style pass the Optimizer prefers a close the reader can answer in one line, asking whether the problem is real for them, over a request for a meeting. A rewrite often replaces "Worth 15 minutes?" with a question, and a draft that ends on a meeting request can come back kept with a clarify_ask suggestion. If the meeting ask is what you want, put it in rules, inside context: a rewrite that breaks a rule is never returned, and a kept draft gets no clarify_ask suggestion.

With connected conversations it also lists the claims in your draft that nothing in those conversations backs, in unsupported_claims, and by default keeps them in the message rather than cutting them. Without connected conversations claims are not checked, and unsupported_claims is null.

The eval is still the full claim-by-claim check, with the quotes behind each verdict. If you run both on the same message, keep the two verdicts separate in whatever you hand back, and run the eval first: get the claims grounded, then put the grounded version into the sender's voice.

Monthly cap

Each call made with an API key or an external OAuth token that returns a result (ok: true) counts toward a monthly cap of 1,000 per workspace. A call that failed does not count. Past it, the call returns 429 quota_exceeded with the family, limit and usage in details, and the optimizer is not run. The cap resets at the start of the next month; ask Amdahl support to raise it. Calls from the Amdahl console do not count.

Request

json
{
  "message": "Hi Jordan - saw you shipped SSO last month. Most teams your size end up rebuilding provisioning right after. Worth 15 minutes?",
  "channel": "email",
  "on_unsupported": "flag",
  "context": {
    "rules": ["Never mention pricing.", "Keep it under 80 words."],
    "voice_examples": [
      "Hey Priya - congrats on the Series B. Are you still running onboarding by hand? Happy to share what worked for a team your size."
    ],
    "facts": [
      "Met Jordan at the GTM dinner on Sep 24; he said his SDRs rewrite every AI draft."
    ]
  }
}
FieldRequiredNotes
messageyesThe draft, verbatim. 1 to 10,000 characters: this is built for a message, not a document.
channelnoemail or linkedin. Inferred from the draft when omitted. It sets the length and format the message is judged against.
evidencenoworkspace or off, default off. workspace also grades the draft against your workspace's synced customer conversations (the four dimensions above, and slower); off is a fast style pass. Any other value is refused.
contextnoWhat the sender wants the rewrite to respect, and what they know: an object holding any of rules, voice_examples and facts, as below. Any other key is refused.
on_unsupportednoWhat to do with a claim in the draft that nothing in your conversations backs, so it acts only with evidence: "workspace": flag (the default) keeps it and lists it in unsupported_claims, remove lets the rewrite soften or cut it. See Claims nothing backs.
include_triesnotrue to also get tries: every version the run scored, your draft first, so you can show how it moved toward the one that came back. Default false, which leaves the response exactly as below without it. See Reading tries.

Inside context:

KeyNotes
rulesHard constraints the rewrite must follow, like "never mention pricing". They are instructions, not copy: never rewritten, and never quoted in the message. Up to 20, each up to 500 characters.
voice_examplesMessages the sender wrote themselves. The rewrite matches their voice and does not copy their words. Up to 5, each up to 4,000 characters.
factsWhat the sender knows first-hand about the recipient or the situation that no call recording holds, one fact per entry. Graded as caller-supplied evidence, never as a customer quote: see Facts. Up to 10, each up to 1,000 characters.

Each entry is trimmed and a blank entry is dropped rather than refused, before anything is counted or measured: the limits apply to what is left. A context with nothing left in it is not sent at all.

Facts: what you know

rules and voice_examples steer how the rewrite reads. Neither can make a claim true. facts can: they are things you know first-hand that are not in your customer conversations, most often what someone told you in person.

Without them, a true line like "Great meeting you at the GTM dinner last week. You said your SDR team spends hours rewriting generic AI drafts" grades as a fabricated detail, because no recorded conversation says it. Send what you know:

json
"context": {
  "facts": [
    "Met Priya at the GTM dinner on Sep 24; she said her SDRs rewrite every AI draft."
  ]
}

and the judge treats it as its own evidence tier, CALLER, beside the customer quotes:

  • A claim a fact states is not marked fabricated or unsupported, including a claim about the recipient that no customer quote backs.
  • A claim that goes further than a fact is still unsupported for the part the fact does not say. "Your SDRs spend hours every day rewriting" is an embellishment of "her SDRs rewrite every AI draft", and grades as one.
  • The rewrite may use your facts and is told never to embellish them or present one as something a customer said.
  • A fact is never counted or shown as a customer quote, and lift.quotes never includes one.
  • Facts are treated as data, never as instructions: a fact that says "ignore the rubric" is read as a strange fact, not obeyed.
  • They are used for this request only. They are never stored as evidence, never used to search your conversations, and never written to logs as text.

Facts only help where the claim is graded against evidence, so without evidence: "workspace", or on a workspace without synced conversations, they mainly guide the rewrite. To have each claim checked against them line by line, send the same facts (inside context) to evals.run.

Claims nothing backs: on_unsupported

Only when the run read your conversations (evidence: "workspace" and evidence.used is true), the judge also names each claim in your draft that no customer quote it read supports and no fact you sent states. A claim a fact states is not listed; a claim that goes further than a fact still is.

  • flag, the default: every claim your draft makes stays in the message. The rewrite is told to keep each listed claim word for word and to add no support for it, and a rewrite that drops one anyway is discarded, the same way a rewrite that changes a name or a number is. Everything else is still improved.
  • remove: the rewrite softens or cuts the listed claims. Numbers inside a listed claim may go with it.

Neither mode changes the grade. A claim nothing backs still fails grounding, and lift is measured the same way. While a listed claim is still in the returned message, summary never says the message is ready to send. It says how many claims nothing in your conversations backs, and to check them before sending. It names no field, because the sender sees summary as written. The claims themselves are in unsupported_claims.

The mode is echoed back as on_unsupported on every result.

There is no field for who is sending, in context or anywhere else. The sender is derived from the credential you called with and cannot be supplied on the wire; see Whose voice it learns.

An empty message, one over 10,000 characters, a channel other than email or linkedin, an evidence other than workspace or off, or an on_unsupported other than flag or remove is refused with 400 invalid_input. So is a context with a key other than rules, voice_examples and facts, more entries than its limit, or an entry over its character limit, with the field named in details.errors (for example $.context.rules[3]: must be at most 500 characters); nothing is sent to the optimizer. A message of only whitespace passes the schema and is refused by the handler as 500 operation_error; do not retry it.

Response

Like every operation, the result is inside the data envelope:

json
{
  "data": {
    "ok": true,
    "message": "Hi Chris, thanks for connecting. I noticed you lead RevOps at Brightpath. We work with RevOps teams to streamline their tech stack and drive pipeline with AI. Curious if you're dealing with either of those right now?",
    "summary": "Rewrote it in your own style to improve how clear the ask is and how human it sounds. This was a style pass only. Its claims were not checked against your workspace's conversations.",
    "unchanged": false,
    "run_id": "ad5a34f6-...",
    "extraction": { "receiver_name": "Chris", "company": "Brightpath", "channel": "linkedin" },
    "lift": null,
    "evidence": { "requested": "off", "used": false, "reason": "not_requested" },
    "on_unsupported": "flag",
    "unsupported_claims": null,
    "notes": [
      {
        "code": "rewrite_checked_yes_no",
        "audience": "sender",
        "says": "Re-checked independently (a different model from the one that picked the rewrite) on yes/no questions, each asked twice of the draft and of the rewrite: it did not consistently lose a clear ask, a human tone or concision, and it was not found to add a claim the draft did not make. This is a pass/fail check, not a before-and-after score."
      },
      {
        "code": "style_pass_only",
        "audience": "sender",
        "says": "This was a style pass only. The draft was graded on how it reads, and its claims were not checked against the workspace's conversations. A style pass does not mean that the draft passed or that it is ready to send."
      },
      {
        "code": "evidence_available",
        "audience": "developer",
        "says": "This request did not ask for evidence, so the draft was graded on style alone. This deployment can read the workspace's conversations: to also grade the draft against them, send evidence: \"workspace\"."
      }
    ]
  }
}

That is a real response to a LinkedIn draft sent with no evidence, with its notes as they read now and one developer note left out: a style pass. Its claims were not checked (unsupported_claims: null), and lift is null, because a style pass measures no before-and-after score; a different model re-checked the rewrite on yes/no questions instead, which is what the first note says. With evidence: "workspace" on a workspace with synced conversations, evidence.used is true, unsupported_claims is a list ([] when nothing was found), and a rewrite that reaches the blind re-grade carries a lift measured against about 20 of your customers' quotes. Optimize a cold email reads a full response, and a kept draft, field by field.

FieldNotes
okfalse means no rewrite was produced. Read reason (or error.code for the monthly cap) and stop.
messageThe version to use: a rewrite, or your own text, unchanged. Use it as returned; do not polish it with another model, which undoes the checks that kept your details exact.
summaryOne line naming what the rewrite aimed at. It says the rewrite scored higher than your draft only when lift.returned is rewrite.
unchangedtrue when nothing beat what you wrote, or when the draft was too weak to rewrite (see ask). Do not present that as an improvement; it is a real and frequent outcome.
extractionWhat the optimizer read out of the draft: receiver_name, company, signal (the reason given for writing, such as a launch or a move) and channel. A key it could not find is absent rather than guessed.
liftThe only before-and-after in the response, so the only score to quote as one. A different model from the one the rewrite was optimized against re-grades your draft and the rewrite blind, one call each and neither told which is the draft, against the same evidence. Measured only when the run read your conversations and a rewrite reached that re-grade; null otherwise, which includes every style pass. See Reading lift.
evidenceWhat happened about customer evidence on this run: { "requested", "used", "reason" }. See Reading evidence. null only from an optimizer that predates the field.
notesFacts about this run, each { "code", "audience", "says" }. Branch on code, never on the wording of says. audience says who says is written for. A sender note is about the draft or the message that came back, in plain words: show it to the person who wrote the draft, as written. A developer note says how to read this response, such as which grader decided or what to send next, and can name fields and parameters: act on it or log it, and never show it to the sender. A code has the same audience on every run, so filter on the field rather than on a list of codes, and treat any audience other than sender as not for the sender. New codes appear over time. Omitted when there are none.
vetoed_round, veto_reasonPresent only when the optimizer refused its best rewrite and returned your draft, and reports that it did (not every deployment reports it yet; unchanged: true says your draft came back either way). vetoed_round is the rewrite round it refused. veto_reason is not_better or breaks_rules. not_better means the independent re-check did not find the rewrite better: on a run that read your conversations it did not score higher than your draft on the blind re-grade; on a style pass it was consistently worse on one of the yes/no questions, for example less concise, or it added a claim the draft did not make. breaks_rules means it broke one of your rules when re-checked.
on_unsupportedThe mode this run used, flag or remove, so a caller that sent none can see the default it got.
unsupported_claimsClaims in your draft that nothing in your conversations backs, each with what happened to it in message. [] means they were checked and none was found; null means they were not checked on this run, which is always the case without evidence: "workspace" and whenever evidence.used is false. See Reading unsupported_claims.
kept_suggestionsPresent only when your draft came back unchanged and the optimizer had edits to offer: what you can change yourself, each { "kind", "span"?, "says" }. Omitted on a returned rewrite and when there is nothing to offer. See Reading kept_suggestions.
askPresent only when your draft was too weak to rewrite: the questions only its sender can answer, at most three, each { "asks", "field"? }. Nothing was rewritten and unchanged is true. Omitted otherwise. See Reading ask.
triesPresent only when you sent include_tries: true: every version the run scored, in order, each { "round", "message", "score", "human_tone", "aimed_at", "returned" }. null when the run's trace could not be read; the rewrite is unaffected. See Reading tries.
contextPresent only when you sent a context: how many rules and voice examples the optimizer received once blank entries were dropped, as { "rules": n, "voice_examples": n }, plus "facts": n when you sent any, so you can confirm they arrived. Counts only, never the text.

Reading lift

FieldNotes
before, after, deltaThe re-grade's 1 to 5 means for your draft and for the rewrite, and the difference.
returnedrewrite, or draft when the rewrite did not score higher than your draft on the re-grade (a tie counts as not higher) or broke one of your rules when re-checked. With draft, your draft came back: message is your own text and unchanged is true.
dimensions, quotesWhat was measured: the rubric dimensions both gradings answered, which include the four evidence dimensions, and how many of your customers' quotes they read, typically around 20. Read both before repeating the number: a lift is a claim about those dimensions and those quotes, no more.
method, grader, grader_version, rewrite_roundHow it was measured, by which model, and which rewrite round was compared. method is always blind_regrade_pinned. Compare two lifts only when their grader_version matches.

lift is null when no before-and-after was measured, and then none is claimed. That is always the case on a style pass (evidence off, or requested and not used): there a returned rewrite is re-checked by a different model on yes/no questions instead, each asked twice of both versions, and the rewrite_checked_yes_no note says what it found. On a run that read your conversations, lift is also null when no rewrite reached the re-grade, the re-grade failed, or its two gradings did not answer the same dimensions. If unchanged is false, lift is null and no note says the rewrite was re-checked, the rewrite was chosen by the optimizer's own judge alone.

Reading evidence

FieldNotes
requestedThe mode the run was asked for: workspace or off, with the default applied when you sent none.
usedtrue only when your conversations were read and held quotes to grade against. Say a draft was checked against your customers only when this is true.
reasonPresent when used is false. not_requested: you did not ask. evidence_unavailable: this environment does not read customer conversations, so the run was graded on style alone; notes carries the same code as a developer note. corpus_empty: the workspace has no conversations to read yet. evidence_read_failed: the read failed, so the four evidence dimensions were not graded; a retry may succeed. None of these is an error: the rewrite came back.

Reading unsupported_claims

json
"unsupported_claims": [
  {
    "span": "we cut onboarding time 40% for teams like yours",
    "kind": "about_recipient",
    "why": "No quote shown names an onboarding figure.",
    "dimension": "verified_specifics",
    "status": "kept",
    "fix": [
      { "action": "add_fact", "how": "if it is true, re-run with context.facts: [\"we cut onboarding time 40% for teams like yours\"]" },
      { "action": "add_evidence", "how": "cite a source, or run evals.run with include_external" },
      { "action": "rephrase" }
    ]
  }
]
FieldNotes
spanThe claim, copied exactly from the draft you sent.
kindabout_recipient (it talks about the person or company you are writing to) or product_or_outcome.
whyOne sentence on why nothing backs it.
dimensiongrounding, or verified_specifics when the claim carries a number.
statusWhat happened to it in message: kept, removed, or rewrite_rejected (it is still there because a rewrite that dropped it was not returned).
fixWhat can be done about it: add_fact, add_evidence, and rephrase, which carries a suggestion when the grader proposed a wording.

At most 10 claims are listed.

If you are an agent relaying this to a person, do not call the message ready while a claim is still in it. Show the user each claim and ask:

  1. Is it true, and do you know it first-hand? Re-run with it in facts inside context, in the user's own words. A fact that states the claim clears it; one that says less does not.
  2. Does it have a source? Add the evidence, or check it with evals.run and include_external.
  3. Neither? Offer to re-run with "on_unsupported": "remove", or let the user reword it.

Never re-run with remove without asking: the claim may be the one thing in the draft the user knows and the conversations do not.

Reading kept_suggestions

When every rewrite was refused, or none scored higher than your draft, your draft comes back with unchanged: true. kept_suggestions is what you can change yourself instead, most useful first, at most 5:

json
"kept_suggestions": [
  {
    "kind": "back_up_or_cut_claim",
    "span": "we cut onboarding time 40% for teams like yours",
    "says": "Cut this claim or back it up with something you can point to: \"we cut onboarding time 40% for teams like yours\""
  },
  {
    "kind": "add_recipient_specific",
    "dimension": "relevant_positioning",
    "why": "Nothing in the draft is about this recipient.",
    "says": "Add one specific fact you actually know about the recipient (something they said, did or published) and tie the offer to it. Leave it out rather than guess."
  },
  {
    "kind": "shorten",
    "says": "Shorten to about 91 words (under 600 characters); it is 160 words (1050 characters) now."
  }
]
FieldNotes
kindWhat sort of edit, to branch on: back_up_or_cut_claim, add_recipient_specific, make_distinct_point, clarify_ask, needs_real_proof, shorten, replace_generic_line, rewrite_sentence_shape or replace_dash. New kinds may be added, so treat an unknown one like any other row.
spanPresent when the edit is about one part of the draft: that text, copied exactly from what you sent.
saysThe edit, in fixed wording that is safe to repeat to a person as written.
dimensionPresent when the edit comes from a failed rubric line: which one.
whyPresent with dimension: the grader's reasoning. Context for the edit, never wording to put in the message.

The list costs no extra model call: every row comes from something the run already measured. Nothing in it invents a fact. Only span and the counts in shorten come from your draft, and a phrase a refused rewrite made up is never repeated. It is a different field from the eval's own suggestions.

Your rules come first. When you send rules inside context, a suggestion about a part of the message that a rule names is left out, and the next one takes its place. The parts are the ask (clarify_ask), the recipient (add_recipient_specific), the pitch (make_distinct_point), customer proof (needs_real_proof), the length (shorten) and dashes (replace_dash). A suggestion with a span is left out when a rule repeats four or more words in a row from it. So "End by asking for a 15-minute call." removes clarify_ask, and "Never mention pricing." removes nothing. The check reads the words of each rule, so it cannot see a rule that decides a part without naming it. If a suggestion still goes against one of your rules, the rule wins.

Reading ask

Some drafts are too weak to rewrite. A rewrite at that score could only make up the missing details, so the optimizer rewrites nothing and returns your draft with unchanged: true. ask lists what it needs from the sender, at most three questions:

json
"ask": [
  { "asks": "Who is this for? Which account, and which person there?" },
  { "asks": "What did you see or hear that prompted this? A call, a post, a launch, a number." },
  { "asks": "What specifically changes for them if they say yes?" }
]
FieldNotes
asksThe question, for the person who wrote the draft.
fieldPresent only when the optimizer's grader named an input it needed, for example account. Most questions carry none, as above. Treat it as an open string: new inputs may be added.

Put the questions to the sender as written. Then add their answers to the draft and run it again. The asked-for-input note, which has the sender audience, says why nothing was rewritten. The response may also carry kept_suggestions. Show the questions first.

Reading tries

Send include_tries: true to see the path, not just the result. Round 0 is your draft. Each later round is one rewrite, aimed at every dimension the round before it scored below the top:

json
"tries": [
  { "round": 0, "message": "Hi Dana, one national campaign, 380 dealers...", "score": 3.8, "human_tone": 4.7, "aimed_at": [], "returned": false },
  { "round": 1, "message": "Hi Dana, I know the challenge...", "score": 3.4, "human_tone": 3.84, "aimed_at": ["cta_clarity", "length_discipline"], "returned": false },
  { "round": 2, "message": "Hi Dana, 380 dealers all needing their own version...", "score": 4.2, "human_tone": 4.86, "aimed_at": ["human_tone"], "returned": true }
]
FieldNotes
round0 is your draft; 1 and up are rewrites, in order.
messageThe body that round produced. A sign-off is set aside before any rewrite and put back only on the returned message, so a try never shows it.
scoreThe Optimizer's own score for that version, 1 to 5. null when it could not score that round.
human_toneHow much that version reads like a person wrote it, 1 to 5. null when the round was not scored.
aimed_atThe dimensions this round was told to improve, from the scores of the round before it, using the names in What it grades plus human_tone, length_discipline, user_preference and cta_clarity. Empty on round 0.
returnedtrue on the one version that came back as message. When your draft came back, that is round 0, even if a later round scored higher and was refused.

Read score as the path, not as proof. It comes from the same grader every rewrite was written against, and that grader picked the winner, so a rise from round 0 to the returned round overstates the gain. Scores also go down between rounds; that is the search working, not a fault. lift is still the only before-and-after to quote. Rewrites the Optimizer discarded before scoring, for example for changing a name or a number, are not listed; notes says when that happened.

When ok is false

reasonWhat happened
optimizer_unconfiguredThe optimizer does not run in this environment. Nothing was attempted.
optimizer_timeoutIt did not answer in time. Your draft is untouched; a retry costs a fresh run.
optimizer_errorIt answered with an error. Your draft is untouched.

A call refused by the monthly cap carries ok: false, success: false and error.code: "quota_exceeded" instead of a reason, and returns 429 over REST.

The failure branch carries no message at all, so there is nothing to mistake for a rewritten draft.

Why it can return your own words

The loop scores your submitted text as round 0, then tries rewrites and keeps whichever version scored highest. Rewrites that score worse than what they replaced are an ordinary outcome, so returning the last attempt would routinely hand back something its own judge rates below your draft. Returning the best round prevents that, and unchanged: true tells you it happened.

The rewrite the loop picks then faces an independent re-check by a different model. On a run that read your conversations that is the blind re-grade behind lift: the rewrite comes back only if the re-grade scores it higher than your draft and finds it breaks none of your rules, and lift.returned says which came back. On a style pass the re-check asks yes/no questions of both versions, twice each, and refuses the rewrite only when it is consistently worse on one of them. Either way, a refused rewrite means your draft comes back with unchanged: true.

A rewrite is also thrown away before any of that if it drops or changes a name, number, date, link or merge field such as {{first_name}}, or adds a figure the draft did not have; the rewrite-rejected-for-entity-loss note names what it tried to change.

A round the judge could not score is excluded from the comparison rather than treated as a zero.

Whose voice it learns

The optimizer keeps a per-sender ledger of tells: small, individually checkable facts about how you write, like "opens with a question". It learns them from the edits you make to what it gives back.

  • The ledger is per workspace and per sender. The identity is composed server-side from your workspace and your user, and a request cannot name it.
  • It has nothing to go on at first. A new sender gets rewrites scored on the base rubric alone.

Latency

An optimize usually takes under 30 seconds: it runs the full rewrite search and grades every candidate before it answers. With evidence: "workspace" each grading also reads your conversations, so it takes longer. A call can run for up to 170 seconds before it gives up with optimizer_timeout, and it returns on the same call, so set your client timeout to at least 180 seconds. The operation declares its expected latency on the operation catalog, so a client can budget for it.