Grade a message
messages.optimize with mode grade grades one draft with the independent judge on five rubric lines and writes nothing. It returns a 1 to 5 score with passed and total beside it, a verdict and reason for each line, and what the grade can and cannot tell you.
messages.optimize with mode: "grade" grades one draft and writes nothing. It is the same call as writing a message, with one more field. The draft goes in message, and the grade comes back on the same call.
The grade measures the message against your evidence and this rubric; it does not predict replies.
| Operation | REST | MCP | Scope | |
|---|---|---|---|---|
| Grade a message | messages.optimize | POST /messages/optimize | optimize_message with mode: "grade" | messages:execute |
It needs the editor role or above. To grade a reusable prompt, by the emails it writes, see Improve a prompt.
Who grades
A grade comes from Amdahl's final judge (Claude Sonnet 5.5 with thinking off). This is the independent judge that also re-grades a draft and its rewrite for lift. It is never the judge inside the rewrite loop that picks which version comes back. The result names the judge in grade.judge.
The five lines
The grade is five pass or fail lines:
| Line | What it asks |
|---|---|
relevant_positioning | Is the offer positioned for this customer's situation, rather than as a generic pitch? |
grounding | Are the message's claims backed by what your customers said? |
verified_specifics | Are its specific details ones your customers stated? |
differentiation | Does it make a distinct, evidence-backed point rather than boilerplate? |
cta_clarity | Is the ask clear? |
The first four read your customers' own words. They are graded only when the call sends evidence: "workspace" and the workspace has synced conversations. Otherwise those four lines abstain, and only cta_clarity is graded. A line that abstains is not a fail: see Unknown is never zero.
Request
{
"mode": "grade",
"message": "Hi Jordan, saw Corvid shipped SSO last month. Teams that add SSO usually find provisioning is the next manual job. We automate it; Helix cut provisioning tickets by 70%. Worth a 15-minute call next week?",
"channel": "email",
"evidence": "workspace",
"context": {
"facts": ["Helix, a 300-person health tech company, cut provisioning tickets by 70% with us."]
}
}| Field | Required | Notes |
|---|---|---|
mode | yes, for a grade | grade. The default, improve, writes a message. |
message | yes | The draft to grade, verbatim, up to 10,000 characters. A grade without a draft is refused. |
evidence | no | workspace grades the four evidence lines against your synced conversations. off (the default) grades cta_clarity alone. |
context | no | Your facts count as evidence you supplied: a fact backs only what it states. rules and the constraints are read as they are on a write. |
channel | no | email or linkedin. |
recipient, recipient_notes, message_type | no | Beta, as on a write. See Recipient fields. |
Two fields that only make sense for a write are refused with 400 invalid_input, so a result never reads as though they applied: include_tries: true (a grade writes no versions) and on_missing_input: "ask" (a grade asks no questions).
Over MCP, call optimize_message with the same fields. Neither console.amdahl.ai/try nor the batch lane takes mode.
Response
{
"data": {
"ok": true,
"outcome": "graded",
"rewrite": null,
"optimize_id": "opt_7f3c2a",
"run_id": "ad5a34f6-...",
"summary": "Graded against your workspace's conversations.",
"lift": null,
"evidence": { "requested": "workspace", "used": true, "reason": null },
"unsupported_claims": [],
"grade": {
"score": 4.2,
"passed": 4,
"total": 5,
"bar": 4.2,
"passes_bar": true,
"judge": { "model": "claude-sonnet-5-5", "judge_version": "v7", "role": "final" },
"lines": [
{ "id": "relevant_positioning", "pass": true, "applicable": true, "reason": "..." },
{ "id": "grounding", "pass": true, "applicable": true, "reason": "..." },
{ "id": "verified_specifics", "pass": true, "applicable": true, "reason": "..." },
{ "id": "differentiation", "pass": false, "applicable": true, "reason": "..." },
{ "id": "cta_clarity", "pass": true, "applicable": true, "reason": "..." }
],
"not_applicable_reason": null
},
"disclosures": {
"single_run": true,
"not_reply_prediction": true,
"repeat_spread": {
"model": "claude-sonnet-5-5",
"moved_past": 0.2,
"share": 0.15,
"measured": "..."
}
}
}
}A grade has no message key and no unchanged key: the draft is yours, and nothing was written. Read outcome first. graded is the only outcome a grade has.
| Field | Notes |
|---|---|
outcome | graded. |
grade.score | 1 to 5, or null when no line could be graded. It is 1 + 4 × passed / total, so it is never 0. |
grade.passed, grade.total | The lines that passed, out of the lines that were graded. They ride beside the score, because the score is that fraction. |
grade.bar, grade.passes_bar | The bar is 4.2. passes_bar is null when score is null. |
grade.lines[] | One entry per line: id, pass (true, false, or null when the line abstained), applicable, reason, and not_applicable_reason on a line that abstained. |
grade.judge | The judge's model, judge_version and role (final). |
grade.not_applicable_reason | Why lines or the whole grade abstained, or null. |
disclosures | single_run: true, not_reply_prediction: true, and repeat_spread. See below. |
optimize_id | The handle of this grade. |
evidence, unsupported_claims, cited_evidence, fit_for_audience, notes | As on a write: see Response. |
lift | Always null: a grade has no before and after. |
Unknown is never zero
A line that could not be graded has pass: null and applicable: false, with a reason: for example corpus_empty (no synced conversations) or evidence_read_failed (Amdahl could not read your conversations, which is our outage, not a fault in the message). Such a line is out of both passed and total. When no line could be graded, score and passes_bar are null, never 0.
What one grade can tell you
- It is one reading.
disclosures.single_runis alwaystrue. A judge does not give the same score every time.disclosures.repeat_spreadsays how far this judge's score moved on a repeat in Amdahl's measurement:shareis the fraction of texts whose score moved by more thanmoved_past(0.2). For Sonnet 5.5 with thinking off that was about 15% of texts. It is not a confidence interval. To compare two drafts, grade both and read the line verdicts, not only the scores. - It does not predict replies.
disclosures.not_reply_predictionis alwaystrue. When Amdahl tested judges on real first-touch emails, one that got a reply and one that did not, from the same sender, every judge scored the replied email higher about half the time. A high grade means the message meets this rubric against your evidence. It does not mean the message is more likely to get a reply. - Length is not graded. No line rewards a shorter or a longer message.
- A grade written for one recipient fits that recipient. Your
factsand a recipient package are evidence for that call only.
Grading the same text again
A grade is stored by its request: the same text with the same fields returns the same grading, and does not grade again. To measure how far the judge moves on a repeat, send grade_nonce (only with mode: "grade"; 1 to 64 characters of letters, digits, _, ., : and -). A request with a different grade_nonce is a different request, so the same text is graded fresh. No judge sees the nonce. The same nonce again returns the stored grading. Each fresh grading is billed as a grade.
Billing and limits
A grade that returns ok: true is billed as a grade, on its own meter (message_grade_run), separate from a write. With an API key or an external OAuth token it counts toward the same monthly cap as an optimize (1,000 per workspace by default). A grade that fails is not billed. The in-flight limits are the same as for a write.
Record what happened
To say what the sender did with a message, use record feedback on a run.
Grades before 22 October 2026
Until 22 October 2026, grades came from evals (evals.run, the MCP run_eval tool). Evals retired that day. Their scores were on a different scale and from a different judge, so they do not compare with a grade from this call. Stored eval runs stay readable through the MCP get_eval_run tool. The REST eval routes answer 410 gone. See the changelog.