Changelog
RSSAll notable changes to the Amdahl Platform API are recorded here. The format follows Keep a Changelog: each entry groups its changes under the sections that apply (Breaking, Added, Changed, Deprecated, Removed, Fixed, Security) and omits the ones it does not use. Breaking comes first and flags anything that needs action on your side. Dates are the day the change landed on the stable path.
Last updated 2026-10-08
Filter
Added
- See and edit the "you've been added" email before it sends. Adding people on the console's People page (one person or a pasted list) now shows the exact email they will get, with an editable subject and message, before anyone is added. Edits are plain text and apply to that one send; the sign-in link is always included.
workspaces.add_memberand the partner API keep sending the standard email. - The Amdahl CLI. Run
npx @amdahl/cli login, or install it withnpm i -g @amdahl/cli.amdahl loginsigns in from your terminal through the browser.amdahl statuschecks your setup,amdahl optimizerewrites drafts from files or standard input,amdahl keys createandamdahl keys revokemanage API keys with approval in the console (the key is made on your machine and never passes through the browser), andamdahl installconnects Claude Code, Codex or Cursor. See Command line (CLI). - Choose a key's Access and expiry when you create it. In the console, Settings, then Developer, then Create key now has an Access picker (Read only or Customer agent, plus the admin levels for workspace admins; Customer agent by default) and an Expires picker (30, 90 or 365 days, or no expiry; 90 days by default).
GET /me/contextnow also returnsemail,workspace(id,name,slug), yourrolein that workspace, andclient_id(the OAuth client for an OAuth token,nullfor an API key). The existing fields are unchanged. See Authentication.- Check your setup in one call. The new
GET /setup/status(setup.status), and thesetup_statusaction on the MCPconnectionstool, report the workspace and caller a credential resolves to, whether it can optimize drafts, and if not, the one thing to fix:missing_scope,role_too_loworquota_exhausted. It also shows this month's optimizer quota and which data connections need attention. It needs onlydata:read. See Connections. - Every MCP tool now has a display title and all four behaviour hints.
tools/listreturns atitle(for example "Customer search") and explicitreadOnlyHint,destructiveHint,idempotentHintandopenWorldHintannotations for each of the six tools. Clients that read them can label the tools and decide when to ask before a call. The values are listed in Connect your agent. - A new agent skill,
amdahl, is one map of what Amdahl can do: each job with its MCP tool and action, CLI command and REST endpoint. An agent asked "what can I do with Amdahl?" answers from it instead of calling every tool to find out: https://docs.amdahl.ai/skills/amdahl/SKILL.md. - The MCP server serves the Amdahl capability map as the
amdahl://capabilitiesresource (markdown, the same text ashttps://docs.amdahl.ai/skills/amdahl/SKILL.md), so a client that cannot open a web page can still read what Amdahl does. - Each MCP tool's
actionparameter now lists every action, what it does, and the parameters it needs. An agent no longer has to call actions one by one to learn the tool. - The MCP server now tells clients its title, a short description, its website (
https://docs.amdahl.ai) and its icon. - Connection
healthhas a new value,paused. It means Amdahl has switched off syncing for the whole workspace, so no source syncs until it is switched back on. These connections used to report the outcome of their last run, such asrate_limitedorerror, orstale, long after that run.pausedranks belowdisconnectedand above every other value. It appears onGET /connections,GET /connections/:id,GET /connections/:id/statusand in theneeds_attentionlist of the setup status read. A client that switches onhealthshould handle the new value;connections.reconnectdoes not resume a paused connection. messages.optimize(REST and the MCPmessagestool): a draft that is too weak to rewrite now comes back withask, the questions only its sender can answer. Each is{ "asks", "field"? }, and there are at most three. Before, the draft came back unchanged and the questions were dropped. Put the questions to the sender as written, add their answers to the draft and run it again.askis omitted on every other result. See Reading `ask`.- Five Optimizer cookbooks, four of them for a workspace with no data connected: Optimize a cold email reads one real answer end to end, including a kept draft and its suggested edits; Optimize a campaign runs a CSV of drafts and writes the before and after side by side; Optimize before you send puts the call between the code that writes a draft and the queue that sends it; Keep your voice, rules and facts covers
context; and Check claims against your customers is theevidence: "workspace"step once your conversations sync. - The docs now start with the Optimizer: the home page, the Quickstart, Primitives, Use cases, Patterns and the Glossary lead with the call that needs no data, and say how to join the beta when you have no workspace yet.
messages.optimize(REST and the MCPmessagestool) takes a new optionalinclude_tries. Sendtrueand the result also carriestries: every version the run scored, your draft first, each with its text, its Optimizer score and Human tone (1 to 5), what it was aiming to improve, and which one came back. Use it to show how a draft improved step by step. Those scores come from the Optimizer's own grading, soliftstays the before-and-after to quote. Without the field, requests and responses are unchanged.- Each note in a Message Optimizer result now carries
audience. Asendernote is about the draft or the version that came back, in plain words, and can be shown to the person who wrote the draft as written. Adevelopernote says how to read the result, for example which grader decided or what to send next, and is not for the sender. A code has the same audience on every run, so filter on the field instead of keeping a list of codes. - A new
developernote,evidence_available, says that a style pass could also have been graded against your workspace's conversations, and how to ask for that (evidence: "workspace"). It comes with thestyle_pass_onlynote, on a request that sent noevidence. - A new agent skill,
amdahl-optimizer, gets a first Optimizer result from Claude, ChatGPT, Codex or Claude Code by pasting one link: https://docs.amdahl.ai/skills/amdahl-optimizer/SKILL.md. It connects over MCP or an API key, finds upcoming outbound drafts in the tools already connected, optimizes 3 to 5 of them and shows each before and after, and writes nothing back without approval. - A new page, Optimizer, step by step, shows one real email (names changed) going from draft to the version you get back, with the score of every try.
- You can now try the Message Optimizer without a workspace at console.amdahl.ai/try. Sign in with your work email, paste drafts or drop a CSV, and watch each version get written and scored. Each person gets 25 free runs, and the first finished run puts you on the beta list.
- Theme search and theme detail now say how many conversations mentioned each theme in a period, when it was last mentioned, and whether it is rising, steady, cooling, quiet or new. Counts are in conversations (calls, emails, meetings, notes), with a split by type. Each response names its period: the last 90 days unless you pass one.
- Both take an optional
time_range({last_n_days: 30}or{start, end}). With it, search returns only themes mentioned in that period, matched on conversations from it. Theme detail adds a 12-week trend and, when part of the period's conversations could not be analysed, a line such as "Based on 212 of 240 conversations in this period". - Theme detail now returns up to 10 dated quotes from the period, newest first, each with its date, conversation type and speaker.
Changed
- The MCP
messagesandevalstool descriptions now tell agents to send a draft to the optimizer instead of rewriting it themselves, and never to grade a draft themselves. - Everyone added to a workspace now hears about it. Adding an existing account by user id with
workspaces.add_membernow emails them the same "you've been added, sign in" notice an add by email sends, and clears any pending invitation for them. A user id with no account returnsinvalid_target. - The instructions the MCP server sends on connect are shorter (under 2,000 characters, so Claude Code no longer cuts them off) and open with the hard rules: send drafts to
messagesoptimizeinstead of rewriting them, and never grade a draft yourself. They list what you can do as tool and action pairs, and no longer point at prompts or web pages that most clients cannot open. - An MCP call that names an unknown action, or leaves out a parameter its action needs, now returns a tool error that lists the valid actions or the missing parameters, instead of a bare validation error.
- The ChatGPT steps now describe the desktop app's real path (Settings, then Plugins, then Add MCP server), and say that ChatGPT calls Amdahl in Work mode when you name it.
- The Claude and ChatGPT steps now end with the ask that makes the app call Amdahl ("Use the Amdahl MCP server to optimize this draft"), and the ChatGPT steps no longer point at a web menu a free plan does not have.
- The docs now offer the Claude and ChatGPT apps as a first way in: add Amdahl as a connector with the URL
https://app.amdahl.ai/mcp. Connect your agent gives the steps with Claude's current menu labels (Customize, then Connectors) and new ChatGPT steps. - The MCP server now describes itself as the Amdahl Optimizer. When a user asks to improve a draft but has not given one, agents ask them to paste it.
- The email a person gets when an admin adds them to a workspace now says how to use Amdahl in Claude or ChatGPT.
- The consent screen now lists the Amdahl Optimizer first, and says on its own line when a client can email members of your workspace.
- The Amdahl capability map gives the Claude and ChatGPT app connector steps, including the reload a new connector needs.
- Connection
healthno longer reports an old run's outcome as current.rate_limited,degraded, and anerrorthat came from a failed run now hold only while that run is recent: once the last run finished more than 48 hours ago,healthreadsstale, because nothing has run since. Anerrorstatus on the connection itself andneeds_reauthare unchanged. - The
gtm-strategistlibrary agent reads the market only when its run has external search on (a Chat withexternal_searchordeepdepth). Without it, the agent says the market side is unread and makes its call on your buyers' evidence alone. Its description in the agent roster now says so. - The guided walkthrough (
system/guide_me) now opens with Improve an outbound email or LinkedIn message. It needs no connected data, so it works on a brand-new workspace: Amdahl rewrites your draft through themessagestool, and the assistant shows you the version that came back. The other choices each move down one number, sogoal: "2"now opens meeting prep. When a choice finds nothing because no conversations have synced yet, the assistant says so, tells you how to connect a CRM or call recorder, and offers the first choice. - The prompt that the console's Copy key + prompt button and Agent prompt tab give you now has your agent optimize a few of your upcoming drafts first, which needs no data. It asks the agent to survey your workspace's conversations only once they have synced.
- The MCP server now lists ten tools instead of six, so that each tool is clearly read-only or a write tool.
research,evals,agentsandconnectionseach became two tools:<name>_readholds the reads (status polls and lists) and<name>_manageholds the actions that start a job or change something.searchandmessagesdid not change. Actions, parameters, results and scopes are the same. A client can now run a status poll without asking you to approve a write. - The
evals_managetool is marked as able to reach outside Amdahl, becauserunwithinclude_externalreads public web evidence. The oldevalstool did not say so. - The console's suggested starting questions now pick the broad customer theme most active in the last 90 days, and say how many conversations mentioned it in that period. They used to pick the largest theme over all time and show its utterance count as conversations.
- Seven Message Optimizer notes that are shown to a sender no longer hold words written for an agent:
kept-by-gate,asked-for-input,run-abstained,style_pass_only,draft_not_scored,rewrite_not_regradedandreturned_message_breaks_user_rules. Each one states the same facts and limits. For example,style_pass_onlynow says "A style pass does not mean that the draft passed or that it is ready to send." in place of "Do not tell the user it is ready to send or that it passed.", and it no longer ends with theevidence: "workspace"hint (that is now theevidence_availablenote). Every note keeps itscode. Code that matches the oldsaystext must change. - The
messagesMCP tool's description is now short sentences, with one list for thecontextfields and one for the result fields. It states the same facts and limits as before. The tool's name, inputs and behavior did not change. - The Message Optimizer now writes
summaryandnotes[].saysin plain words for a sales rep. A summary names what the rewrite aimed at in words, such as "how clear the ask is", instead of an internal key such as "cta clarity". Every note keeps itscode, so code that branches oncodestill works. Code that matches the oldsaystext must change. - The Message Optimizer
summaryno longer names a field of the result. While the returned message still holds claims that nothing in your workspace's conversations backs,summaryends with a sentence for the sender. It says how many claims there are, and to check them before sending: "Nothing in your workspace's conversations backs 3 claims in this message. Check them before you send it." It used to end: "3 claims couldn't be matched to your workspace's conversations. See unsupported_claims before sending." Thedevelopernoteunsupported_claims_keptis unchanged and still points atunsupported_claims. Code that matches the oldsummarytext must change. - Rate limits now depend on what a call costs. Each credential has four separate budgets per minute instead of one budget of 60: 600 for MCP protocol traffic (
initialize, notifications,tools/list, resource and prompt reads,ping, the/mcpevent stream), 300 for reads (every RESTGET,search, status, list and get actions), 30 for expensive calls (messagesoptimize,evalsrun,researchstart, Chat start, routine run-now,connectionsconnect) and 60 for everything else. Several agents on one API key no longer use up the budget with session traffic alone. A JSON-RPC batch counts each item. Requests that hold only protocol calls now count against their own ceiling of 1,200 per minute per client IP; the 300 per minute per client IP ceiling still covers every other request. The429response is unchanged, andX-RateLimit-Limitnow reads the limit of the class the request was counted in. Monthly quotas are unchanged. See Rate limits. - A Chat that a research job opened now reports
trigger: "research"on the Chat list, the Chat read and its run reads. It used to report"chat", so a client could not tell a research job from an ordinary Chat. Treat anytriggervalue you do not recognize as a plain Chat. search.query(and the MCPsearchtool) no longer refusesaudience: "customer_voice"on the fuzzy lane. That lane writes its own SQL and cannot apply the audience, so the request now runs on the semantic lane withhydrate: true, which can. The response carriesmode_ran: "semantic", a newrerouted_from: "fuzzy"field and a sentence in a newnotesarray. The rows are the customer utterances closest in meaning to the query, not a computed answer; for a count, sendfiltersormetricswith the same audience. On the semantic lane, leavinghydrateout now turns it on instead of refusing. Sendinghydrate: falsewith this audience, or asking for it on the lexical lane, is still refused.search.querynow clamps alimitabove 1000 to 1000 instead of refusing it with "Number must be less than or equal to 1000". The response reports the change on a newlimit_clampedfield ({ "requested", "applied" }) and innotes. The maximum is still 1000.- Theme answers now say which period their counts cover on every surface.
searchreturns the period, when themes were last updated, or why there are no counts, incorpus.themes, including when it falls back to the theme index. - Theme cards in agent answers can carry the theme's conversations in the period, its status and its last mention. A theme's size is described as utterances over all time, never as conversations.
- A theme that has not been named yet (minutes after themes are recomputed) is titled "New theme (title pending)" instead of showing a blank title.
- Theme reports (VoC Cluster Report, Weekly GTM Digest, GTM Priority Briefing, Ecosystem Map, Positioning Scorecard) created from now on rank themes by recent activity, where the last few weeks count most, instead of all-time size. Counts are conversations in the last 90 days, and each theme shows its last mention and status. "Themes accelerating now" is now "Themes rising now": themes up in the last 30 days against their own prior 90 days, relative to the whole workspace. A report created before this change keeps its saved queries; create it again from the template to get the new ranking.
- Asking
searchabout themes with a period ("what are customers saying about pricing in the last 30 days", "last quarter") now returns only themes mentioned in that period. Each theme shows its conversations in the period, last mention and status (rising, new, steady, cooling or quiet), and the answer says which period the counts cover and when themes were last updated. - Theme sizes in research answers and agent activity cards are now labelled as utterances over all time. They were labelled as conversations, which overstated them: a theme's size counts utterances, and one conversation has many.
- Creating a workspace now needs beta access. Amdahl is letting teams in a few at a time.
POST /workspaces(workspaces.create) returns403 creation_disableduntil your address, or your company's domain, is approved, andGET /workspaces/minereportscan_create_workspace: falseuntil then. Join the waitlist at amdahl.ai/beta, or in one click from the console once signed in; we email you when you're in. Joining an existing workspace (your company's domain, an admin adding you, or an access request) is unchanged.
Deprecated
POST /api/mcp/api-keys/{business_id}is deprecated. It still works, but it takes a name only, so its keys get a fixed permission bundle and never expire. Create keys in the console instead, where you choose Access and Expires. Removal will be announced here first.- The MCP tool names
research,evals,agentsandconnectionsare deprecated. They are no longer listed, and a call on one still works for clients that hold an older tool list. Use<name>_readfor reads and<name>_managefor everything else. The old names will be removed after a deprecation period.
Removed
- The
researcheragent is gone from the Amdahl agent library. For one cited answer from your own calls, CRM accounts and deals, use Research (research.start). For a go-to-market recommendation, use thegtm-strategistlibrary agent. A Chat start or a delegation that namesresearchernow returnsnot_foundwith a message that names both, and no workspace agent can use the slugresearcher.
Fixed
- The Cursor setup that Amdahl's MCP setup instructions return now uses the server URL alone and signs in over OAuth, like the other desktop clients. It no longer tells you to paste an API key into an
X-API-Keyheader. The Claude Desktop and Codex snippets on amdahl.ai are URL-only too. Codex setup still setsrequired = true. - Signing in to connect an AI client now works for anyone with a work email. Before, the OAuth sign-in page refused an email that had no workspace and no company match, and showed "book a demo" instead of the beta waitlist. Now any well-formed email gets a sign-in code. After sign-in, a person with no workspace sees Join the beta (or Create a workspace first, if they are already approved), with a link to console.amdahl.ai/new. Members connect as before.
- Chat and research answers no longer lose their last tables and figures to formatting slips. When a table's rows or a figure's value arrived in a slightly different shape than expected, the block was refused, and a run that retried it near the end could stop before the corrected block or the closing summary was written. Those shapes are now accepted when their meaning is clear. A refused block no longer counts toward the limit that ends a run for repeating itself, and a corrected retry is no longer mistaken for a repeat of the attempt that was refused. A block whose meaning is unclear, such as a figure given two different values or two different changes, is still refused so it can be fixed.
- Research and Chat answers describe where a figure came from in business terms (the calls, deals, accounts and period behind it) instead of naming internal tools, queries or search modes. Ask how an answer was found and the agent still explains the mechanics.
- The console Data Explorer's query route now reports when its row cap or your own
LIMITcut the result. A query with noLIMITover 198 rows used to come back as 100 rows marked complete, and aLIMITequal to the cap had the same gap. It now peeks one row past the limit the same waydata.querydoes: a cut is flaggedtruncated: true, and aLIMITyou set is never refused, only marked when more rows exist. data.querywithallow_truncation: truenow says when the row limit cut the result. A query with noLIMITover 150 rows at the default limit of 100 returned 100 rows withtruncated: false; it now returns the same 100 rows withtruncated: true. A population that fits the limit exactly still readstruncated: false, and at the 1000-row cap a full result reads as truncated. Withoutallow_truncationthe call still refuses a cut it imposed withresult_truncated, and aLIMITyou write is still returned and marked rather than refused.data.querynow says when a query's ownLIMITcut the result. ALIMITsmaller than the row limit returns at most that many rows, but a larger population was reported as complete:SELECT … FROM deals LIMIT 100over 150 deals returned 100 rows withtruncated: false. It now returns the same 100 rows withtruncated: true. Writing theLIMITis your choice of a top-N, so this needs noallow_truncationand is never refused; a population that fits yourLIMITexactly readstruncated: false. A query with noLIMIT, or one at or above the row limit, is still refused withresult_truncatedwhen it is cut, unless you passallow_truncation: true.- A
data.queryLIMITabove the row cap no longer drops itsOFFSET.LIMIT 5000 OFFSET 200used to return the first rows; it now reads from row 200, capped. AnOFFSETthat is not a non-negative whole number, and MySQL'sLIMIT <offset>, <count>, are refused with a message naming the fix rather than run as a different query. - Invitation links open a working page. The invitation email now links to the console. The invitee signs in with an emailed code, and the console adds them to the workspace. Accepting needs that signed-in session, works only for the invited email address, and never asks for a password.
messages.optimize(REST and the MCPmessagestool): a kept draft'skept_suggestionsno longer tell you to break one of your ownrules. A suggestion is left out when a rule names the part of the message it would change (the ask, the recipient, the pitch, customer proof, the length or dashes), or when a rule repeats four or more words in a row from itsspan. Before, a draft kept under the rule "End by asking for a 30-minute call next week" could come back with aclarify_asksuggestion to replace that call with a question.- When an MCP connector was authorized before a capability existed, a call to that capability is still refused, but the message now says to disconnect and reconnect the Amdahl connector (in Claude: Settings > Connectors) to re-authorize it, and the result's
structuredContentcarriesreauthorize: trueand themissing_scope. - The
summaryof a draft that is too weak to rewrite no longer points to a field. It said "listed under ask", but the response had noask. It now reads "Kept your version: it needs details only you can add, so nothing was rewritten.", and the questions are inask. messages.optimize(REST and the MCPmessagestool): on a style pass, the check for an invented claim now catches more of its forms, such as "have seen strong results", "I've seen teams cut it by half" and "we've helped". Before, a rewrite that added one of these could come back as an improvement. Now it is discarded, the previous version stands, and therewrite_rejected_for_invented_claimnote names the phrase. A curly apostrophe counts like a straight one, so "We’ve seen" is caught too. If your draft or a fact incontextalready says it, a rewrite may keep it.messages.optimize(REST and the MCPmessagestool): a rewrite no longer offers a day or a time that your draft did not give. Before, a draft that said "next week" could come back asking "Does Thursday at 2pm work?". Such a rewrite is now discarded and your draft comes back, with therewrite-rejected-for-entity-lossnote naming the added day or time. The same check now sees a number with letters attached, such as2pm,12thor40k, so a rewrite can no longer change one of those unnoticed. A fact incontextcan supply a day or a time. A rule cannot: put a slot you want offered in the draft.- The Message Optimizer reference now matches what the default style pass returns:
liftisnullon a style pass, because a second model re-checks the rewrite on yes/no questions instead of scoring it, andextractioncarriesreceiver_name,company,signalandchannel. It also says that the default pass prefers a one-line question to a meeting request, and how to keep a meeting ask with a rule incontext. messages.optimizenow states its expected latency as 25 seconds, the production median, in the operation catalog and in themessagesMCP tool's description. It said 90.- The free runs on console.amdahl.ai/try now count per mailbox, so
[email protected]and[email protected]share one set of 25. Each company domain also has its own daily limit, so one company can no longer use up the day's runs for everyone else. - A research job's
answer.textno longer comes back as a working note from earlier in the run ("Let's pull…") instead of the answer. Two causes are fixed. A job that ended by presenting its write-up as one more block had that block discarded as a loop; it is now saved, and the run ends right after it. And when the closing turns carry no prose,answer.textno longer reaches back past the job's last data read to quote its narration: it renders the presented evidence instead. A Chat'sanswer_textfollows the same rule. - The
search.queryfilter lane now says plainly when its row limit cut the result.corpus.truncatedused to be guessed from the row count matching the limit, so a result that fit exactly was reported as cut, and a read that filled the 1000-row maximum could not say whether more rows existed. The lane now checks for one row past your limit, and at 1000 it counts every match and reports the total on a newcorpus.total_countfield. A line innotesstates the truncation, for example "1000 of 2341 matching rows returned". - The email a person gets when they are added to a workspace, invited, join through their email domain, or ask for access is now delivered. These emails did not reach anyone before.
- The sign-in consent screen now says what Claude or another client can do in plain terms, including improving drafts with the Amdahl Optimizer, running evals and starting chats.
- Theme quotes now say who spoke them. Each quote in a theme result (
data.cluster_search,data.cluster_detail, andsearchwhen it answers from the theme index) carriesspeaker_type:externalfor the customer side,internalfor your own team, orunknownwhen the speaker could not be identified. It is omitted when a quote has no speaker recorded. Until now these quotes carried no speaker, so tools reading them treated your team's words as customer quotes. External search now keeps only customer quotes when you ask about a topic, shows your team's quotes as yours when you ask about a company or a person, and leaves out quotes whose speaker is unknown. In evals, a theme quote from your team now carries thespeaker_sidebadge and is marked as yours to the judge, which is told not to treat it as evidence of what customers think. prompt-and-message-evalis now version2.38.0anddeal-brief-evalis now1.1.0. The only change is the speaker marking above. On current data it reaches about 1 theme quote in 22, so scores can shift slightly; do not pool scores across these versions. Evals you authored that read theme quotes are graded afresh rather than reusing a result graded before this change.- A workspace with no conversations in the last 30 days no longer shows every theme as "Cooling". Status compares a theme against the whole workspace; when the workspace itself is silent (for example, a data source stopped syncing), a theme now shows as Steady, and its last mention date shows how long it has been quiet.
- Embedded links to theme reports now show their themes. A link shared with a data scope says that theme activity is not available under that scope, because theme counts include every conversation in the workspace.
- Theme detail for agents now keeps the theme's name and summary when the theme has long quotes. Dated quotes are shortened like other quotes.
Security
- Sign-in no longer sends a code to an
@amdahl.coaddress. That domain is no longer ours. The desktop app's code request answers 400 and points to youramdahl.aiaddress. - A deleted workspace can no longer be reached through partner act-as, membership-checked API routes, or public artifact links. Partner act-as for a deleted workspace answers the same
404 workspace_not_foundas a workspace that does not exist, and a public artifact from a deleted workspace answers the same "Not Found" as any missing artifact. - Sign-in codes and workspace domain claims now also refuse
amdahlapp.co, a look-alike domain that is not ours.
Changed
- A new ceiling of 300 requests per minute per client IP address sits around the per-caller limit. It counts every request from one address, whatever credential it sends. A
429from it has the same body andRetry-Afterheader.
Fixed
- The global limit of 60 requests per minute now counts each caller on its own. It used to count by the network address the request arrived from, which in production was a shared Cloudflare edge server, so unrelated callers routed through one edge shared one budget and could get
429without sending much traffic. Requests that send a credential (AuthorizationorX-API-Key) are now counted per credential, and requests without one per client IP address. The limit itself and the429response are unchanged.
Breaking
POST /workspaces/:id/request-accessreturns a new response shape. Therequested: truefield is gone; the body is now{ status, request_id, workspace_name, notified }, wherestatusisrequestedfor a new request oralready_pendingwhen an open request already exists (admins are not emailed again). The endpoint now refuses with 403not_eligiblewhen the workspace does not claim the caller's own email domain. Rate limits are a 24 hour wait after a denial and at most 5 new requests per user per 24 hours, both answered with 429rate_limited.- Keys created with
POST /api/mcp/api-keys/{business_id}now carry a named permission bundle: Customer agent (mcp_customer_agent) when a workspace admin creates the key, Read only (mcp_read_only) for anyone else. The response names the bundle and lists its scopes. These keys used to be issued with["read"], which carried every permission of the creator's workspace role, including connecting and disconnecting data sources, which neither bundle grants. Existing keys created this way now carry the same bundles. To choose a bundle, create the key in the dashboard. - Chat, Routine and Subscription configs (and saved Chat preferences) now refuse the retired
write_memoryandwrite_outputsknobs withinvalid_argument, like any other unknown field. Drop them from your requests; neither had any effect. data.searchno longer returns knowledge-base documents. Itssource_typeis nowsaved_research,content_creationorartifact;knowledge_bankis gone.actions.invokeandactions.listoffer one action,email_member. Thenotion_syncaction is gone.notifications.list_sendsno longer takes ablueprint_run_idfilter, and itssummaryno longer hascap_per_run.notifications.email_memberkeeps only the per-workspace hourly cap.workspace_readme://currenthas two sections, pages and actions. The knowledge-document and workflow sections are gone.- New MCP keys no longer carry the
knowledge_base:*orblueprints:executescopes. Existing keys may still list them; they no longer grant anything. corpus.contentonPOST /search/query(and the MCPsearchtool) is now omitted when the rows have nocontenttext to describe. It used to read"full"on every warehouse read, includingdealsanddeal_qualificationanswers, every other surface a fuzzy answer can query (competitor_mentions, …) and theme-index answers, none of which has acontentcolumn, so the field vouched for quotable text that was not there. It is now"full"only oninteractionsreads and"preview"on the vector-mirror lanes (lexical, andsemanticwithouthydrate), and absent everywhere else. This is the same encodingspeaker_scopeuses for a missing speaker column: absent means the rows hold no such text, not that the build is silent. It describes the surface, not your projection, so aninteractionsaggregate that selects nocontentstill reports"full". Action only if you required the field: a client that treats it as always present, or reads a missing value as"full", should test for"preview"to detect a truncated read and treat absence as "nocontenttext on these rows". Code that already testscorpus.content === "preview"is unaffected.
Added
messages.optimize(RESTPOST /messages/optimize, MCPmessages→optimize) takes an optionalcontext, so a rewrite can follow the sender's standing instructions and sound like them.rulesare hard constraints the rewrite must follow, and are never rewritten or quoted in the message.voice_examplesare messages the sender wrote themselves: their voice is matched, their words are not copied. Up to 20 rules of 500 characters and 5 examples of 4,000; blank entries are dropped, and an unknown key or an entry over its limit is refused with400 invalid_input.- The
messages.optimizeresult now carrieslift, the only before-and-after score: your draft and the rewrite, each re-graded blind by a second model against the same evidence.lift.returned: "draft"means the rewrite did not beat your draft on that re-grade, so your draft came back withunchanged: true.liftisnullwhen nothing was measured. - The
messages.optimizeresult now carries three more facts about the run, each only when present and well-formed.noteslists what changes what may be said about the rewrite, each with acodeto branch on and asayssentence to repeat as written; it is omitted when there are none.contextgives the counts of the rules and voice examples the optimizer received, when you sent acontext. And where the optimizer reports a refused rewrite,vetoed_roundandveto_reason(not_betterorbreaks_rules) say which rewrite was refused and why your draft came back. - Workspace access requests. A signed-in user whose email domain matches a workspace can ask to join it, and the request is now recorded instead of only emailed.
GET /workspaces/access-requests/minelists the caller's pending requests. Workspace admins review them from a signed-in console session withGET /api/team/:businessId/access-requestsand decide each one withPOST /api/team/:businessId/access-requests/:requestId/approveor/deny. Approving adds the requester as a member and emails them. An API key or OAuth token sent to these admin endpoints is refused with 403session_required. - Fast search answers deal questions from your CRM deals.
searchin fuzzy mode (and the console's Ask AI) can now count deals, total and average won deal value, compute win rate and list open deals by size, in one call. Before, these asks came back as "open a Chat" or as estimates built from conversation rows, where each deal is counted once per utterance. A question that needs deals and conversations together, such as the deal value of accounts that raised objections, still points you to a Chat. - What you know, as evidence.
messages.optimizeandevals.runtakefactsinsidecontext: things you know first-hand about the recipient that no recorded conversation holds, such as "Met Priya at the GTM dinner on Sep 24; she said her SDRs rewrite every AI draft". Facts are graded as their own evidence tier,CALLER: a claim a fact states is no longer marked fabricated, a claim that goes further than the fact still is, and a fact is never counted or shown as a customer quote. Up to 10 facts of 1,000 characters each; blank entries are dropped and an unknown key insidecontextis refused with400. Onevals.run, onlyfactsis accepted insidecontext, and only by evals that can grade it (prompt-and-message-eval); each graded side of the report lists the facts it relied on ascaller_facts, and the ack echoes how many facts arrived. A run that sends no facts is graded and addressed exactly as before. prompt-and-message-evalis now version2.37.0. The only change is the facts tier above, reached only by a run that sends facts; do not pool the scores of a run with facts and one without.- Codex setup with
required = trueeverywhere you connect: theamdahl://setupresource and the console's setup instructions now include acodexConfig, and Reliability and retries has a "When the agent skips Amdahl" section for agents that reach for other tools first. - Computer-use credential vault. A workspace admin can store a sign-in credential once — domain-bound and encrypted at rest — and the browser agent fills it by reference: it names an opaque handle, never the value. The plaintext never reaches the agent's context, the chat transcript, or the command audit, and a fill is refused unless the live tab's origin is on the credential's allowlist.
- Connecting an API-key source now tells you where to get the key and what it needs to see. Every connectable API-key connector carries a link straight to the vendor's key page plus the scope traps that otherwise present as an empty workspace: a Granola key from the wrong workspace, a Fathom key scoped to one person, a Grain personal token where a workspace token was needed, Fireflies API access being Business-plan only. The guidance rides the catalog, so
connections.list_catalogreturns it onconnect.fields[].helpand the newconnect.keyUrlfor API and MCP callers too. - The documentation is now its own MCP server, at
https://docs.amdahl.ai/mcp. Point an agent at it and it can list every page, read one in full, and search across all of them, so it can look up how a tool actually behaves instead of guessing at a parameter name — no more pasting a docs URL into every prompt. It is public: no API key, no OAuth, nothing to approve. It is deliberately a second server rather than new tools onhttps://app.amdahl.ai/mcp, so connecting for documentation costs your workspace connection nothing, and your data stays behind the credential that already protects it. Connect it withclaude mcp add --transport http amdahl-docs https://docs.amdahl.ai/mcp, or install theamdahl-gtmplugin, which now ships both. See Read the docs over MCP. - Author your own evals.
POST /evals,PATCH /evals/:slugandDELETE /evals/:slugare open to every workspace — define the input fields, cases and graders you want, with a rubric scoped to the question you are actually asking. Every write is validated first, so you get author-time errors instead of a broken run, andPOST /evals/validatedry-runs a definition without storing it. The built-inprompt-and-message-evalis tuned for seller-to-buyer writing; author your own when you are grading something else, such as landing headlines against evidence alone. - The
POST /evals/runresponse now includespoll.rest, the REST paths for the run:url(GET /api/platform/v1/eval-runs/{id}?wait_ms=30000, which waits until the run finishes) andreport_url(/eval-runs/{id}/report). Until now the poll block gave only MCP addresses, and the obvious guess,/evals/runs/{id}, returns 404. The existing fields are unchanged. - An eval report now says how concentrated its evidence was. A grounding score over twenty quotes from two companies and one over twenty quotes from fourteen are very different readings of "your customers say this", and the two were indistinguishable: the prompt takes a fixed-size prefix of the retrieved pool with no per-company consideration anywhere between the merge and the slice, so one talkative account could fill the window unnoticed.
evidence_provenancenow carriescompanies_in_contextbesidein_context— how many distinct companies the corpus-tier quotes the judge actually read came from. It counts the corpus tier because that is the only one without a bound: the cohort draw already caps quotes per company and an account run is one company by definition. Read it in one direction only: a company whose CRM resolution failed appears under its raw email domain, so the count can over-state diversity and never under-state it. A low number is trustworthy; a high one is not a diversity certificate. Absent, rather than zero, when no quote in context named a company at all. - An eval run now tells you what its evidence search never got to. Retrieval plans a set of queries — two framing seeds, then one per specific claim in your draft — and walks them a round at a time, stopping as soon as it has enough quotes. On a workspace with a rich corpus the first round is usually enough, which means the later per-claim queries are never run at all. The run looks entirely healthy, and thin support for your fourth claim was indistinguishable from "your customers have not said anything about it".
evidence_provenancenow carrieslegs_plannedbeside the legs that actually ran, plusplan_stopsaying why the walk ended.plan_stop: "pool_full"next to a plan larger than the legs run is the case to read: nothing failed, and some of your claims were simply never searched for. - Home tells you when a large share of a call sits on one anonymous speaker track. Some recorders label a voice "Speaker 2" or "Phone Caller #3" instead of naming it, and sometimes they fold two or more people into that single unnamed track: one call in the test workspace has 253 of its 420 lines on a "Speaker 2" track that holds both of its external attendees. Those lines stay searchable, but they are attributed to no one, so they were silently absent from anything that splits a call by speaker — and nothing said so. A new inbox row now counts the calls where this happens, so the gap in attribution is visible instead of invisible. It is informational on purpose: the recording itself cannot say whether an unnamed track is one participant or several, and either way the fix has to come from a corrected recording at the source — the review queue can flag a call, not separate the voices inside one track. Conference-room and personal-device speaker names are deliberately not counted; only anonymous placeholders like "Speaker 2" or "Phone Caller #3" are.
- A kept draft now comes back with edits you can make. When
messages.optimize(REST and the MCPmessagestool) returns your draft unchanged, the result can carry a newkept_suggestionslist, at most 5 rows, each{ "kind", "span"?, "says" }with an optionaldimensionandwhy: for example, back up or cut a claim, add something specific about the recipient, make the ask clearer, or shorten to a stated length.saysis fixed wording, safe to repeat as written;spanis copied exactly from your draft. The field is omitted when a rewrite is returned and when there is nothing to suggest, so existing callers see no change. POST /messages/optimizeand the new MCPmessagestool (optimizeaction) rewrite one outbound draft in the sender's own voice and return it on the same call, with a line on what changed. It needs no connected data, so it works in a brand-new workspace. It is judged on style only, never on whether a claim is true; for that, use theprompt-and-message-eval.unchanged: truemeans no rewrite beat your draft and your version was kept. Needs themessages:executescope, which Customer agent keys carry (keys minted earlier are backfilled). Calls from an API key or external OAuth token are capped at 1,000 a month per workspace; past that the call returns429 quota_exceeded.- Research: hand Amdahl one question about your customers and get one cited answer back.
POST /research(MCP: the newresearchtool, actionstart) starts a read-only job that investigates the question over your calls, CRM accounts and deals; read it withGET /research/:id?wait_ms=30000(actionstatus) until it settles. A job may pause to ask a clarifying question, answered withPOST /research/:research_id/respond, and one that reaches its usage or step budget answers from what it found, markedpartial: true. External callers can start 100 jobs a month per workspace. See Research. corpus.speaker_scopeon everyPOST /search/querylane. The field previously appeared only asexternal_onlyon the vector-mirror lanes, so on the warehouse lanes "this store holds both internal and external speech" and "this build does not say" were the same absence — and an internal-vs-external count had nothing on the wire confirming it was whole. Every lane whose surface has a speaker dimension now states its coverage,allincluded, and absence means one thing only: that surface has no speaker column (deals,deal_qualification). It describes the store, never your query — a search narrowed withspeaker_type: "external"still reportsall— and a hydrated semantic search reportsallwhere the un-hydrated one reportsexternal_only, because hydration leaves the mirror and returns full warehouse turns. Additive, with one caveat worth stating plainly: code testing the VALUE (speaker_scope === "external_only") is unaffected, but code testing the field's PRESENCE (if (corpus.speaker_scope)) now fires on every warehouse lane where it previously did not, because those lanes went from absent to"all"— inverting what such a check meant. Nothing breaks today (no known consumer reads the field: the frontend declaresspeaker_scope?: stringand never reads it), but if you wrote a truthiness check against the old shape, read the value.- Subscription source health.
GET /subscriptions/:idnow carries a derivedsource_healthblock that names why a quiet fires ledger is quiet —ok,never_swept,sweep_failing,connection_needs_reauth,connection_unavailable, orconfig_invalid— each with a plain-language remedy. A dead Google Calendar credential also flips the connection toneeds_reauthon the Connections surface (with the Reconnect affordance), instead of failing silently; recovery is automatic once the calendar is readable again.
Changed
- The message optimizer service now authenticates callers directly, so
BASE=https://staging.amdahl.ai/optimizeraccepts your own platform API key again alongside in-app use. - Natural-language SQL questions in the Data Explorer (ask-AI) respond faster, especially on repeated or self-correcting queries, by caching the shared schema and playbook prompt.
- The message optimizer now grades against your own buyer evidence instead of a placeholder. Until now it scored a draft with three surface checks — is there a name after the greeting, is there a digit anywhere, does it end in a short question — and that score decided whether the draft got rewritten at all. A weak message could score low enough to be sent back with questions rather than improved. It now runs the same graded eval the rest of the product uses, so the decision to rewrite, keep or ask is made against what your customers actually said.
- Looking up a social handle (
POST /social/verify) now needs the same permission as external search, which covers paid lookups outside your workspace. Read-only keys and viewer roles can no longer start one. Research jobs and other read-only agents are also kept off outside lookups, so a research job reaches nothing beyond your workspace's own data. - A help request from an agent on the workspace computer now emails one person, the one the run acts for (usually whoever started it, or for a routine or a subscription, whoever set it up), not every workspace admin. If that person is no longer a member of the workspace, no email is sent; the run still pauses, and its help card still shows in the chat.
- Agents driving the workspace computer now treat human help as a last resort. The computer-use guidance teaches solving obstacles independently first (dismissing dialogs, retrying, re-snapshotting, taking an alternate route to the same information), and passing a sign-in wall itself where possible - filling a stored workspace credential by reference when the vault holds one for the site, or clicking through single sign-on the browser already holds a session for. A help request is reserved for a wall none of that covers - a CAPTCHA, or a credential with no vault entry or SSO session - at most once per run, and only when no fallback path can deliver the result without it.
- The pasted eval report card now says what one run can and cannot tell you. Every card carries a short note that grading is not perfectly repeatable and that you should re-run a few times and read the median before treating a verdict as settled. That warning has been on the report page in the console for a while; the markdown block you copy out never had it, which is the copy that tends to end up in a decision.
- The report card's two-sided table now shows how long each draft is. The length check this eval runs applies to the message you submitted and has no counterpart on the rewrite, so the two sides were being held to different rules with nothing on the page saying so. Both numbers were already measured on every run. It is shown, not graded: the measured effect of message length on replies reverses direction between workspaces, so neither number is a target.
- An existing OAuth connection no longer picks up new default scopes when it refreshes. When Amdahl adds a scope to the default set, a connected client gets it the next time the user authorizes the client again; until then, calls that need the new scope return
missing_scope. - A temporary server problem during an OAuth token refresh now returns a server error (5xx) instead of
invalid_grant, so the client keeps its tokens and can retry instead of sending the user back through authorization. - Evidence grading in `messages.optimize` is now opt-in per call. A new
evidencefield takesworkspaceoroff, and defaults tooff: a fast style pass that reads no customer conversations. Passevidence: "workspace"to keep the previous behavior on a workspace with synced conversations, where the draft is also graded against what your customers said (relevant_positioning,grounding,verified_specifics,differentiation), which is slower. Every result now carriesevidence: { requested, used, reason };usedistrueonly when conversations were read. Where none can be read, the run still succeeds, graded asoff, with a reason such asevidence_unavailableorcorpus_empty. Any otherevidencevalue is refused with400. The MCPmessagestool takes the same field. messages.optimizenow also grades a draft against the workspace's own customer conversations once they are connected. On top of style, it scores whether the offer is positioned for the customer's situation (relevant_positioning), whether its claims are backed by what customers actually said (grounding) and whether it makes a distinct, evidence-backed point (differentiation).lift.quotescounts the customer quotes the independent re-grade read. A workspace with no connected conversations is graded as before, on style alone. It still does not rule on each claim; the eval does that, line by line.- Unsupported claims are now kept and flagged by default. In a workspace with connected conversations,
messages.optimizeused to soften or cut any claim in your draft that those conversations did not back, without saying which. It now keeps every claim and lists the ones nothing backs in a newunsupported_claimsfield, each with the exact text from your draft, why nothing backs it, what happened to it in the returned message (kept,removedorrewrite_rejected) and what you can do about it: add it tocontext.factsif it is true, add evidence if it has a source, or rephrase it.unsupported_claimsis[]when claims were checked and none was found, andnullwhen they were not checked, as in a workspace with no connected conversations. While a listed claim is still in the message,summaryno longer says the message is ready to send. - New `on_unsupported` field on
messages.optimizeand the MCPmessagestool:flag(the default) keeps those claims,removelets the rewrite soften or cut them. Pass `"on_unsupported": "remove"` for the old behaviour. Any other value is refused with400 invalid_input. Every result echoes the mode it ran ason_unsupported. The grade itself is unchanged: a claim nothing backs still fails grounding, andliftis measured as before. - When a search ask is past what one search can answer, its notes and
retry_guidancenow point atresearchinstead of a Chat. - The
context/onboard_me,context/state_of_business,settings/warm_workspace,system/account_prep,system/amdahl_gtm_playbook,system/guide_meandsystem/research_playbookprompts now orient on the full company profile (ICP, competitors, content themes and industry keywords) instead of a summary that held only a few counts from it, and describe only what the profile holds. On MCP, each prompt says to ask a Chat for the profile (agents { action: "start_chat", input: ... }).context/onboard_meandsystem/guide_menow also read your connected sources, socontext/onboard_meneeds theconnections:readscope as well. Every MCP key bundle, and the OAuth grant, carries it. settings/warm_workspaceno longer says author voice and brand voice summaries are warming (only company profile discovery is), and no longer tells the reader to read the profile again until discovery finishes.- The setup checklist says when it could not check.
GET /console/setup/statusnow returnsdone: nullfor an item it could not compute, instead offalse, andcompleted_countcounts only items that are done. - Routine schedules fire on their cron again. The 2026-08-15 entry said Routine schedules were switched off along with Workflow schedules. Routines were separated from that switch on 2026-08-17 and have fired on schedule since. The schedules disabled on 2026-08-15 were left disabled, so a Routine you had before then stays off until you enable it again with
PATCH /routines/:idand"enabled": true. - A filter that names a field that does not exist now tells you which field you probably meant.
search.queryused to answer an unknown filter field by listing every filterable field on the surface — 63 names and over a thousand characters oninteractions, with no indication of which one was close to what you typed. It now names the closest matches (compnay_name→company_name;company→ the fourcompany_*fields;time→timestampand its siblings) and points atsearch.fieldsfor the full catalog. When nothing is close it says so rather than guessing, and shows a handful of examples instead. The structured error carriesclosest_fieldsandfield_countalongside the existingfieldandsurface. - Self-serve workspace creation is back on. Any signed-in user can create a workspace with
POST /workspaces(workspaces.create) or from the console, andGET /workspaces/minenow reportscan_create_workspace: true. Each owner can hold up to 20 workspaces. Joining an existing workspace (domain self-join, admin add) is unchanged. - Stage velocity questions now start from the funnel's median days per stage. The data schema and the SQL writer used to say per-stage time had no one-query answer and fell back to overall cycle time; they now read the per-pipeline median (completed stays only, so a stage where deals stall is shown beside its current deal count) and keep the won-vs-lost split as a second query.
Removed
- Removed the unused social listening features: listening boards and sources, Reddit post ingestion, and post-based author "voice" profiles (including the
voice_corpussearch target and voice regeneration). Existing accounts, outbound, connectors, and interaction data are unaffected. - The console's Ask AI and
searchin fuzzy mode now write queries over conversations and CRM deals only. An ask whose only source is another table (competitor mentions, deal qualification scores, deal stage history, pipeline rollups) comes back as a short refusal instead of a query on that table. Read those tables withdata.query, or open a Chat. - The Simulate page is gone from the docs. It described a planned
POST /simulateendpoint and asimulateMCP tool that were never built, and agents reading the docs treated its request schema as callable. Nothing was removed from the API, because nothing had shipped. The Message Optimizer page now sits under "Preview" and says plainly thatmessages.optimizeis not callable with an API key or an external OAuth token. - Removed the
external_search_jobs.read_streamoperation (external_search_job://resource) and theGET /api/platform/v1/external-search-jobs/:jobId/streamevent stream.external_search.executenever acceptedsubscribe_to_events, so no call could start a streamed job and the stream had nothing to carry; every action already returns its full result in one response. - Google Calendar is no longer offered as a connection: it is hidden from the connector catalog, and
connections.connectandconnections.reconnectrefuse it, so calendar subscriptions have no source to fire from. - MCP sessions no longer append the learned data-access rules to the server instructions. Every rule taught how to write SQL for
data.query, an operation an MCP client cannot call, and some clients copy the server instructions onto each tool's description, so the block was repeated once per tool. The rules still apply where the SQL is written:searchcomposes its SQL on the server with them, so answers are unchanged. - MCP sessions no longer add a "Workspace canon" list of starred artifacts to the server instructions. The list told the client to open each item through a
resourcestool and anartifact://address, neither of which MCP serves, and it had no size limit, which matters because some clients copy the server instructions onto every tool's description. No workspace had a starred artifact, so no session carried the list and no answer changes. - The
invalidate_on_eventrefresh policy formetricartifacts is removed. It promised to clear a cached value when a matching artifact changed, but nothing ever cleared it, so a metric using it kept its first value indefinitely. Creating a metric with it now fails validation; useon_readfor a value that must never be stale, orcache_5m/cache_1h. - Removed the artifact write operations (
artifacts.create,update,create_child,delete,archive,unarchiveandrevert_to_version) and the four MCP prompts that saved through them:artifact/draft_weekly_report,artifact/compose_campaign,artifact/embed_artifactandartifact/refine_content_piece. They were switched off for customers and held by no agent or MCP tool, so nothing could run them. Reads (artifact://) are unchanged. For a weekly summary usedata/weekly_pulse, and for a deliverable start a Chat withagents { action: "start_chat" }. The Content Calendar page template no longer has a "New piece" button, which could not create a piece. - The legacy specialist agents are gone: the Copilot, Researcher and Content Writer agent profiles, along with the
agents.start,context.askandagent_profile.listoperations behind them (POST /agents/run,POST /context/askandGET /agent-profiles). None had run since July, none was on the public API, and Chat replaced them. The Researcher agent in the agent library is a different agent and is unchanged. Delegation inside a Chat goes throughagents.delegate. - Long-term memory (workspace context entries) is gone. That removes the console's Business context list, the
/api/context-entriesendpoints, thememory.write,context_entries.writeandcontext.review_kitoperations, thecontext_entry://reads, thecontext_entries:read/context_entries:writescopes from new API keys, and thecontext/save_insight,context/update_entry,context/review_contextandcontext/capture_competitive_intelprompts. Agents no longer save findings between runs. Existing keys that still list the old scopes keep working; the scopes simply grant nothing. - The following endpoints are removed; each now 404s. None of them was part of the public API — the 2026-07-23 note said an API key or OAuth token reaches only the public surface, and the 2026-09-26 note ("Every remaining console endpoint now requires a signed-in console session") already narrowed every one of these to a signed-in console session, so no API key or OAuth token could ever call them. They are removed because the Amdahl console and desktop app do not call them either.
POST,GET /sessionsandGET,DELETE /sessions/{id}(substrate sessions).POST /data/queryis unaffected.POST /data/searchandPOST /data/clusters/search.POST /data/exploreandPOST /data/queryare unaffected.GET /context/summary,GET /context/substrate,POST /context/substrate/query,POST /context/askandPOST /context/remember.GET,POST,PATCH,DELETE /context/entries[/{id}].POST /stars/{scope}/{id}/star,/unstarandGET /stars.POST /library/uploadandPOST /library/classify.GET /agents/{id}/stream. The Chat turn stream (GET /conversations/{id}/turns/{turn_id}/stream,stream_urlon a chat run) is unaffected and is the one to use.POST /console/field-overridesandGET /console/field-overrides. Resolving a review-queue task with a field override is unaffected.GET /console/metrics/link-rate.POST /console/issues/{id}/reopen. Reopening an issue is unaffected viaPOST /console/issues/bulkwith"action": "reopen".GET,POST /console/projects/{projectId}/interactionsandDELETE /console/projects/{projectId}/interactions/{interactionId}.GET /console/projectsis unaffected.GET /console/computer/help-requests.GET /console/computer/statusand/computer/sessions/{id}are unaffected, as is resolving a computer's paused run (POST /console/computer/help-requests/{id}/resolveand/secret).- Workflows and living documents are retired. Workflow recipes, the living GTM documents they refreshed (including the GTM Health Report and the reports a new workspace used to receive), and backtests are gone. For recurring work, use a Routine: a scheduled Chat whose answer lands in that Chat.
- The knowledge base is retired. Knowledge-base documents, their versions and promotion, knowledge-base search and chat, and the three knowledge-base MCP prompts are gone.
- Notion Knowledge Sync is retired. It is no longer in the connector catalog. The Notion connector that brings your Notion pages in is unaffected.
- Starring artifacts is retired. The
artifacts.starandartifacts.unstaroperations are removed, andartifact://listno longer filters onis_starred(orstarred): a request that still passes it gets the unfiltered list. No workspace had a starred artifact, so no listing changes. External search, which put starred content pillars first when it adds a workspace's pillars to a query, now takes the most recently updated ones, the order every workspace already got.
Fixed
- Fixed a race in connecting a single-instance integration (CRM, call recorder, etc.) where two overlapping connect attempts could leave a duplicate, never-credentialed connection sitting next to the active one instead of being refused.
messages.optimizeworks again. The optimizer service began requiring a credential and this caller sent none, so every call returned a typed failure instead of a rewrite.- Corrected the advertised latency for
messages.optimizefrom ~35s to ~90s (median; up to ~2 minutes), so clients budgeting off the operation catalog no longer time out on runs that were still going to succeed. messages.optimizecountscontextentries after dropping blank ones, as the optimizer does, so 20 rules plus a blank one is accepted instead of refused with400 invalid_input.lift.returned: "draft"is now documented as also covering a rewrite that broke one of yourruleswhen re-checked, not only one that did not score higher than your draft.- The MCP
messagestool no longer says a call takes around 30 seconds; it takes about 90, and up to about 2 minutes. external_search.executeno longer advertisesmax_items(it was never applied).include_domains,exclude_domains,sourcesand the cache knobs now take effect where the action supports them, and the parameter descriptions say which actions those are.- A query that ends in its own
LIMITsmaller than the row cap (for example "top 10 accounts by calls") now returns exactly that many rows. Previously the limit was widened to the cap, so a top-10 answer came back with dozens of rows and a "capped at your row limit" notice. A largerLIMITis still clamped to the cap. - Fixed the
deal_stage_history"where deals die" example ondata://schema, which thedata.querygate rejected (aQUALIFYplaced directly afterFROM). It now runs as written. - The
account_type_normalizedfield description (served onsearch fields/search_field://listanddata://schema) now tells a search client how to get the customer-voice rule: passaudience: "customer_voice"onsearch, with modefilteror modesemanticplushydrate: true(the fuzzy lane refuses it). It used to say the rule needed a SQL lane, which MCP clients do not have. Callers that write their own SQL still use the predicate in the description. - Aircall connections work. Aircall's API is HTTP Basic and needs both an API ID and an API token, but the connect flow only ever collected and stored one value — so the connection reported itself connected while every request to Aircall was rejected, and nothing surfaced it. Connecting or reconnecting Aircall now asks for both halves and stores them under the names its sync reads. An existing Aircall connection needs one reconnect with both values to start syncing.
- Codex reaches for Amdahl's tools sooner. The MCP server's instructions are much shorter, each tool's description now opens with what it covers (customer calls, CRM deals, outbound drafts), and Connect your agent has a Codex setup that waits for Amdahl to connect before the first answer (
required = true). - Ask AI in the Data Explorer, and the fast lane of
search, now answer questions that need a two-step calculation in one query: the median number of calls per account, how many accounts had more than five calls, each account's first-call month, or the share of accounts that did one thing and also another. The SQL writer was still being told the query checker rejects subqueries, which stopped being true in July, so it refused many of these or returned one row per account for you to finish the math. On a benchmark of such questions it answered about one in ten correctly before this change and nearly all of them after it. - The query guidance Amdahl's agents read for theme lifecycle now takes each theme's latest record before setting disappeared themes aside, so a theme that has disappeared is no longer listed as live.
- Asking a human for help on the workspace computer works reliably now, and the notification is actionable. A
computer.request_helpcall previously errored the run with a validation failure instead of pausing (the stored expiry timestamp came back from the database in a form the pause schema rejected), and the help email could arrive with no link to the paused browser at all. The expiry now round-trips correctly, and when the agent supplies no link the server derives one itself: the live screen of the paused browser where the computer's deployment supports it, and otherwise the computer's connection page. While the computer is connected and healthy, the email and the pause card carry one of these links. - A computer help request now waits for a person. A run paused on
computer.request_helpused to have its computer session reclaimed about five minutes after it asked, which dropped the request long before its 45-minute expiry and left the run paused with nothing able to resume it. The session is now held for as long as the request is open, and it shows aswaiting_humanincomputer://statuswhile it waits. If nobody helps before the request expires, the run resumes on its own with a "help not provided" outcome and carries on without that source, instead of staying paused. computer.request_helpnow refuses a session that is missing or already finished with asession_not_activeerror, so the agent claims a live session instead of pausing on a request nobody can act on.- When the help email cannot link the live screen, it now labels its fallback link as the computer's connection page rather than promising the live screen.
- Opening a browser tab through
computer.act(open_tab) reported success but never recorded the tab lease, so every subsequent tab-scoped act on the new tab was refused as "not leased by this session" - a dead end with no recovery. The dispatcher read the opened tab's id from response keys the remote deployment never sends; it now reads the remote contract'stabId(keeping the legacy shapes as fallbacks), and an open whose id genuinely cannot be read is a loud refusal instead of a silently unusable tab. - Claiming a computer session (
computer.sessionclaim/start_watch) failed for every caller with a database error (invalid input syntax for type uuid) because the claim wrote a placeholder connection id. The claim now resolves the workspace's real computer connection: an unhealthy computer still claims (health gates at the first act, as documented), and a workspace with no computer connection gets a typed refusal result instead of an error. - Reading a single connection (
GET /connections/:id, the equivalent MCPconnectionstool read, andread_resourceonconnection://<id>) no longer reports "not found" when the read itself fails - a database hiccup used to look identical to the connection not existing. A failed read now answers with a retryable server error instead; a genuinely missing or cross-tenant id still answers 404, unchanged. - Console access settings now apply only to the area they name. A member whose access excluded Data was refused across the whole console, including the home checklist and onboarding, not just on the Data pages. Excluding Review did the same to most pages outside Review. So did turning the Data or Review app off for a workspace, for every member including admins. Data and Review access now gate only the Data and Review endpoints.
- Three SQL templates in the author-blessed prompts were wrong in a silent way:
data/top_companiesandartifact/draft_weekly_reporthanded the agent Postgres-stylenow() - interval '7 days', which the query gate refuses outright, bound on ingest time rather than event time, and in one case grouped on a column the interactions surface does not have. All three now matchdata/weekly_pulse. Two new checks run on every commit so neither class can come back: a prompt may only name operations the registry serves, and any SQL a prompt hands you must pass the same gate your agent will hit. - The customer-voice filter (
audience: "customer_voice"onsearch) and the Voice of Customer, Persona Voice Deep-Dive, Cross-Persona Divergence and GTM Proof & ROI Library page templates now decide whether a conversation belongs to a deal from the deal your CRM attached to it. Before, they read the workspace's stored stage mapping, which keeps its value after a deal is detached and stays empty on some attached conversations, so a conversation with no deal could still count as customer voice on a partner or other non-sales account. On the workspaces measured, this moves a handful of rows. A page already created from one of these templates keeps its original queries; create a new page from the template to get the change. - The
account_type_normalizedfield description no longer says that dropping the deal-attachment half of the customer-voice rule loses about 18% of the excluded rows, a figure from one workspace. Measured across workspaces on 2026-10-01, that half rescued 2% to 58% of the rows an account-type exclusion alone removes. - An unscored deal no longer reports as your worst one.
deal_qualificationrows on a no-LLM workspace carry no coverage at all by design: the warehouse leavescoverage_score,coverage_band,risk_labelanddimensionsNULL and recordsscore_source: "structural"to say so. The deal-health reader defaulted those NULLs to0,"long-shot"and"high"instead, so an unscored deal and a genuinely badly-qualified one arrived as the same numbers, and an entire structural workspace rendered as a book of scored long-shots. The four fields now come backnullwhen no rubric ran, andscore_sourceships alongside them so you can tell which case you are looking at.0and"long-shot"now mean a rubric ran and found little, which is what they always claimed to mean. - The
deal_stage_categoryanddeal_stage_normalizeddescriptions oninteractions(search.fieldsreturns them, and the fast search lane writes its SQL from them) now say what the values mean. A category is the funnel bucket your CRM mapping gives each stage, not an order through the pipeline: a BOFU stage can come before a MOFU one, so a question about late-stage deals could be answered from the wrong stages. The descriptions now point todeal_stage_position,deal_stage_probabilityanddeal_stage_labelfor how far along a deal is, and todeal_stage_statusfor won and lost. They also say that the stored bucket can trail a deal that recently changed stage. - Two deal-stage fields on
interactionsnow carry descriptions that match the data (search.fieldsreturns them, and the fast search lane writes its SQL from them).deal_stage_positionis a fraction from 0 to 1, where the pipeline's last stage is 1, but its description said only "lower = earlier", so a question about late-stage deals could come back asdeal_stage_position >= 3, a filter that matches nothing. Its description now opens with the range and warns that closed and dead-end stages share the same order, often at the top.deal_stage_probabilitysuggested summingdeal_amount * deal_stage_probabilityinto a stage-weighted pipeline, but a deal's amount repeats on every row of its conversations, so that sum counts each deal once per row and misses deals with no conversations. Its description now says not to weightdeal_amountby it on this surface. - Three fields on the
dealssurface now describe what they hold (search.fieldsreturns them);deal_stageno longer sends callers to the raw stage id when the label is empty.pipeline_namewas described as the pipeline's name, but on HubSpot it holds the pipeline's id (defaultor a number), and it is empty on every Salesforce, Pipedrive and Attio deal, so grouping by it on those CRMs lumped all pipelines together. The description now says so and points toref_deal_stagesfor pipeline names.deal_stage_labelwas described as filled on HubSpot, Salesforce and Pipedrive deals and empty only for a workspace with no stage list; it is also empty on a deal with no stage, and on a deal whose stage is not in the list synced from the CRM (for example a stage deleted from the CRM). Both cases are now stated. The same descriptions no longer use em dashes. - The
dealsguidance indata://schemano longer presentsSUM(deal_amount)as ACV;deal_amountis the CRM's amount field, not annual contract value, so ACV questions now go todeal_acvordeal_arr. Funnel examples that ordered stages bydeal_stage_positionnow order within each pipeline, because positions are scaled per pipeline: the same stage name can sit at 0.5 in one pipeline and 0.57 in another, so a single sort across pipelines interleaved them. - Two stage fields on the
dealssurface now carry descriptions that match the data (search.fieldsreturns them).deal_stage_probabilitysuggested summingdeal_amount * deal_stage_probabilityover every deal for a stage-weighted pipeline, but closed deals keep their stage's probability (1.0 on most won stages), so that sum counted won deals as pipeline. Its description now limits the sum to open deals outside the qualification band, within onedeal_amount_unit.deal_stage_positionsaid some Salesforce workspaces carry no stage order; Salesforce deals do carry it, and the CRM whose stages carry no order is Attio. Its description now also says the first stage sits just above 0 on Salesforce and Pipedrive, which number stages from 1. - When an agent retries the
email_memberaction with the sameidempotency_keyafter an attempt that reached nobody, the retry now sends. Before, an attempt whose every recipient failed (an email provider error, for example) still claimed the key, so every retry with it returned that failure without sending anything. A key is now claimed only once an email goes out, so a repeat still never sends the same email twice. - External search no longer feeds soft-deleted or archived content pillars and research reports into the query rewrite it builds from a workspace's own content. No workspace had any, so no result changes today.
- An eval that was asked to scope to one account and could not now says so on the report. It previously fell back to grading your whole corpus and reported full confidence, which read as a normal run. Accented account names are also told the spelling that would scope.
- Grounding scores no longer count your own reps' words as customer evidence. The grader behind evidence-judged evals draws its quotes from your conversation themes, and about a third of those quotes are your own team speaking (deliberately, so a draft can see what has already been said). Each quote already recorded whose voice it was, and the grading prompt was dropping that before the model read it, then describing the whole set as customer quotes. Quotes now reach the judge tagged with whose voice they are, with the instruction that a seller-side quote is context and never proof of what the customer thinks. Expect grounding to read more strictly on drafts that were echoing your own positioning back.
- A long eval stage no longer looks like a hung one. Every stage of a graded run already bounded itself with a declared wall-clock budget, and every one of those budgets was kept private, so a caller watching a live run saw a stage name and then silence. On the generate stage that silence can legitimately run to seven minutes, which is indistinguishable from a crash if all you have is the name. The stage-start step on
eval_run://<id>now carriesdeadline_at, the instant the stage stops being allowed to run, so a watcher can render the whole interval instead of a spinner. It is absent on a stage that declares no budget, and on one reusing a pinned prior run, which returns at once and would otherwise announce a wait it has no intention of taking. - A report's summary paragraph no longer names a rubric line the eval itself declared inapplicable. A line excluded by the caller's
artifact_type— website copy graded against "is the offer positioned for this customer's specific situation", which nothing addressed to a market can meet — is annotation-only, so it keeps a failing score. Every other surface on the page already dropped it: the findings list,dimensions_failedon the gate block, and the row that renders itn/a. The summary did not, and sorted it to the front, so one screen said "the gap is Relevant positioning" and "n/a" about the same line. It is dropped there too now; when it was the only low-scoring line, no gap sentence renders at all. Scores, pass verdicts and the headline are unchanged — this moves what the paragraph SAYS, not what the run measured. Applies to runs graded from here on; the paragraph is written once and stored, so existing reports read as they did. - The
research_customerandresearch_competitorprompts, and the GTM playbook, no longer tell your agent to passdeadline_ms,user_queryorprior_messagestoexternal_search.execute. The operation never accepted them and dropped them without an error, and it caps its own fan-out deadline, so a longerdeadline_mscould not have helped.research_competitoralso stops passing asourceslist on itssearchcall, which the operation ignores for that action. - The playbook's note that a
partialresult is normal and that itspending_sourcesarrive on the next call is replaced with whatpartialmeans: a source missed its time budget or a synthesis pane came back incomplete.incomplete_sourcesnames the sources andsource_errorssays why. The response has nopending_sourcesfield. - Field descriptions on the
interactionsanddeal_qualificationsurfaces no longer use em dashes or en dashes (search.fieldsand the schema read return them). They now use plain hyphens, matching thedealssurface cleaned up earlier. The wording and facts are unchanged. POST /search/queryfuzzy answers (and the MCPsearchtool's) now describe the run that produced them, in three places where thecorpusblock and the run disagreed. A query that failed after its repair round used to reportempty_reason: "no_match", the one empty that means "your corpus holds nothing about this"; it now reportsread_failed, and a writer that declined the ask reports a newunsupportedvalue because no query ran.corpus.truncatednow comes from the run's own cap check instead of comparing the row count to yourlimit, so a complete result that lands exactly on the cap is no longer reported as cut, and it agrees withcoverage.basis. And a collected async job now carriescorpus.speaker_grainand its rate counters, as a synchronous call already did. Code that branches onempty_reasonshould treatunsupportedas "nothing was asked of your data", like any other value it does not recognise.GET /agents/:id(and theagentsMCP tool'sget_agentaction) now returns a 404not_foundfor another workspace's agent or a missing id, matching every other by-id read. It previously returned a 200 withagent: nullinstead.system/getting_startednow pointsevalsat checking each claim in a draft against what your customers said, the part only an eval does. It used to say "grade a draft", which no longer tells it apart frommessages.- Joining a search result back to the warehouse on
interaction_idnow fails loudly instead of quietly returning nothing. That column name means two different things: on theinteractionssurface it is TURN grain (one grouped utterance, a 32-character hex id), whilesearch.query's semantic and lexical lanes return the parent CALL id under the same name. Joining one to the other matched zero rows and raised no error, so an empty result read as "this call is not in your corpus".data.querynow refuses a call-shaped id compared tointeraction_idand namesparent_interaction_idas the column that does match — the lanes already emit it for exactly this join. The refusal is structural rather than a guess: an all-digit literal that is not 32 characters cannot be a turn id, so the comparison was empty by construction. The column's own catalog entry now states its grain too, instead of leaving that on a neighbouring column. - The interaction schema's timestamp fields describe what they actually hold. The schema your agents read called
updated_at"when this record was last updated" andcreated_at"when this record was created", so an agent asked what changed in your CRM recently filtered on our pipeline's own write clock and got backfill times instead of source activity.updated_atis the row's version key: it moves whenever any pipeline job touches the row, and on Salesforce and Gong it has never carried a source timestamp at all.created_atfalls back to the conversation time whenever the source gives us none, which is every Salesforce and Gong activity, so on those connections the two columns are the same value. Both descriptions now say this, and send recency questions totimestamp, which is when the conversation happened. system/guide_meandsystem/amdahl_gtm_playbooktold your MCP client to call tools the MCP server does not have. The guided walkthrough, which the server instructions offer every new user, ran its menu choices throughdata query,data cluster_search,context summary,external_search, and adata exploreoperation that does not exist, so the first step of most choices failed. The GTM playbook sent research, enrichment, account history and content drafting to the same missing tools. Both prompts now give two routes for every step: the call your MCP client makes (asearchquery, aconnectionsread, a Routine throughagents, or a Chat started withagents { action: "start_chat", input: ... }that runs the work for you), and the operation an in-app Amdahl agent runs directly.- The GTM playbook has a new row for improving an outbound draft you already have.
messages { action: "optimize", message: ... }rewrites it in the sender's own voice and returns it on the same call. It needs no connected data, and it needs themessages:executescope, which Customer agent keys carry. system/account_prep,system/research_customerandsystem/research_playbookwrote some operation names without their dot (data queryfordata.query), which reads as a call on adatatool. They now write the operation names in full.- A new check runs on every commit: a call a prompt writes as
tool action=x, or astool actionwhere the pair spells an operation, must be a call the MCP server serves. - MCP prompts now cite resource URIs in the spelling MCP publishes.
getting_started,guide_meandschema_tourtold MCP clients to readsearch_overview://current,search_field://list,eval_run://andgrader_kind://; an underscore is illegal in a URI scheme, so a standards-compliant client refused to parse them. They now readsearch-overview://,search-field://,eval-run://andgrader-kind://, matchingresources/list. - Seven MCP prompt descriptions in
prompts/list(data/weekly_pulse,data/top_companies,data/account_deep_dive,artifact/draft_weekly_report,artifact/refine_content_piece,context/remember_this,settings/warm_workspace) named in-app operations an MCP client cannot call, such asdata.queryandcontext.summary. They now describe the plan without naming those operations. What the prompts return is unchanged, and still tells an MCP client to hand the plan to a Chat. - Nine of the author-blessed prompts told your MCP client to run steps it cannot.
system/account_prep,system/research_customer,system/messaging_check,data/account_deep_dive,data/top_companies,context/remember_this,artifact/compose_campaign,artifact/draft_weekly_reportandartifact/embed_artifactsaid "Calldata.query", "Callcontext.summary", "Callartifacts.create" and the like, and said nothing about MCP. Those are operations an in-app Amdahl agent holds, not tools on the MCP server (agents,connections,evals,messagesandsearch), so a client that fetched one withprompts/getand followed its first step got a tool-not-found error. The nine now open with the hand-off a dozen sibling prompts already carry: on MCP, start a Chat withagents { action: "start_chat", input: ... }and poll it for the result, and the agent inside the Chat runs the steps. An in-app agent still runs them directly.context/remember_thisalso stopped telling clients they could callcontext.rememberas an MCP tool. - A new check runs on every commit: a prompt that names an operation the MCP server does not carry must also give the MCP route, either a call on a live tool or a Chat hand-off.
- Fourteen of the author-blessed prompts told your MCP client to read a resource URI it structurally cannot reach. They named
context://,data://andartifact://(or, in the two conversation prompts, the legacyconversation://) and said "on MCP use resources/read" — true on the Anthropic surface, false on MCP, where nativeresources/*only serves ten allowlisted schemes (chat,routine,subscription,agent,search_field,search_overview,eval,eval_run,grader_kind,connection). Following one of those recipes on MCP hit a clean 404 partway through.conversation/conversation_recapandconversation/resume_worknow readchat://<id>instead — the same conversation, on a scheme MCP does serve. The rest (onboarding, weekly pulse, content refinement, win/loss and positioning research) now tell MCP callers to hand the whole task to a Chat (agents { action: "start_chat" }) instead of attempting steps that call operations no MCP client can invoke directly. A new check now runs on every commit: a prompt that names a resource scheme off the MCP allowlist must say so. - Eleven of the author-blessed prompts told your MCP client to call tools that are not there. They named
data,context, andartifacts— coarse tools that were retired when the surface narrowed toagents,search,evals, andconnections— so following one of those recipes step by step hit a tool-not-found error partway through. Every prompt now states its routing per capability: which reads you can do directly, which questions thesearchtool answers in one call, and which work you hand to a Chat because the operation lives on the agent rather than on your client.system/getting_startedhad the most to correct: it opened by describing three tools that no longer exist and pointed at resource schemes that are not exposed, and it now describes the live tools and the schemes you can actually browse. The three knowledge-base prompts are gone rather than fixed: they left with the knowledge base itself. Two more silent breakages went with them — four prompts named adata exploreoperation that does not exist (the read isdata://schema), and the two account-briefing prompts selected columns the interactions surface has never had, five of eight in one case, so neither could return a row. Four checks now run on every commit: a prompt may only name live operations, its SQL must pass the same gate your agent hits, every column that SQL names must exist, and a tool named in call position must be one that is really there. - The MCP prompts now describe all five tools.
system/getting_startedsaid Amdahl exposes 4 tools and had nomessagesrow in its tool map, andsystem/research_playbookand the delegate-to-Chat notice a dozen prompts carry listed the live tools asagents,connections,evalsandsearch. An agent that learned the tool set from a prompt never learned it could rewrite a draft. The getting-started prompt now has amessagesrow. Its one action,optimize, needs no connected data; it does need themessages:executescope, which Customer agent keys carry. - The delegate-to-Chat notice told your client to start a Chat with
agents { action: "start_chat", message: ... }and to pollchat_statuswith only achat_id.start_chattakes the ask asinputandchat_statusalso requiresrun_id, so a client that copied either call was refused on its first try. The notice now spells both calls with the params they take, andsystem/research_playbookstarts its Chat withinput. - A new check runs on every commit: a prompt that lists, maps or counts the MCP tools must match the tools the server registers, and a call a prompt spells out must name an action the tool serves and pass the params that action requires.
- Connecting an MCP client (Claude, Cursor, or your own agent) no longer asks you to sign in over and over. A token refresh that gets retried — a dropped response, or two client processes refreshing at once — now succeeds instead of invalidating the connection and forcing the whole authorization flow again. Reconnecting in a browser you have signed in from before skips the email code entirely, and a browser already signed into the console is handed straight to the authorize screen with no login at all.
- MCP API keys created without an explicit
scopeslist work again. A key minted that way was issued with["read"], and every call to a coarse tool (agents,search,evals,connections) answeredInvalid scope format: "read"instead of running. That message named a value and a grammar but never the credential, so it read as the product being broken rather than as a key to reissue, and the same key kept authorizing normally over the REST API and over MCPresources/*. The coarse tools now resolve a key's stored scopes before checking them, the way every other surface already did, so such a key got the permissions its role allows. This grants nothing new: a key that genuinely lacks a scope is still refused, now with a message naming the scope it needs. - The OpenAPI spec now lists the OAuth2 scopes the documented endpoints check, such as
conversations:writeto start a chat andevals:executeto run an eval. The list used to name 16 scopes, includingwebhooks:readandwebhooks:write, and onlydata:readapplied to any documented endpoint. The OAuth2 authorization and token URLs now point at the OAuth server (/oauth/authorizeand/oauth/token, as in the authentication guide) rather than paths that do not exist. The spec also drops 20 component schemas that no documented endpoint used, among them theWebhookfamily, and its emptywebhookssection. No endpoint behaves differently: this corrects the spec published atdocs.amdahl.ai/openapi.jsonand/api/platform/v1/openapi.json, and any client generated from it. messages.optimizeno longer offerswhatsappas achannel. The optimizer scores onlyemailandlinkedin, so a WhatsApp draft was sent anyway and came back asok: falsewithreason: "optimizer_error", which read as an outage rather than a bad input. Any other channel is now refused up front with400 invalid_inputnaming the channels it accepts, and the MCPmessagestool lists onlyemailandlinkedintoo.- The
channelfield no longer saysemailis scored with a reply-probability model, which the optimizer no longer uses. The channel sets the length and format the message is judged against. - Research reports from the Deep research workflow and the researcher agent now pass the
research_reportschema. Both writers left out required fields: neither setschema_versionordepth, and the researcher also skippedtopic. The researcher's saves got through only because a validation bypass waved them past the schema, and the workflow's save would have been refused. Both now write all three fields, withdepthtranslated from the run'squick/standard/highsetting to the report'sshallow/standard/deep, and the bypass is gone, so everyresearch_reportis validated when it is saved. - A finished chat answer stuck in a re-presentation loop is now cut off even when each repeat rewords the text. The runner's repetition guard previously fired only on byte-identical repeats, so a run that paraphrased its wrap-up each iteration could post the same answer a dozen-plus times until its turn limit. The guard now also detects reworded re-presentations (content-vocabulary overlap with the previous answer) and caps consecutive iterations that do no real tool work; iterations making real tool calls are never terminated, so legitimate progress updates across polling turns are unaffected.
- `search.query` (mode `fuzzy`) no longer answers a question with a curated list that ignores part of it. "Pricing objections in the enterprise segment", "objections in 2024", "champions at acme", "pain points by industry", "objections from my calls", "how many calls mentioned a competitor" and "win rate against each competitor in the last 30 days" matched a list by its subject and came back as the whole workspace's rows, reported as complete. A question that carries a topic, segment, grouping, record type, owner, outcome, lower-case name, second question, or a time window the list cannot apply now goes to the SQL writer, which applies it.
- "Last month", "last quarter", "last week" and "last year" now mean the previous calendar period in the curated lists. They used to start at the beginning of the current month or quarter, so on the 1st "last month" returned one day of data from the new month. "Last 3 months" and "past year" count back by the calendar, "last two weeks" and "past six months" are read, and "yesterday" and "this week" are exact. A question that names two windows ("this quarter versus last quarter") or puts one beside a word that reverses it ("other than last month") goes to the SQL writer instead of answering half of it.
- "Top 5" and "give me 10" now cap the rows a curated list returns.
- "Which competitors do we lose to" is answered by the SQL writer, not the win-rate list. The list sorts the best win rate first, so it returned the rivals we beat most under a question about the ones we lose to.
POST /search/queryfuzzy answers (and the MCPsearchtool's) now say which data they actually read.corpus.surfacealways saidinteractions, but the lane also answers from other surfaces: a question about average won-deal size ran againstdealsand came back labelled as conversation data, withspeaker_scope: "all".corpus.surfacenow names the surface the query that ran actually read, so it can carry any surface the lane queries, not only the three the filter lane accepts assurfaceinput.speaker_scopeis omitted on a surface with no speaker column, andcoveragereportsbasis: "unavailable"(no_time_field) on any surface other thaninteractionsrather than dating its rows as conversations. A themes answer read from the theme index now reportsstore: "theme_index"andgrain: "theme"instead of a warehouse row. Applies to synchronous calls and to collected async jobs.- `search.query` (mode `fuzzy`) answers the question you asked, not a reworded one. The step that splits a question into parts used to rewrite it before the SQL was written, and the rewrite could change what got counted. "How many deals are in each stage?" was answered with a count of conversations per stage, not deals. "What's our win rate by pipeline?" was answered with a share of positive-tone remarks by deal stage. Both were reported as covering the whole question. A question that needs no split now reaches the SQL writer word for word, and each part of a split may only reuse your own words. When a split would change the wording, it is dropped, the whole question runs as one query, and
uncoveredcarries aplanner_rewroteentry withretry_guidanceto ask the parts separately. - `search.query` (mode `fuzzy`) no longer answers a figure with a list. A question asking for a total, a value, an average, a rate, a count, a trend or a won/lost cut could be matched to a curated template by its subject alone. "What is the total pipeline value of accounts that raised objections?" came back as a list of objection quotes, and "What's the average deal size of accounts with a champion?" as a list of champions, both reported as complete. A template now answers only the figures its query computes, and any other question goes to the SQL writer.
- `search.query` (mode `fuzzy`) no longer answers a question about one account with every account's rows. "What objections did Acme raise?", "Show me the pushback on the Acme deal", "Degreed pain points", "champions at Acme" and "objections from 11x" were matched to a curated list by their subject and came back as the whole workspace's rows, reported as complete. A question that names a company, person, product, segment, region or quarter (any capitalized or digit-bearing name, an email address, a domain, or text in quotes) now goes to the SQL writer, which filters on it. The champion and economic-buyer lists had no such check at all. A name typed entirely in lower case is not detected, so capitalize it or put it in quotes.
- Questions that start with "What's", "Who's" or "It's" can use the curated competitor lists again. "What's our win rate against each competitor?" was read as naming a company called "What", so it skipped the per-competitor win-rate list.
searchno longer reports a zero when your data access excludes the table an answer reads. Such a part now comes back empty, withempty_reason: scope_disjointand a note that your access does not include those records, instead of a count of 0 that the data never said.- Semantic search no longer pads an answer with distant matches. The lane asks the vector store for its nearest rows, and "nearest" carries no promise of "near" — so a question about a topic the corpus does not cover came back with a confident page of rows that were merely the least unrelated, indistinguishable from real matches. Rows below a relevance floor are now cut, and
search.queryreports the floor it used (corpus.min_relevance) plus how many rows it dropped (corpus.weak_matches_filtered). When everything came back below the floor the result carriesempty_reason: "weak_matches_only"instead of"no_match"— the two now say different things: your corpus holds nothing that matches this question, versus your corpus holds nothing about it at all. Those call for opposite next moves and used to arrive as the same empty list. - The Pipeline Health, Pipeline Funnel, Deal Size Distribution and Win Rate page templates now read whether a deal is open, won or lost from your CRM's own stage flags, and show your CRM's stage names in pipeline order. Before, they used the workspace's stage mapping, which can lag a deal's current stage, is empty on some rows, and let won and lost deals into open pipeline value. The win rate gauge now counts an account as won when any of its deals is won, so it gives the same answer on every run. A page already created from one of these templates keeps its original queries; create a new page from the template to get the change.
- The analysis playbook (
data://schemaand thesystem/amdahl_gtm_playbookprompt) now tells agents to split won from lost deals bydeal_stage_statusinstead ofdeal_stage_normalized, and thedeal_stage_statusfield description says what it cannot see: a stage your CRM does not flag closed reads open whatever it is called. - The "Unknown speakers need identification" inbox row opens a queue that can actually contain those speakers. Clicking it filtered the review queue to identity-linking tasks, which are only ever created for speakers carrying a name or an address the pipeline could not resolve. The biggest group behind the row's count — anonymous placeholder speakers like "Speaker 2" — never produces one, so the row led to a filter that structurally could not hold what it was counting (in the test workspace, 1,450 utterances matched that filter and none of them were anonymous speakers). The row now opens the queue that does carry these calls.
messages.optimizenow reportsveto_reason: "not_better"when the optimizer refuses its best rewrite on the independent yes/no re-grade. That refusal arrived under a reason name this API did not list, so the response carriedvetoed_roundwith noveto_reason. The run'snotesstill explain what the re-grade found.- A complete research job now always returns its answer in `answer.text`. A job that presented its findings and then wrote its closing answer could lose that answer, so
research.getreturnedanswer.text: ""with the findings only incontent_blocks. The closing answer is now kept. When a job writes no prose at all,answer.textlists each finding's label with its insight or value.
Security
- Deleting a workspace now revokes all of its API keys and OAuth tokens, and any credential tied to a deleted workspace is rejected with 401.
- Acting on a computer help request now requires a console session and permission to use the computer, the same as
computer.act. This coversPOST /console/computer/help-requests/:id/resolve(mark solved or skip) andPOST /console/computer/help-requests/:id/secret(hand the computer a one-time code). Anyone who can start a run that uses the computer already has the permission; console session means you're using the app, not a bare API key. Before this, a read-only console session (or, separately, a bare platform API key) could resume or skip someone else's paused run or type a value into the shared browser. Both now get a 403. Reading help requests is unchanged. - The console endpoints that change who can reach what, or that store or use a credential, now require a signed-in console session. An API key or OAuth access token sent to one is refused with 403
console_session_required. Until now a key or token acted there with every permission its user holds, whatever scopes it was granted. A token granted onlydata:readcould register a webhook (which returns its signing secret) or type a credential into a live computer session, and an admin's token could also change access grants, roles, per-page access and the workspace's enabled apps, store a credential in the computer vault, or mint a public link to a page. This covers/console/grants,/console/roles(including role members),/console/pages/{id}/grants,POST /console/pages/{id}/share,/console/computer/credentials,POST /console/computer/help-requests/{id}/secret,/webhooksandPATCH /self/config. None of them is part of the public API: the 2026-07-23 note said an API key or OAuth token reaches only the public surface, and these endpoints now keep that promise. Nothing changes in the Amdahl console, andGET /self/configand the computer status and help-request reads answer a key or token as before. - Query errors from the
data.queryandsearchsurfaces, and from token-scoped page embeds, no longer include raw warehouse-engine error text. The warehouse is one shared table scoped to each workspace by an injected tenant filter, and a database engine does not guarantee the order in which filter conditions are evaluated — so an error raised while evaluating a value (for example a failed cast or parse) could be triggered on a row outside the caller's workspace, and the engine's error message echoes the offending value. Those messages were returned to the caller largely verbatim, so a workspace with read access could craft a query that surfaced a single cell value from another workspace through the error text. Error responses are now mapped to a fixed set of actionable messages (unknown column, too-broad scan, type mismatch, timeout, and so on) that never carry raw engine text or cell values; the full error is still recorded in the server-side audit log for operators. Valid queries and their results are unaffected. - Data query rate limits now apply uniformly to every API key and OAuth token. An internal request header that the platform's own background runner used could previously change which rate-limit bucket a call counted against; it is no longer read from requests. Clients that never sent it are unaffected.
data.querynow refuses a SQL statement that calls a table function in its FROM clause, in a subquery as well as at the top level. A statement like that is refused with a message pointing at theas_ofparameter, which is how a past snapshot is read. Reading tables by name,UNNESTand derived tables work as before, andas_ofreads are unaffected.- Queries on the
data.query,searchand page-embed surfaces are now checked against the warehouse's own query planner before they run. A query that would read data outside the documented queryable tables, or that is anything other than a singleSELECT, is refused before it executes, with a fixed message that names nothing from the query. If the check itself cannot complete, the query is refused with a retry message rather than run unchecked. Refusals use the existinginvalid_sql(400) andquery_failed(500) error codes. Valid queries and their results are unchanged; uncached queries gain a short pre-flight step, typically under a second. - Excluding a conversation from your workspace's data, or re-including one, now requires a workspace admin, the same as saving and applying data filters. This covers
POSTandDELETE /console/interactions/:id/exclude, which back "Exclude this call" in the review queue and "Re-include" on the Filters page. An exclusion removes the conversation for everyone in the workspace, and any member who could open the review queue could previously make one. Other members now get 403not_admin. - MCP now applies the caller's workspace role, the same way the REST API does. Tool, resource and prompt calls on the Amdahl MCP server (
/mcp) were evaluated as if the caller were an editor, so an API key belonging to someone no longer in the workspace could still start chats, create or change routines and agents, and run evals over MCP while the REST API refused it. Operations that require a minimum role now enforce it over MCP, and when run from a page action button. - API keys and OAuth tokens held by Amdahl staff that name specific scopes are now limited to those scopes on the operation endpoints of the REST API and on MCP. Such a credential used to act with the staff member's full platform access whatever scopes it named, so an integration a staff member authorized on your workspace with a narrow scope could do more than its consent screen said. Staff access through the Amdahl console is unchanged.
- Refreshing an OAuth access token now re-checks that the user still belongs to the workspace, the same check already applied to every request on the access token itself. Before, a user removed from a workspace kept a working session alive indefinitely by refreshing, and a later re-invite silently resumed it with its pre-removal permissions. A refresh on behalf of a removed user now fails with
invalid_grantand revokes every other OAuth token that user holds for that workspace. - The
POST /oauth/tokenauthorization-code exchange is now single-use under concurrent requests. Two simultaneous exchanges of the same code could previously both succeed, each minting its own token pair; only one now succeeds, per RFC 6749. - Refreshing an OAuth access token no longer adds scopes. Until now every refresh issued the full default scope set, so a client that asked for fewer (for example only
data:read) held every default scope after its first refresh, usually about an hour after the user approved it. A refresh now keeps the scopes of the token it replaces, minus any Amdahl has retired. For a connection authorized after this release that is exactly what the user approved; a connection authorized earlier keeps the scopes it holds when this release ships. - An OAuth access token issued for a request that named no scope Amdahl offers (for example only
openid) is now rejected. Before, such a token acted with the member's full role permissions. Clients that request scopes from the advertised list are unaffected. - The MCP prompt catalog no longer lists
webhook/debug_deliveryorwebhook/webhook_health_check, andwebhooks:readis no longer granted by any MCP API key bundle or OAuth token. Webhooks are configured only through the console, and no workspace has ever registered one. Both prompts told the calling agent to read awebhook://resource, but that resource has always been hidden from MCP's nativeresources/*surface, so everyprompts/geton either one produced instructions the agent could never finish.webhooks:readwas granted to every MCP credential regardless of bundle, so removing it changes nothing an MCP client could previously do — it only stops advertising a scope and two prompts that led nowhere. A stored API key or OAuth grant that still nameswebhooks:readis unaffected by anything it could do before; an OAuth token whose entire grant waswebhooks:readand nothing else will need to re-authorize on its next refresh, since that grant no longer intersects any offered scope. - Every remaining console endpoint now requires a signed-in console session, finishing the change that started with the access and credential endpoints. An API key or OAuth access token sent to any
/console/...endpoint,GET /self/config,/sessions,/data/...,/context/...,/stars,/library/...,/meeting-sessionsorGET /agents/{id}/streamis refused with 403console_session_required. None of them is part of the public API: the 2026-07-23 note said an API key or OAuth token reaches only the public surface. Until now a key or token still acted on them with every permission its user holds, whatever scopes it was granted: a key withoutdata:readcould run SQL through/data/query, any key could create or delete memory entries, and an admin's key could star an artifact into the instructions every connected agent reads, or change CRM mappings and data filters. For warehouse questions over the API, usePOST /search/query. Nothing changes in the Amdahl console or the desktop app. - The eval export files (
GET /evals/export.csv,GET /evals/export.jsonlandGET /evals/export/count) and the Chat stream atstream_url(GET /conversations/{id}/turns/{turn_id}/stream) now check a key's scopes exactly asPOST /evals/exportandread_urldo:evals:readfor the export files,conversations:readfor the stream. A key or token without the scope gets 403forbidden. Until now any key or token could download the graded runs its user can export, and watch any turn in its workspace, whatever scopes it was granted. Keys created with the Read only or Customer agent bundle already hold both scopes. - The benchmark-only
raw_datatool on the/mcp/rawserver now requires thedata:readscope and confines every query to the workspace's own raw dataset. Before, the tool checked only that a credential was present, so a key granted some other scope (and no data access) could still run raw SQL; and its query guard inspected only references to the workspace's own dataset, so a query that also named another workspace's dataset — or the shared warehouse — was not caught and could return that data. A query is now parsed and refused if it references any table outside the workspace's own raw dataset, across joins, subqueries, CTEs, set operations, wildcards, andINFORMATION_SCHEMA, and a query that cannot be parsed is refused rather than run. Queries that read only the workspace's own tables are unaffected. - Removed a shared-secret authentication path on the platform-admin routes and the workspace CRUD / page-embed-secret routes. Alongside a valid Supabase session, both accepted a single platform-wide API key plus an
X-Amdahl-Caller-Emailheader naming any user by email, and authenticated as that user — a superadmin's email included, on the admin routes. Anyone holding the shared key could act as any named user with no per-caller credential and no way to revoke just that use without rotating the key for every legitimate caller at once. No caller of this path was found anywhere in the organization's codebases (an org-wide code search and a sweep of every repository's default branch), so it has been deleted rather than narrowed. A valid Supabase session is unaffected. data.query, and everything that runs SQL through it (thesearchtool's fuzzy mode, Page queries, the console's Data Explorer), now refuses three query shapes: a CTE that takes a table's name, a table name qualified with a dataset or project, and a CTE read from a place it is not visible (its own body, a CTE defined before it, or outside the subquery that defines it). Before, a query whose CTE shared a table's name could read that table without the workspace filter and return other workspaces' rows. Write table names bare (deals, notdataset.deals) and give each CTE a name of its own; queries written that way are unaffected.- The
/api/teamendpoints that change who belongs to a workspace, or that create or revoke its API keys, now require a signed-in session. An API key or OAuth access token sent to one is refused with 403session_required. Until now a key or token acted there with every permission its user holds, whatever scopes it was granted and whichever workspace it was issued for. A token granted onlydata:readcould create a new API key that never expires or revoke the workspace's keys, and an admin's token could also change members, invitations, domains and team settings, in any workspace its user belongs to. This covers creating and revoking API keys; changing a member's role, removing a member, adding members directly (one at a time or in bulk) and the add-member email lookup; creating, listing, resending and revoking invitations; changing team settings; and adding and removing domains. Reading the member list, team settings, domains and the API key list with a key or token works as before. This narrows the 2026-07-30 note that one API key works on the team endpoints. - An embedded page stops rendering once its workspace is deleted. Embed tokens last up to 30 days, and deleting a workspace revoked its API keys and OAuth tokens but not its embed tokens, so an embed kept running live queries against the deleted workspace's data until its token expired. The embed now answers the same "Not Found" it gives for any invalid token.
Added
- Eval runs now have a feedback loop.
POST /eval-runs/{id}/feedbackrecords what happened after a run — whether its recommendation was applied, where, and why not when it was not — andGET /eval-runs/{id}/feedbackreads it back.GET /evals/{slug}/adoptionrolls it up across runs, alongside the existing/kpi: a rising score with no adoption means callers improved on their own. - The write requires
evidence(did_it_myself|reported_to_me|inferred), because an agent cannot observe what a person did outside its session.inferredis stored but never counted toward a published rate. - Pass
applied_to(e.g.{ "kind": "agent", "id": "…" }) and the server checks the named target really contains the recommendation.verifiedis three-state:truefound,falsechecked and absent,nullnothing checked —falseandnullare different answers and are never collapsed. - Every count on the adoption read states its denominator.
adoption_ratemay benullwith anabstain_reason(thin_window,no_signal,no_eligible_runs) rather than divide a handful of rows into a percentage — an abstain is never a zero. - The Chat list (
GET /chats) now takesagent_refandroutine_id, so you can pull just the chats one agent drove or one routine fired instead of paging the whole workspace.agent_refaccepts either the agent's slug or its uuid and matches chats started with either form. - New
GET /search/overviewread (search.overview, resourcesearch_overview://current): per-surface row counts and time spans for the three queryable warehouse surfaces, the orientation read to take before composing a firstsearch.query. A surface whose read fails reportsnull, never0, so an outage is never mistaken for an empty corpus. - Agent first-prompt snippet. The quickstart and Connect your agent now carry a copyable prompt that points an AI assistant at your workspace data — read the docs, pull your history, report the top use cases, and explain rather than build. The console offers the same snippet beside the key: Copy key + prompt on the create-key dialog, and an Agent prompt tab under Connection snippets.
- AskElephant is now a connectable call recorder. Connect it in Settings -> Connections with an AskElephant API key and its meeting engagements, transcripts, contacts and users sync into your workspace alongside Gong, Fathom, Fireflies, Grain and Granola.
- Speaker attribution comes from AskElephant itself rather than being inferred: it tells us whether each speaker is one of your contacts or one of your users, so internal-vs-external is asserted by the source on every turn.
- Connecting checks the key against AskElephant before the connection is marked live, so a bad key fails at connect time instead of going quiet until the first sync.
- Connections are now a public API and MCP surface. All thirteen
connections.*operations are on the platform API — browse the connector catalog, connect a source (API key, handle, or OAuth), watch its sync runs and health, and repair it in place — and a newconnectionsMCP tool carries the six reads plus the four lifecycle writes, so an MCP-connected client can manage a workspace's data sources end to end. See the new Connections endpoint guide. - Every catalog entry is self-describing: a
logoblock (key, an absolute asseturlornull, and a two-lettermonogramfallback) and aconnectspec declaring exactly which fields the connect flow needs (with the vendor's key-shape placeholder and scope guidance where one exists). Every connected-instance read carries a matchingconnectorblock, so a client renders names and logos straight off the read with no hardcoded connector list. - Connector lifecycle writes (
connect/disconnect/update/reconnect) authorize on OAuth tokens — a signed-in member's delegated session — and deliberately not on headless API keys, which keep full read access to the catalog, connections, runs, and summaries. - A setup guide for calling Amdahl from model platforms — OpenAI's built-in tools (Playground + Responses API), LangSmith, and any client with a remote-MCP tool type: OpenAI and tool platforms. The console's Settings -> Developer page now also shows a ready-to-paste OpenAI tool config and curl snippet prefilled with your workspace's endpoints.
- Partner API (
/api/partner/v1): design partners can now provision Amdahl workspaces for their customers, list and inspect them, add members, and mint short-lived act-as sessions inside them — authenticated by a dedicated partner API key (amdhl_ptnr_...) with its own scope grammar (partner:read/partner:provision/partner:act_as). - Every partner write names a real human on the partner's team (the workspace owner on provision, the actor on member-adds and act-as), so the audit trail never attributes an action to a credential. Act-as sessions are clamped to the workspace admin role and every request under one is recorded to the platform audit trail.
- Partner orgs, members, and keys are managed by the Amdahl team (
/api/admin/partners); suspending a partner fails all of its keys closed, reversibly. Guide:/partners. - Subscriptions: event-fired Chats, the event-driven sibling of Routines. A subscription names a source kind, a prompt, and an optional agent pin; when the source observes an occurrence it fires a fresh headless Master Chat with the event rendered into the turn. First source:
calendar.event_upcoming— fire a configurable lead time before each event on a connected Google Calendar ("prep me 6 hours before every meeting"), with one subscription per lead time. Events discovered already inside their lead window fire immediately; rescheduled events re-fire for the new start; cancelled events never fire. - Full surface on REST (
/subscriptions+ the self-describing/subscriptions/kindscatalog + the per-subscription fire ledger), on the MCPagentstool (create_subscription/update_subscription/delete_subscription/test_fire_subscription+ the reads), and assubscription://resources. New scopessubscriptions:read(viewer) /subscriptions:write(editor); existing MCP keys backfilled. Guide:/subscriptions. - Google Calendar joins the connector catalog (Connections → Google Calendar, OAuth, per-member) as the calendar source's credential.
GET /chatsacceptssubscription_idto scope the Chat list to one subscription's fires, and every fired Chat carries asubscriptiontrigger badge.- Computer Use: an agent on the Master bundle can now drive a per-workspace remote browser. Connect a
computer_agentconnector (base URL + service token), which is health-probed every couple of minutes and gated on connected + fresh-healthy before any action runs. computer.actforwards a browser action through a single dispatcher that enforces tab-lease coordination (8 tabs, 1 reserved for humans) and returns a typed outcome (deferred/human_has_control/stale_snapshot/refused) rather than throwing;computer.sessionclaims/heartbeats/releases a tab and starts long-horizon watch sessions.computer.request_helppauses the run for a human when the browser hits a challenge, then auto-resumes once the human releases control; a secret handed back is forwarded straight to the browser and never logged, persisted, or echoed.- New read surfaces
computer://status/computer://sessions/computer://commands/computer://helpand the globalcomputer_rule://listplaybook expose live state and the self-improving browser-driving mechanics. - Attio connections now sync call recordings and transcripts. An Attio-only workspace gets its meetings and call transcript turns alongside the CRM objects, with no separate call recorder to connect. The workspace API key needs the
meeting:read,record_permission:read, andcall_recording:readscopes for the two new streams; a key without them keeps syncing the CRM objects while recordings stay off — re-issue the key with the extra scopes and reconnect to turn them on.
Changed
- Eval scores now count only the rubric checks your artifact could actually satisfy. If you declare an
artifact_typeand a check does not apply to it — landing copy has no single recipient, so "is this positioned for THIS customer's situation" has nothing to hold it to — that line now leaves both sides of the fraction instead of being counted as a failure.checks_totalcan therefore be smaller than the rubric, on the full report and on thegateblock a pipeline reads. - The exclusion is symmetric: an inapplicable line is dropped whether it passed or failed. Across the runs that carry one, this is net upward — but it can move an individual score either way, and a run whose excluded line had been passing scores slightly lower than before. That is the rule working, not a regression: a version that only dropped inapplicable *failures* could only ever inflate a score.
- Each run now records what it divided by, as
denominatoron thegateblock and on each graded facet —applicable_onlyorall_dimensions. Read it before trending or comparing scores. It is a per-workspace setting, soeval_versionalone does not tell you which one a run used, and runs with differentdenominatorvalues should not be pooled. Runs graded before this change carry no value; read that asall_dimensions. - Stored runs are unchanged — nothing was re-scored, and an older run still reports the fraction it was graded with.
- Turn-grain results now name their speakers from the roster the pipeline records on each turn, instead of re-deriving it from the underlying utterances at read time. Nothing about who a row is attributed to changes: the same rules decide it, from a source that was written when the turn was built rather than reconstructed when you read it. Two new fields say which source answered —
speaker_identity_sourceon each row (stamped= the pipeline's roster,overlay= the read-time reconstruction), andidentity_source/identity_stamped_rows/identity_fallback_rowson thespeaker_grainenvelope. - `identity_fallback_rows` has a floor, not a trend. Excluded interactions never carry a roster, by design and permanently, so this reaching zero tells you what your query covered — not that the reconstruction is no longer needed. It is still there and still answers for those rows.
- A speaker can legitimately have no name, and results now say how many. Some voices arrive with neither a name nor an email address and never will. Such a voice keeps its place in
turn_speakerswithspeaker_name: null— read that as an anonymous speaker, not as a lookup that failed, and not as an empty string.anonymous_voice_rowsonspeaker_graincounts the rows holding one. The testimonials people-picker cannot offer a person it cannot name, so it leaves them off the list and reportsanonymous_voice_turnsrather than dropping them silently. - This is concentrated, not spread — do not read the fleet rate as your rate. Almost all workspaces have no anonymous voices at all; one has thousands. Read the per-result count.
- Two counting bugs fixed, both only visible where anonymous voices exist: a turn holding two unnamed speakers was scored as one person on deal-health, and two unnamed speakers in different calls could cap each other out of a quote panel. Both now count as the distinct people they are.
turn_speakersis in the queryable column catalog, so a SQL lane can aggregate per PERSON —UNNEST(turn_speakers) AS s GROUP BY s.key— instead of grouping onspeaker_name, which counts turn stamps and omits anyone who never leads a turn. Prefer it overWHERE speaker_ambiguous = FALSE: that scope fixes a mislabel by dropping people, and this one drops nobody. Group ons.key, which is always present;s.speaker_nameis not.
Fixed
- Knowledge-bank people matching no longer reports "not in your conversations" when it could not check. If the lookup fails, the person now carries
lookup: "unavailable"instead of an empty match list, so a failed check is never stored as a finding of absence. - Uploading a file larger than an endpoint's limit now returns
413with a machine-readable body (error: "file_too_large",code: "LIMIT_FILE_SIZE", and the field name), instead of an opaque500 Internal Server Error. This affects every upload endpoint — the platform library upload, the knowledge bank uploads, file conversion, and context-entry attachments. A file sent under a field name an endpoint does not accept now returns400rather than500. - Upload size limits are now inclusive. A file of exactly the documented limit (25 MB on the platform library upload, 50 MB on the knowledge bank, 5 MB on context-entry attachments) is accepted; previously the largest accepted file was one byte under, so a file of exactly the limit was rejected by a different rule than the one the error message quoted.
search.query'sfuzzymode (the NL-to-SQL lane) no longer fails outright during an Anthropic outage. Its query planner and SQL writer now retry through the same OpenAI/self-hosted fallback the agent conversation loop already uses, instead of reporting a generic "could not translate that ask into a query" that mislabeled an outage as a timeout.- An Anthropic outage no longer fails the calls that run underneath ordinary work. The completion seam most of the platform shares now retries through the same OpenAI/self-hosted fallback the agent conversation loop already used, so intent enrichment, the external-search enrichment and gate calls,
data.askand the classification passes degrade to a slower provider instead of erroring. - Splitting an uploaded document into writing samples now reports a typed "temporarily unavailable, try again shortly" when every model provider is down. It previously surfaced a raw server error on a document you had just uploaded, while every other failure in that flow returned a readable message.
- Reloading Chat mid-turn no longer loses the live step. The run stream now catch-up-replays persisted tool events from
?after=<seq>, and the run READ returns the in-flight answer draft pluslast_seqso a second tab or a 10-minute SSE reconnect can resume the same transcript. - Connecting a Computer Agent now asks for the deployment URL. The connect flow for the
computer_agentconnector advertises abase_urlfield alongside the service token, and passing it lands the connection instead of failing withbase_url is required. Its catalogconnect.fields(and the MCPconnections.connectaction) now carry the non-secretbase_urlfield, so a console or API caller supplies both the deployment URL and the token.
Added
- The pipeline gate now tells you WHY, not just which lines failed.
verdict.gate(andGET /eval-runs/{id}/gate) carries two new fields:reasoning, the judge's rationale for the copy you submitted, andcritique, what would have to change for it to pass — both in the judge's own words. They were computed on every gate-mode run already and reached no caller, so a pipeline holding a send could report that three checks failed and nothing a writer could act on.critiqueis null on a full run rather than absent, because a full run critiques the version the eval wrote rather than yours; read that null as "not measured on this run", never as "nothing to say". Runs graded before this shipped answer with both fields set to null instead of omitting them, so a consumer can branch on one shape. - Filter and group on how many people spoke in a turn. Three columns are now available on the
interactionssurface insearch.fields,search.query,data.query,data.askanddata.aggregate:speaker_ambiguous(does this row hold more than one voice),speaker_count(how many), andturn_speaker_keys(the roster of identity keys, readable viaUNNESTbut not filterable). A row on this surface is a grouped TURN whosespeaker_namenames only its FIRST member, so an aggregate keyed on a speaker was folding rows together inside the warehouse where nothing downstream could repair them.WHERE speaker_ambiguous = FALSEscopes such an aggregate to turns whose speaker is verified — 4.88% of stamped rows hold more than one voice (102,992 of 2,110,805, fleet-wide, measured 2026-08-16). Read that denominator literally: rows written before these columns existed carry no verdict at all and are excluded from it, so the share of *every* row in the surface is 4.67%, not 4.88%. Your own workspace will not read either number — per-tenant values range from 0% to 20.9% — so scope the query to your data before quoting a rate. UseCOUNT(DISTINCT k) FROM interactions, UNNEST(turn_speaker_keys) AS kto count distinct PEOPLE:COUNT(DISTINCT speaker_name)counts turn stamps and omits anyone who never leads a turn. - Eval runs can now grade against the market, not only your own conversations. Pass
include_external: trueonPOST /evals/run(MCP: the same param on theevalstool'srunaction) and one public-web fan-out runs — the query planned by a lightweight model from your submission — and its snippets join the evidence pool taggedexternal, each labelled with its publication. The tier is a licence the writer and the judge are both held to: external quotes back market claims ("analysts report"), never customer voice, and citing one for a customer claim is a grounding failure. Opt-in (a paid fan-out), gated on theexternal_search:executescope — refused without it rather than silently dropped — andexternal_cap(default 15, clamped 1–25) bounds the retrieval. The run's evidence disclosure gainsexternal_status,external_query,external_sourcesandexternal_quotes, and the progress trail reports the fan-out asexternal_resolved.
Changed
- Correction: the speaker mis-attribution rate we published is one workspace's, not the fleet's. The 2026-08-15 entries state 11.84% of members mis-stamped and 13.58% of turns holding more than one voice. Those numbers are correct, and they are workspace
a7c1bd65— which turns out to be the least representative workspace on the fleet for this defect: every one of its multi-voice turns *also* mixes an internal speaker with an external one, the kind a regroup clears, while on every other workspace the reverse holds and almost none of the rate is clearable that way. Measured the same day across nine workspaces, the fleet rate is 7.70% of members mis-stamped (234,473 of 3,044,300) and 6.11% of turns holding more than one voice (134,510 of 2,201,887). The four published entries now carry both figures. Nothing about the fix changed — the behaviour those entries describe is what shipped; only the magnitude was quoted from an atypical sample. - And the part of it no regrouping can reach. Of the 134,510 multi-voice turns, only 8,419 (6.3%) mix speaker types at all; the other 126,091 (93.7%) hold two people of the *same* type — two buyers, or two of your own team. Grouping breaks a turn on a speaker-*type* change and never on identity, so those 93.7% are not a backlog that a repair drains: they are what the grain does. That is why the fix is a new shape in the data rather than a tightening of the existing rule.
- "Does this row hold more than one voice" is now READ from the warehouse instead of being worked out at query time. A row on the
interactionssurface is a grouped turn — several utterances folded into one row and stamped with its first member's name — and until now this platform answered "did anyone else speak in this turn" by re-walking the turn's members on every read. The pipeline now records the answer on the turn itself when it builds it, and 2,109,611 turns have been stamped (102,963 of them, 4.88%, hold more than one voice). Every read path that verifies speakers —search.query(both lanes),data.query, page templates, blueprint steps, embedded pages, deal health and the testimonials people picker — now takes the recorded answer first. You may see `speaker_ambiguous` change on rows whose members are no longer resolvable: a turn the pipeline counted as one speaker at the time it was built is no longer reported as ambiguous just because one of its utterances has since gone. - NEW per-row field `speaker_ambiguous_source`, on every turn-grain row that carries
speaker_attribution."stamped"= the verdict was read from the column the pipeline writes;"overlay"= the pipeline recorded nothing for that turn, so it was reconstructed at read time. A"stamped"row also carries `speaker_count`, the number of distinct speakers the pipeline counted. Both are additive — nothing was renamed and no existing field changed shape. - A turn with no recorded verdict is NOT reported as single-speaker. This is the distinction the whole design turns on: an absent value means *not measured*, never *one speaker*. Interactions excluded from your workspace's analysis are never stamped by design, so an unrecorded turn is a permanent state for part of the corpus, not a gap that closes. Those rows keep the read-time reconstruction and say so in
speaker_ambiguous_source. - NEW envelope counters, so you can see which path answered.
corpus.speaker_stamped_rowsandcorpus.speaker_fallback_rowssplit the rows on both search lanes; thespeaker_grainblock addsmultiplicity_source("stamped"/"overlay"/"mixed"),stamped_rows,fallback_rows,disagreed_rows(rows where the recorded verdict and the reconstruction differed — the recorded one is served) andstamped_columns_available. Deal health'sspeaker_grainand the testimonials picker'sspeaker_grain(stamped_turns/fallback_turns) carry the same split. Emitted whenever verification ran, including when every row took the same path — a counter that only appears on the interesting value cannot be watched. - Nothing was removed. The read-time reconstruction is still there and still runs; it is now the fallback rather than the primary answer. It is also still the only thing that can NAME a speaker — the recorded columns count voices, they do not identify them — so re-attributing a mis-stamped row, the
turn_speakersroster and the people picker all continue to work exactly as before. *(Correction, 2026-08-16: "they do not identify them" is too strong.turn_speaker_keyscarriese:<email>andn:<name>keys, and an email identifies a person as well as an email ever does; what no key carries is a display name —n:keys are lowercased, and 50.6% of multi-voice key slots aree:with no name in the row at all. The practical consequence in this entry is unchanged: you still cannot render who spoke from the recorded columns alone. The sentence understated what the keys are, not what they can do for you.)* - `data.aggregate` now discloses when a number was computed from turn stamps. Results already carried a
speaker_grainenvelope when a speaker column appeared in the returned rows. They now also carry one when the speaker column reached the SQL only through ametricor afilter— for examplecount_distinctofspeaker_namegrouped by company, whose rows contain no speaker column at all and so previously came back with nothing saying a speaker was involved. The numbers are unchanged; only the disclosure is new. - The "could not verify the speaker" notice now names a remedy an aggregate can use. It previously advised selecting
interaction_idalongside the speaker columns, which aGROUP BYcannot do without changing its grain. It now points atWHERE speaker_ambiguous = FALSEand states what that scope costs: it turns a wrong name into a missing person, because anyone who never leads a turn drops out of the result. - Persona counts in external search are now computed over verified single-speaker turns.
inferred_personais derived from the speaker's title, which on a multi-voice turn belongs to the turn's first member — and because the facet is a ranked top-N, an over-counted persona displaced a real one. The facet reports its own denominator, which is smaller thantotal_utterances; read shares against that, not against the total.
Removed
- `enrich` and `lookalike` are retired. The five endpoints they served —
POST /enrich/company,POST /enrich/person,POST /enrich/topic,POST /lookalike,POST /lookalike/themes— and the two MCP coarse tools of the same names are gone. The MCP surface is now three tools:search,agents,evals. - What replaces them. Similarity over your own corpus is
POST /search/querywithmode: "semantic"— the same meaning-based ranking, through the door you already use, with the full filter vocabulary available alongside it. Depth on one company, person, or topic is a Chat (POST /chat), which still reaches the market fan-out and returns a written answer rather than a brief envelope. - Nothing else on the API moved.
search.query/search.fields, Chat, the agent library, Routines and Evals are unchanged, including their scopes and response shapes.
Fixed
- `POST /search/query` — a whole-corpus aggregate now runs in its bare form.
{"mode":"filter","metrics":[{"fn":"count"}]}used to be refused as an empty request while the same aggregate ran once a filter or an unrelatedquerywas added. Agroup_bywith no metric now gets a message naming the missing metric instead of the generic empty-request error. - `POST /search/query` — params a lane cannot honour are named in `corpus.ignored_params`. A
querysent to thefilterlane is not read by that lane, andasync/max_subqueriesdo not apply to thesemanticlane; all three are now disclosed rather than dropped silently. Notably, anasync: truerequest whose wording routes it to the semantic lane answers synchronously and says so — previously it returned no job handle with nothing explaining why. - `POST /search/query` — broad asks on the async lane no longer come back empty. The async planner ran on the synchronous 5-second budget, so an ask split across many sub-questions was cut while planning and returned no coverage — exactly the case the async lane exists to serve. It now plans on the async budget.
- `POST /search/query` — an ask naming one competitor no longer returns the whole rival leaderboard. "How many interactions mention the competitor Acme" matched a corpus-wide template that has no place to put the name, so any two named competitors produced identical results presented as a complete answer. Asks that name a specific company, product, or competitor now route to the query writer that can scope to it.
- `POST /search/query` — questions that fit a single query are answered instead of refused. The query writer declined asks it judged ambiguous, or expected to return nothing (an unfamiliar product name, a date range before your data starts). It now resolves the ambiguity and states which reading it took, and returns an honest zero rather than reporting the question as unanswerable.
- `GET /search/fields` — `deals.company_ids` is correctly advertised as not filterable. It is an array column, but was listed with the full set of string operators; filtering on it returned a server error. It is still discoverable and still readable in results.
- `GET /eval-runs/{id}/drafts` — the submitted side no longer shows a prompt you did not send. On a run submitted with only a message,
submitted.promptcarried the eval's own suggested reusable prompt. It is nownull, per the documented contract. The suggested prompt is unchanged onimproved.prompt, and a simulated message on a prompt-only run still appears, still flagged bysimulated_before. - PUBLIC SURFACE: an embedded page now shows the same attribution the app does — and embed consumers see this without logging in. A row on the
interactionssurface is a grouped turn — several utterances folded into one row and stamped with its first member's name — sospeaker_name,speaker_title,role_level,is_champion,is_economic_buyerandchampion_scorecan be about somebody else. 11.84% of member utterances carry a stamp that is not their own and 13.58% of turns hold more than one voice (measured live 2026-08-15). The page-embed renderer deliberately bypasses the shared SQL gate (that gate derives its access predicate from a session user, and an embed has no session user, so reusing it would fail open) — but the bypass also skipped what the gate does on the way out. In-app a row arrived carryingspeaker_attributionsaying whether anybody had verified it; through a public embed link the identical row arrived bare. It no longer does:/api/embed/pages/.../renderruns the same turn-grain verification, stampsspeaker_attributionon every row, and returns aspeaker_grainenvelope per query. Names on public embeds will change — a two-voice turn now names nobody rather than naming its first member. - Embedded pages also stop serving wrapper-shaped cells. The same bypass skipped the gate's cell flattening, so one page served
{"value":"abc"}publicly and"abc"in-app. Both are now scalars. This was load-bearing, not cosmetic: an unflattened turn key is unkeyable, so it silently disabled verification too. - Page renders carry `speaker_grain` on both paths. The authed page runner was computing the gate's verdict and discarding it, leaving a page holding corrected values with no statement of what was corrected — indistinguishable from a build that never checked. Both
runDeclaredQueriesand the embed renderer now forward it. - The Champion & EB Voice template says which of its numbers are checked. Its two quote tables now select the turn key, so each quote is verified against the atomic utterance record and renders an Attribution column beside the name (
utterance= verified,ambiguous= the turn held more than one voice and the name was withheld,turn= an unverified stamp). Its three headline stats and the seniority chart cannot be fixed this way —COUNTIF/AVG/COUNT(DISTINCT)fold the stamps into scalars inside the query, before any row exists — so the page now carries a callout saying those numbers are unverified, on the page itself rather than only in the envelope, because an embed consumer reads the render and never sees an envelope. - The testimonials people picker was hiding people, not just miscounting them. It asked BigQuery to
GROUP BY speaker_name, so a person who is never the first member of any turn at an account produced no group and did not appear in the picker at all — on an account where two people always talk together, the second one was invisible. It now reads at turn grain, verifies each turn against the atomic utterance table, and aggregates on the verdict.interaction_countcounts turns the person was verified to have spoken in, so it goes up for anyone who habitually shares a turn; each person's title comes from their own atomic rows rather than from a window function partitioned on the stamp; and the response carriesspeaker_grainplus per-personambiguous_turns/unverified_turns. The read is bounded at 1,000 turns per company — it is row-grain where it used to be a warehouse-side aggregate — so a larger account reportstruncated: trueand counts over its most recent turns rather than letting a partial count read as a total. - Scoped cluster-detail quotes name a verified speaker. When a data-scoped member opens a theme, its representative quotes are re-drawn from the turn-grain view; those quotes now carry
speaker_attribution, and a quote from a turn holding two voices ships its text with no name rather than crediting the turn's first member. Slice sizes are unchanged — this was a mislabel, not a miscount. - Still not covered, named rather than left silent. Five reads compound a wrong attribution into a number and none of them can be fixed by a read-side overlay, because each groups or filters on a speaker column inside BigQuery, where the turn key no longer exists: the knowledge-bank people matcher (
peopleMatchingService), the company-catalog persona facet, the external-searchpersona_countsaggregate, the listening-lens documentauthorand transcript blob, and the shared deal-attached-voice SQL fragments (whoseQUALIFY ROW_NUMBER() OVER (PARTITION BY speaker_name)caps quote diversity on the wrong key across three blueprint starters and four page templates). Each needs its aggregation moved out of SQL, the way deal-health's and the people picker's were. Separately, the sales-digest transcript reads through a remote MCPdatatool, so whether it is already covered depends on which buildSALES_DIGEST_MCP_URLpoints at — undetermined, not assumed either way. - Quote panels stop hiding the second person in the room. A row on the
interactionssurface is a grouped turn — several utterances folded into one row and stamped with its first member's name — and eight quote-panel queries (three blueprint starters, four page templates) splicedQUALIFY ROW_NUMBER() OVER (PARTITION BY speaker_name ORDER BY quality_score DESC) <= 3into their SQL. That cap does not relabel a row, it deletes it, and it ranked inside a partition keyed on the stamp: a turn holding two voices spent the *first* member's slot even when the quotable sentence was the second member's, and anyone who never leads a turn had no partition at all and so could never occupy a panel slot under their own name. 11.84% of member utterances carry a stamp that is not their own and 13.58% of turns hold more than one voice (measured live 2026-08-15). A panel whose entire purpose is speaker diversity was delivering *stamp* diversity, and the difference was invisible from the output. The cap now runs after the gate has verified who spoke, against that verdict — a turn holding two voices charges a slot to every member, so a shared quote cannot be spent twice. - Five page-template quote panels were not being attributed at all. They selected no
interaction_id, so the turn-grain overlay reportedno_turn_keyand every name they rendered was an unchecked stamp — the panels looked identical to verified ones. They now select the turn key, so the same verification the in-app and embed paths already run actually reaches them. - The bounded-read trade, stated rather than absorbed. The cap used to run inside BigQuery, so a
LIMIT 25returned 25 diverse rows drawn from the whole corpus. It now runs over the rows the read returned, so a panel can come back shorter than its limit.speaker_diversity.pool_exhaustedsays which of the two happened — the corpus genuinely has few quotes, or the read ran out of candidates and more exist that this query never saw. Only the second is fixable by reading further. Previously the panel was full and capped on the wrong key; now it can be short and says so. - A name the corpus contains is no longer reported as absent. The knowledge-bank people matcher asked
WHERE LOWER(speaker_name) LIKE … GROUP BY speaker_nameon the turn-grain surface, so a person who is never a turn's first member has no row carrying their name, matched nothing, and came back asmatches: []— indistinguishable from a genuinely unknown person. It now reads the atomicutterancestable, where the filter key and the grouping key are the speaker's own name, so nobody is hidden and no aggregation has to leave SQL.interaction_countnow counts turns the person was verified to have spoken in: it goes up for anyone who shares turns and down for whoever was absorbing their turn-mates' words. If the atomic read fails the old turn-stamp query still answers, and every match it returns is labelledspeaker_attribution: 'turn'. - Internal lens documents stop inventing an author.
ANY_VALUE(speaker_name) AS authorpicked an arbitrary turn's stamp and presented it as the author of a whole conversation — non-deterministic, and on a five-speaker call it erased four of them before a lens engine ever saw the document. A conversation with more than one speaker now returns no author rather than one chosen by row order. The transcript body is a separate matter and is disclosed, not fixed: its per-line speaker labels are still turn stamps, and every internal document now carriesspeaker_attribution: 'turn'saying so. - Two reads are disclosed rather than corrected, with the reason named. The company-catalog persona facet (
GROUP BY speaker_title) omits any title held only by people who never lead a turn, so an audience cohort the tenant genuinely has cannot be selected — but it is an unbounded tenant-wideDISTINCTwith no turn list to verify, and moving it to the atomic table would silently change its denominator (unlike its two siblings it carries nointeraction_type = 'UTTERANCE'filter, and confirming what it counts today needs a live schema read). The external-search `persona_counts` aggregate cannot be attributed at all:inferred_personais a speaker-*derived* column that the shipped attribution nulls rather than re-derives, so there is no verdict to aggregate on and turn-grain attribution would replace the distribution with a smaller one plus a remainder — differently wrong, not better. Every option and every bucket from both now carriesspeaker_attribution: 'turn', stamped on the value rather than announced on the envelope, because values get copied out of envelopes. - Still not covered, named rather than left silent. Re-deriving the population turned up reads outside this change's five that compound the same stamp into a number, and none is fixed here: the
data.aggregateMCP verb, whosegroup_byallowlist accepts all nine speaker columns from a model at runtime while the column catalog it reads describesspeaker_nameas "Name of the speaker" with no turn-grain caveat; theaccount_tier_intelligenceengagement aggregate, whosehas_championgate andexecs_engagedcount decide which accounts a seller works; thechampions/economic_buyerscurated search templates, whichGROUP BY speaker_namebehind a stamp-keyedWHERE; the evals cohort floor, whoseCOUNT(DISTINCT speaker_email)under-reports the very speaker diversity it exists to certify and fails toward refusing to grade; and the data-governance filter preview, which counts would-be-excluded rows on a turn stamp. - The API reference no longer advertises the retired `enrich` and `lookalike` endpoints. Both families were removed from the API, but the reference landing page, the docs index, the tool catalog, the error catalogue and the rate-limit page all still described them as live surface — including bullets pointing at per-endpoint pages that no longer exist. Those pages now describe only what you can actually call: Search (
search.query/search.fields), Evals (evals.runand its read surface), and Agents (Chat, the agent library, Routines). The monthly verb-cap families behindquota_exceededare likewise listed correctly assearchandevals. No API behaviour changed — this corrects the documentation only.
Added
- `GET /agents/usage` — thin workspace rollups of recent agent and routine activity (7-day / 30-day run counts, last run, schedule counts). Read-only under
agents:read; not a billing surface (token/cost usage stays on chat usage). - MCP API keys list rows now include `request_count` (lifetime requests that validated the key) so Operators can see key activity without a separate metrics product.
- Connections last-run summary exposes richer stream failure fields when the connector reports them.
- Evals can now be graded against a named slice of your corpus, not just all of it. A new top-level
scopeonPOST /evals/run(and the MCPevalstool'srunaction) takes typed filters —{ surface, field, op, value }, ANDed, up to 25 — over the same field vocabularysearchadvertises, so _"grade this draft against what executives at closed-won accounts actually said"_ is one call. Filters may mix theinteractions,dealsanddeal_qualificationsurfaces in a single scope, whichsearch.querycannot do (it takes one surface per call); the deal-grain half is resolved to the accounts it matches and scopes the quote draw to those. Discover field names, types and admitted operators fromGET /search/fields(search_field://list, or thesearchtool'sfieldsaction) — never guess them. - A scope changes the evidence, not only the framing. A slice that clears the check has its own utterances retrieved and tagged
segment— the tier that licenses _"teams like yours"_ — spread across companies so one talkative account cannot speak for the slice. Filters and the existinginputs.audienceCOMPOSE rather than race: when both resolve, the cohort predicate is ANDed into the filters before the floors are measured, so the counts, the quotes and the slice's name all describe the same intersection. - A slice must clear an evidence floor before it is graded against: 3+ distinct external speakers, 25+ utterances, 2+ distinct companies. A search hands you four rows and you judge them; an eval hands you a grade with the slice's name stapled to it, and four rows reported as "what enterprise champions say" is a real number under a false label. When the check does not pass the run still completes and still grades your prompt and message, and names which of seven things happened (
not_provided·invalid·no_evidence·thin_evidence·lookup_failed·no_matching_accounts·too_many_accounts) on ascope_resolvedstep in the run's progress trail (progress.steps[]oneval_run://<id>), carrying thereason, a writtenmessageto show a person, anddetailwhere there is something actionable to say. That step is the only surface the outcome reaches today — it is not on the verdict or the report card, whereaudiencealready is.lookup_failedmeans our check broke, never "you have no data", and deal filters matching 500 accounts or more abstain rather than silently grading a clipped subset under the slice's name. What an abstained run falls back to is your whole corpus UNLESS a resolvedinputs.audienceis still in play, which is gated on its own path and survives. Passallow_thin_evidence: truewhen the narrow cut is the question: the run grades it and the step carriesbelow_floors: truebeside the real counts. - `scope` is a named dimension of the run fingerprint, so a filtered request and an unfiltered one are different runs under the default
reuse: "cached"— otherwise the second would be served the first one's corpus-wide verdict under a slice's name. Filter order is canonicalized there, so re-ordering the same filters does not fork the address. The evidence-pin conflict check compares a different, order-SENSITIVE rendering of the slice, so a pin re-stated in another order is refused rather than reused — the safe direction to be wrong in, but reuse the same filter array across an A/B rather than rebuilding it. It is additive: a run that sends noscopebehaves exactly as before. - A malformed scope is refused, never silently dropped. An unrecognised key (
operatorforop,columnforfield) or more than 25 filters comes back asinvalid_argumentfrom theruncall itself, before a run id exists. The alternative — stripping what we do not recognise — returns a perfectly well-formed report about a wider slice than you asked for, with nothing in it saying so. A scope that narrows NOTHING is a different case and is accepted:{},{"filters": []},{"audience": "all"}and a bareallow_thin_evidenceall grade unscoped and all recordnot_provided, so nothing is hidden and there is nothing to refuse. - `department` is not filterable, and never was. It is the axis most GTM teams reach for first, and it does not exist as a column on the warehouse view evals and
searchboth read — no spelling of it works on any surface. Cut onrole_level(ic·manager·executive·unknown) instead, which is both reachable and, hand-labelled against production titles, substantially more accurate. - Attio is a first-class CRM connector. Connect it with a workspace API key and Amdahl syncs companies, people, deals, notes, tasks, and lists — plus the deal stage history Attio keeps natively, so stage-change timing is exact rather than reconstructed. It lands with the same setup surface the other CRMs have: your stage vocabulary, workspace members, and attribute catalog arrive in seconds at connect time, so you can map stages while the first full sync is still running. Attio has no pipeline object — the deal
stageattribute is the pipeline — so the pipelines step reads as "not applicable" rather than failing. - Connecting Attio or Granola now checks the credential against the vendor before it reports connected. Both call the provider for real with the key you just pasted and show what they found, so a token scoped to the wrong workspace surfaces at connect time instead of as a source that syncs zero records indefinitely.
- `deal_match_method` on the
interactionssearch surface — filterable, groupable, and published insearch.fieldswith its full value vocabulary. It answers a question the API previously could not: whendeal_stage_status,deal_stage_labelanddeal_amountcome back null, was there genuinely no deal on that company, or did the deal match *decline* to attach one because none was open at the time of the conversation? Those arenulland"no_open_deal"respectively, they need opposite handling, and together they cover roughly 61% of linked rows — so a blank deal read as "no pipeline here" was the easiest way to misread the surface. The remaining values name the tier that did attach (crm_association,engagement,contact_role,company_time_bounded,company_heuristic,company_post_close), which also lets you keep post-close conversation out of win/loss analysis. - Emails an agent sends you are now formatted, with tables and charts. The body of a
notifications.email_membersend was rendered as plain text: a markdown table arrived as literal pipes,##as literal hashes, and**bold**as literal asterisks, so a weekly digest landed as a wall of unbroken prose. Bodies are now markdown — headings, bold and italic, links, nested lists, blockquotes, code, rules and GFM tables all render — plus two data visuals written as fenced blocks ofLabel: valuelines:chartdraws a horizontal bar chart sized against the largest value in the block (a ranking), andmetricsdraws KPI cards where a| +12%caption renders green and| -3 ptsred. Both take a title after the fence word. Nothing changes for a caller who keeps sending plain text, and the plain-text arm of every email still carries the same content for clients that prefer it. Line charts, pie charts, sparklines and images are deliberately not supported: mail clients block or strip all of them, and a visual that renders for only some recipients is worse than a table that renders for all of them. Raw HTML in a body is escaped rather than rendered, as before. - Agent-authored emails now flag values they could not source. The fact-check engine annotates a claim it cannot tie to evidence, and those annotations reached the inbox as literal
<unverified>194</unverified>text — a real integrity signal that read as a rendering bug. A marked value now renders as an amber, dotted-underlined number with a one-line legend beneath the email explaining that it could not be matched to a source during that run; the plain-text arm reads194 (unverified). The legend appears only when something is actually marked, so an ordinary email gains no extra chrome. Nothing about the signal changed — only whether you can read it. - GTM Strategist joins the agent library. A shipped agent (
gtm-strategist) that answers go-to-market decisions — who to sell to, how to position, why deals stall — from your own customer conversations, and compares them against what the market says. It reads the corpus before the market, sizes a finding by distinct companies rather than mention count, and closes with the observation that would disprove the call. Available in the agent directory alongside Researcher; dispatch it by slug or run it from chat. industry_normalizedandindustry_sourceon theinteractionssearch surface. Group or filter byindustry_normalizedfor a canonical industry bucket instead ofindustry, which is a raw passthrough of your CRM's and the enrichment provider's different spellings — so a plainGROUP BY industrysplits one industry across rows and undercounts it. On a corpus we measured,computer softwareandCOMPUTER_SOFTWAREcame back as separate rows for the same industry, and the canonical bucket was 23.5% larger than the biggest raw one.industry_source(crm|enrichment:pdl|enrichment:pdl_bulk| null) tells you which rows your CRM asserted versus which were inferred, so you can weight or filter on provenance. Both are visible inGET /search/fields.- You can now ground a draft in a comparable customer, by name. A seller writing to a prospect wanted the message built on the work we did with a similar customer — the ordinary sales move — and the eval refused it: every quote came back CORPUS-tier, so naming that customer was an unlicensed account-specific claim. The grader was right; the input vocabulary was the gap.
accountwas single-valued and did double duty as *who the draft is going to* and *whose evidence to retrieve*. Nowaccountstays the recipient and a new `reference_accounts` input names comparable customers whose quotes are drawn at ACCOUNT tier, tagged to that company. The licence is per company: a quote licenses a claim about that company and no other — including the recipient — so "11x cut onboarding time" is groundable when 11x is a reference account, while a claim about the prospect still needs the prospect's own evidence. Up to 3 names, 2 quotes each; the recipient's own draw shares the account tier's slots; unresolved names are reported individually rather than silently dropped.prompt-and-message-evalmoves toeval_version2.28.0 and scores are not poolable across it — the per-company rules are inlined into every grading prompt, so the judge reasons differently on every run whether or not you pass the new input. - `/compare` stops emitting a score delta across an eval-version boundary that invalidates it.
overall_scorechanged meaning at 2.14.0 andverdictchanged sides at 2.16.0, and nothing checked either side's version — so a delta spanning one of those was an artifact of the version change rather than a change in your copy. Comparisons now carry aversion_boundaryblock naming the transitions crossed, anddelta_withheld_reasongains a sixth valueeval_version_boundary. The two crossing kinds behave differently: across a payload-meaning bump onlyscore_deltais withheld andsubmitted_score_deltasurvives; across a grading-work bump both sides' numbers came from two different instruments, so attribution is blocked entirely. - `truncated` on search actually fires now. It was documented as the cap-hit signal and was false by construction on every lane — the SQL gate rewrote every
LIMITto the requested value, so the field could never be true. Any client using it as a sampling-bias guard was recording nothing. Both lanes now peek one row past yourlimit, detect the overflow, and slice the extra row off, so row counts are unchanged and the flag means what it says. Therow_count == limitheuristic people used instead is now a false positive on a population that lands exactly on yourlimit— readtruncated. One deliberate exception: atlimit: 1000, the engine's own maximum, there is no room to ask for a row past it, so a full result always readstruncated: true— there it means "there may be more" rather than "there certainly is". - `cached` is on the response envelope. It was fuzzy-only and buried under
detail.internal, so on the filter lane there was no way to tell a cached answer from a fresh one and any consistency check was silently meaningless. It now rides the top level on the filter and fuzzy lanes, and is omitted (notfalse) on a miss. The semantic lane still has no result cache, so it never carries the field at all rather than implying a cache that missed. - A gate run no longer fails you on a check your artifact type is exempt from.
mode: "gate"told the judge what kind of writing it was reading but never applied the exclusion map the full report applies, so a declaredlanding_copygate run was graded on all five rubric lines and reported the excluded line insidedimensions_failed— an actionable-looking failure on a check that was supposed to abstain. Scored denominators are unchanged; only the reporting was wrong. - A custom eval's `reliability` declaration is no longer lost on edit. Authoring validation requires it for any grader that reports a magnitude, but the store dropped it on write — so a declaration that passed validation never reached the row, and the next edit failed on a block the author had in fact supplied. It now persists,
evals.updatecan revise it, and all three authoring surfaces advertise the field instead of leaving callers to discover it through a refusal. - Every `search.query` answer that reads a store now carries a `corpus` block naming the store that answered, at what grain, over how many rows, and why it was empty. The three lanes read different stores —
filterandfuzzyread the warehouse,semanticreads a vector mirror bounded to customer-side (external) speech at ingest, so it searches a strict subset — and nothing on the wire said so, which is a difference people were left to infer from empty results. The block reportsstore,grain,row_count, whether yourfiltersactually reached the store (filters_applied), whether rows carry fullcontentor the mirror's truncated preview, andempty_reason, which separates an honestno_matchfrom aread_failed. The semantic lane degrades to a theme index — a different row shape — when the vector path is unavailable; that now readsdegraded: truewithfilters_applied: falseinstead of passing for a normal answer. - `audience: "customer_voice"` applies the workspace's canonical customer-voice predicate server-side: it keeps customer, sales-target and unknown-relationship accounts, drops investor / advisor / media / competitor / vendor / partner / reseller / other, and rescues a genuinely deal-attached buyer sitting on a dropped account — the half a hand-written relationship filter usually misses.
interactionsonly. Available on thefilterlane, and onsemanticwithhydrate: true; refused onfuzzy, which writes its ownWHEREclause and would overwrite it rather than combine with it. - `hydrate: true` on the semantic lane bridges each vector match back to its full warehouse row, so one call can find by meaning and cite by attribute — full
contentpluscompany_name,speaker_title,is_championand the other label columns, none of which the mirror stores. The bridge is call-grain, so a hydrated result is the matched utterance's whole call rather than the single turn;corpus.match_countalongsidecorpus.row_countshows where the rows went. Capped at 200 distinct calls, reported ascorpus.truncatedrather than silently sliced. - Semantic matches also return `parent_interaction_id`. The two lanes'
interaction_idwere different things — the semantic lane's is the parent call id, the filter lane's is turn-grain — so joining them by matching field name returned nothing, every time.parent_interaction_idjoins tointeractions.parent_interaction_idand is the finest join available today; joining at utterance grain needs a pipeline change and is not possible yet. - All of this is additive. No existing field changed shape, name, or meaning, and a caller that ignores
corpus,audienceandhydrategets exactly the responses it got before. - Every `search.query` answer that reads a store now carries `corpus.coverage` — the time range the returned evidence actually covers (
max_timestamp,min_timestamp), an age histogram (last_7d/last_30d/last_90d/older, always all four, including zeros), anddated_row_count. It replaces a manual discipline ("ask for the max timestamp before calling anything a trend") that had no enforcement and no input. The sharpest case is thesemanticlane, which ranks by similarity and carries no temporal guarantee at all: a workspace whose newest call landed this morning could ask about pricing objections and get its best matches from three months ago, with nothing on the wire saying so. - Read `coverage.basis` before any number under it. Coverage is not uniformly derivable — an aggregate carries no event-time column, a truncated result is an arbitrary slice of a larger match set, and the fuzzy lane's SQL is written per call so its projection may contain no timestamp at all. Those report
basis: "unavailable"with abasis_reason, and their numbers are nulled or zeroed as unmeasured, never as measured zero.matched_setmeans the block describes your whole match set;returned_rowsmeans it describes the slice you were given. The event-time field comes from a fixed allowlist (timestamp,occurred_at) rather than a name heuristic, which would otherwise pick upclose_date— a projected deal close, not an event time — and report a confident wrong span. - `coverage: true` adds `gaps` — windows where the corpus held rows and your result set had none. Deliberately not "windows your matches skip": conversation volume is bursty and empty days are business-normal (one healthy week here carried 276 calls with zero on Saturday, Sunday and Tuesday), so an empty-bucket scan would fire constantly on a healthy workspace. When your
filterscarry a range ontimestamp/occurred_atthe gaps are measured across that range, which is the only framing that can see your evidence running out *before* the period you asked about; otherwise they are measured across your matched set's own span. It costs one extra read, which is why it is opt-in. A failed baseline reads asgaps_status: "unavailable", distinct from"ok"with an emptygapsarray — a broken measurement never poses as "no gaps found". - `empty_reason` now reaches aggregates. It keyed off
row_count, and a bare aggregate returns one row containing a zero — sorow_countwas1and the field was structurally unreachable on every aggregate ever run: acountover an empty window came back as{"count": 0}with nothing explaining it. A bare aggregate over an empty population now carriesempty_reason: "no_match". Aggregates with agroup_bywere always fine. - All of this is additive. No existing field changed shape, name, or meaning, and a caller that ignores
coveragegets exactly the responses it got before. - `search` has a new `lexical` mode — exact terms, ranked by how rare they are. The existing
semanticmode finds utterances that *mean* something like your query, which is the wrong tool when the exact wording is the point: searching your own marketing language returns whoever used those words, not the buyers reacting to them.mode: "lexical"matches literal terms and scores each hit by how uncommon the term is across your corpus, so a match on a standard, product name, or error string outranks a merely-similar line. Reach forlexicalwhen the wording matters andsemanticwhen the meaning does;autopickslexicalon its own when your query contains a quoted phrase. - The lexical lane tells you what it actually searched for. Query terms too common to be informative are dropped before the search, and the reply carries
corpus.search_lexemes(what was used) andcorpus.dropped_lexemes(what was set aside). This matters for reading a zero: without it, "no customer said this" and "we searched for something narrower than you asked" look identical. Field weighting favours the call title and the topic over raw body text, so a hit in context ranks above an incidental mention. - The `lexical` search mode now tells you how relevant each match is, and filters out filler. Every result carries
relevance(0–1): how distinguishing the best term it matched is, compared with the most distinguishing term you searched for. Previously the only signal wasscore, which is unbounded —1.50and8.97gave you no way to tell a real match from a row that happened to clip one common word. Matches that fall below the bar are removed, and the reply reportscorpus.min_relevance(the bar used) andcorpus.weak_matches_filtered(how many were removed), so a short result set is never unexplained. When every match was filler the reply saysempty_reason: "weak_matches_only"— which means "nothing relevant was said", a different and weaker claim thanno_match's "nothing was said". - A quoted phrase now scopes the search to what you quoted. Asking
did anyone mention "data residency" concernssearches for *data residency*, not for "anyone", "mention" and "concerns" as well. Previously the words around a quoted phrase competed with it on equal footing, so the top result could match only the phrasing of your question rather than its subject.
Changed
- Self-serve onboarding retired. New workspaces no longer start in the self-serve setup funnel; members of a fresh workspace get a book-a-demo welcome instead, and the Amdahl team runs onboarding. The member-state read (
GET /console/onboarding/member-state) can now returnnext_action: "book_demo"for a fresh workspace on a tenant without the funnel flag. - Self-serve workspace creation is now opt-in (default off).
POST /workspaces(workspaces.create) returnscreation_disabledfor non-staff callers unless the deployment setsWORKSPACE_CREATION_ENABLED=true, andGET /workspaces/minereportscan_create_workspace: falseaccordingly. Platform admins can still create workspaces, so Amdahl provisions new tenants during onboarding. Book a demo to get a workspace; joining an existing workspace (domain self-join, admin add) is unchanged. - Member personas now include
marketing,revops,sales, andotheralongsideexecutiveanddeveloper(PUT /console/member/persona). New roles map onto the existing Home vs Explore suggestion lean — they do not invent parallel APIs. - Asking the copilot how to set up the Amdahl API now points at Settings → Developer for minting keys, without claiming the agent can create them.
- Scheduled runs are off: living documents and Routines now run only when you ask. Workflow schedules, Routine schedules, the default living-doc bundle that used to be seeded for a new workspace, and the background sweeps that refreshed or re-ran reports on their own are all switched off. Every existing schedule has been disabled rather than deleted, so nothing you configured was lost and no schedule is left advertising a next run that will never happen. Running things on demand is unchanged — "Run now" in the console,
POST /agent-blueprints/:id/run(agents.run_blueprint),POST /routines/:id/run-now, and backtests all behave exactly as before. - Every Amdahl email now looks like it came from Amdahl. Sign-in codes, invitations, "you've been added" notices, workspace access requests, domain-join notices and agent digests were each rendered by their own template, so the same product sent six visibly different-looking emails. They now share one shell: the Amdahl logo at the top, the wordmark and a plain line explaining why you received it at the bottom, and the brand palette throughout. The shared shell also brings improvements the older templates never had — a table layout that survives Outlook, dark-mode handling so text is not washed out when your client inverts the message, an inbox preview line, and a button that renders properly in Outlook. Because Outlook blocks remote images by default, the logo ships with alt text and reserved dimensions so a blocked image never shifts the layout, and the wordmark is repeated as text so the branding is there either way. Subjects are now prefixed
Amdahl:(applied once, never twice, even if a subject already carries it). One email is deliberately left plain: the short personal welcome note, which is written to read like a message from a person rather than from a template. - `buyer_pushback` no longer advertises a precision figure. The schema catalog and the query-strategy guidance used to describe a
buyer_pushback = TRUEas measured at 72.5% precision (with 85.3% recall inside the judged candidate set). Both numbers came from a single gold-set adjudication that the pipeline team has since withdrawn — one LLM grading another on one tenant, measured before a 2026-07 promotion pass that supplied roughly four fifths of the TRUE rows now live. The field's precision is unmeasured: no hand-labelled ground truth exists for it. What the catalog states instead is the measurable ceiling — re-running the classifier over rows it had already judgedTRUEreproduces only about 54% of them, so aTRUEis a pointer to a quote worth reading, not a certified label. Coverage, tri-state (NULLis notFALSE), inherited-recall and does-not-predict-deal-outcome guidance are unchanged. - The
account_type_normalizedfield description (served onsearch fields/search_field://listanddata://schema) now spells out the canonical customer-voice predicate in full, instead of pointing at a source file you cannot open. It also warns that the predicate is a disjunction: run it ondata.queryor the NL-to-SQL lane, becausesearch.query's typedfiltersare ANDed together and cannot express it. Filtering an account-relationship band without the deal-attachment half silently drops roughly 18% of the rows it excludes — a genuine buyer can sit on an investor- or advisor-typed account. - Clarified in the quickstart and the Search endpoint guide that
compiled.sqlis the query as you asked for it, not the statement that reached the warehouse: the gate resolves the surface to its fully-qualified table and injects your tenant filter and any data-scope predicate before running it. Read it as a record of intent, not something to paste back verbatim. - Documented why an eval run names its failed rubric lines two ways.
gate.dimensions_failedonGET /eval-runs/{id}/gatecarries each line's full text, because it travels without a reasoning field;report.findings.failed[].dimensiononGET /eval-runs/{id}/reportcarries the stem only, because there it renders as a lead-in to that line'sreasoning. Same lines, two renderings — the strings differ on purpose. Both field descriptions now say so. - `account_status` now says when the account tier came only from comparables. A run that names reference customers and cannot resolve the recipient — the ordinary first-touch case — reports
reference_onlyinstead of the sameoka recipient-backed run reports.account_namealways disclosed this in prose (11x (reference customer)), but the status is the field you filter on, so counting runs "grounded in the recipient's own words" over-counted unless you knew to parse a display string. Filter onokfor that population,reference_onlyfor the other. Eval version2.28.1: no grading work changed and no score moved, so submitted-side deltas stay attributable across the boundary. - `GET /eval-runs/{id}/gate` and `.../compare/{other_id}` now publish their response shapes. Both returned real bodies while the API reference showed an empty object, so a generated client had no way to learn that the gate answers
statusat the root, thatgateis nullable and a null is not a fail, or that a compare reportsdelta_withheld_reasonwith six typed values when it declines to emit a delta. Reading the absence ofscore_deltaas "no change" is the exact misreading the withholding exists to prevent, and the six reasons need different things from you — so they are now in the spec you can branch on. - `GET /eval-runs` / `eval_run://list` is substantially cheaper. The lean list read no longer pulls every jsonb blob off disk to build a summary row:
evidence_setandcandidate_setare read as the single scalar the wire carries (the pin origin's run id) instead of being fetched whole, andtarget/as_ofare no longer read at all. No field on the lean row changed — this is the query underneath the same response.?include=fullis unchanged and still returns every column. - `total` on that same response is now a planner ESTIMATE, not an exact count. An exact count re-scanned every matching row on a read whose page is at most 200. Treat
totalas approximate for display; page withlimit/offsetand stop on a short page rather than computing page counts from it. - Evals: the attribution tally now says WHY a span was unlicensed. The second-person attribution check reports how often improved copy tells the recipient what *they* said — but it counted two very different failures as one number.
second_person_wrong_tieris the real defect: a covering claim backed the sentence with another customer's words.second_person_undeclaredis weaker and frequently benign — the writer simply declared no claim for an ordinary sentence. The two buckets partitionsecond_person_unlicensedand both now ride on the improved-message facet. Nothing else changes: the policy is stillflag_only, so no copy is marked, no score moves, and a reader who ignores the new keys sees the payload they saw before. Runs graded before this shipped carrysecond_person_unlicensedwithout the split — filter on the specific field you are reading rather than on the aggregate, or a rate taken across that line reads low. - Evals: account evidence now reads your emails, and both sides of the thread. Account-tier retrieval drew only the customer's words, so on a relationship carried by email between calls — where every recent touch is *yours* — the evidence pool froze at the last thing the customer happened to say, and a draft got graded as though nothing had happened since. Measured on one workspace before the change: 37 of 95 accounts had no visible evidence at all, another 30 were stale by an average of 61 days, and 82% of the workspace's email was unreachable. The reported symptom was a draft opening "last we talked" while two months of follow-up — including a meeting the customer had taken — sat unread in the corpus. Up to 3 of the 10 account slots now go to your team's most recent messages, ranked by recency rather than by relevance to the draft, because "what have we done lately" is a different question from "what did they say about this". Replayed across production runs: 4 of 5 resolvable accounts gained fresher evidence, by an average of 45 days.
- Evals: an email is now judged by its message, not by the thread it quotes. Quote length was bounded by a band sized against spoken turns (a call turn runs about 200 characters); email runs roughly six times longer, so the ceiling was excluding about a third of inbound customer email on its quoted reply chain rather than on anything the sender wrote. Chains, envelope headers and stray HTML are now stripped before the bound is measured, and the quote you see is the stripped text. Call transcripts have no chain to strip and are unaffected. Quotes also carry a new
channel(call/email/meeting) on account- and segment-tier evidence, so a written line and a spoken one are no longer presented identically. - The rule that your own words never back a customer claim is unchanged — it is now enforced by labelling instead of exclusion. A quote from your team carries
speaker_side: "internal", the grader sees it taggedOURS, and citing it as evidence of what the customer thinks is a grounding failure. It is there so a draft does not repeat what you already sent, contradict it, or write as though it never happened. This is the same treatment corpus quotes have always had. - `eval_version` moves to `2.27.0`, and scores must not be pooled across it. The evidence a draft is graded against changed, so this is a grading boundary like any other: filter on
eval_versionbefore averaging, trending or thresholding storedoverall_scorevalues, or readverdict.headline.submitted.score_15, which is comparable across it. One cost worth stating plainly — customer-side quotes fell from 50 to 35 across the replayed accounts, because the internal slice takes its slots from somewhere. - Evals: improved messages stop quote-collaging. The generate writer could paste stacked attributed quotes into the email ("As one exec put it…", "Another team…") because the prompt handed it customer quotes to cite by id and asked for claim records with those ids — without saying the *message body* had to stay in the writer's own voice. That produced the Instantly-competitor failure mode: stunning specificity, unreadable narrative.
MESSAGE_VOICE_RULEnow rides both rewrite and advisory: paraphrase into your voice, at most one short quoted phrase, grounding stays inimproved_message_claimsvia quote ids, one proof point over three, no manifesto closer. - House prose style gains the 2026 register tells (negative parallelism, colon reveals, staccato triplets, participial tack-ons, synonym cycling, cadence uniformity, quote collage). Same constant reaches agents, living docs, and every reader-facing eval prompt.
- `eval_version` moves to `2.29.0`, and scores must not be pooled across it. The improved message the writer emits changed, so every score that depends on that artifact is a different instrument on either side of the line.
- Evals: a specific claim can now find the line that backs it.
prompt-and-message-evalretrieved from the theme index alone, so the evidence pool held aggregations of what customers keep saying and never the utterances themselves. Two of the five message dimensions — Grounding and Verified specifics — ask whether a concrete claim rests on something a customer actually said, and for any claim more specific than a theme the pool structurally could not contain the answer. The grader then reported the claim as unsupported rather than reporting that it could not check, so a draft citing real, verbatim, in-corpus quotes came back reading as invented. Retrieval now declares both legs, and the utterance leg is the one that answers "did anyone actually say this". - What this does not fix, stated plainly. A quote must still carry at least 40 characters to be citable, on every evidence leg. The shortest customer lines — the five-word objection that is the whole point — stay invisible to the grader whatever you scope the run to. That is a separate defect being fixed separately; a run that still cannot find a short quote is hitting the floor, not this.
- `eval_version` moves to `2.30.0`, and scores must not be pooled across it. The evidence a draft is graded against changed on every run of this eval, scoped or not, so this is a grading boundary like any other: filter on
eval_versionbefore averaging, trending or thresholding storedoverall_scorevalues. The rubric, the bar, the judge and its blinding are all unchanged — what moved is which quotes the judge was holding. Grounded copy should score higher now, and that is a prediction rather than a measurement. - Correction: `grouped_utterance_id` is not a durable key. The turn-grain
grouped_utterance_idcolumn on theinteractionssurface was documented as a "stable id" when it was added (2026-07-30). It is stable *within* a single read, but it is not durable across a pipeline regroup — regrouping re-mints the id for any turn whose group boundaries change. Its schema description (visible viadata.explore/ schema discovery) now says so. Join on it freely; if you have persisted it as a long-lived key in your own systems, re-derive it at read time, and useparent_interaction_idwhen you need an id that survives regrouping. - `search` hydrate — the
hydratedescription on the MCPsearchtool said it bridges at call grain. It has bridged at turn grain (one row per match, containing the matched utterance) since the turn bridge shipped; the description is now correct and points atcorpus.hydrated_grainfor the degraded case. - Attribution caveat on hydrated rows — a hydrated row is a grouped turn, and its speaker/stakeholder fields (
speaker_title,role_level,is_champion,is_economic_buyer) carry the turn's first member's identity, so they name a different person on 2.3% to 13.5% of external utterances depending on workspace. Each rate is only as good as its name coverage, so both are quoted together: 2.3% at 83.3% coverage, 5.0% at 100%, 5.7% at 91.6%, 6.3% at 100%, 6.1% at 99.2%, and 13.5% at 62.8% coverage — that last one is 21.5% among the rows where both sides are actually named, its published shape biased downward by the 37.2% that are unnamed on both sides and score as a match. Two workspaces have zero name coverage and are UNCHECKED, which is not the same as 0%. Measured 2026-08-15 (amdahl-pipelines#806). This supersedes the 5.0–20.7% band published 2026-08-11, which is kept as dated history rather than deleted: #806 recovered that measurement's unrecorded predicate by reconstruction and reproduced the band to the digit, so the two dates are genuinely comparable. - A hit is therefore reliably quotable but not reliably attributable. The caveat now ships on the
search.queryschema, the MCPsearchtool, and thedata://schemafield catalog for the three affected columns, with guidance to read speaker fields from the atomicutterancessource instead. The mis-attribution itself is unchanged by this entry — this documents the behaviour, it does not alter it. - A workspace reporting zero mixed-speaker groups is still affected. Grouping breaks a turn only when the speaker *type* changes (your team vs theirs), never when the person changes, so two different people on the customer side never split a turn. One workspace reports zero mixed groups and still mis-attributes 2.3%. The fall from 16.2% to 6.1% in another is unexplained — its mixed-group count did not move — and is not evidence of a repair.
- `search` hydrate — the completeness check it taught could not bear the weight. The
hydratedescription told callers to comparecorpus.match_countagainstcorpus.row_count. That delta does not measure what it reads as: the semantic lane's RPC takes alimitand applies no similarity threshold, somatch_countis how many nearest neighbours were pulled and trackslimit, not how many things genuinely matched. The description now names the fields that do answer it —corpus.unresolved_matches(matches with no turn) andcorpus.collapsed_matches(matches sharing one turn) — says to read them directly instead of inferring a delta, and states plainly thatmatch_countis bounded bylimit. - `corpus.hydrated_grain: "call"` is the FAILURE branch, and now reads as one. It means the turn lookup failed and the returned rows are arbitrary turns from the right conversations — context, not your matches (also flagged
corpus.degraded). Both the MCPsearchtool and thesearch.queryschema now say to readhydrated_grainfirst. - Corrected the
inferred_persona,role_level,industryandcompany_segmentfield descriptions in the search catalog, which agents and the NL-to-SQL generator read to decide how to query.inferred_personadescribed itself as inferred from conversation content and better populated thanspeaker_title; it is derived from the CRM job title, so it is neither — treating the two as agreeing is the same fact counted twice.role_levelis derived from the same title rather than complementing it, and both now separate "not scored" (null) from "scored, no rule matched" (unknown), which is the modal value and will dominate a persona mix if you leave it in.company_segmentnow states that it is unpopulated rather than implying a usable CRM segment. - `deal_match_method` now advertises every post-close tier, and the guidance names the suffix rather than one value. The pipeline began tagging additional attach tiers as post-close (
crm_association_post_close,engagement_post_close,contact_role_post_close) alongside the existingcompany_post_close. Those values were reaching the warehouse without appearing in the field's published vocabulary, so a filter on one of them matched nothing and read as an absence in your data rather than a gap in ours. All are now listed. The accompanying warning also changed shape: exclude every value ending `_post_close` from win/loss, conversion and outcome analysis — that is four values, and a filter naming onlycompany_post_closeleaks the other three while looking correct. - New workspaces start in onboarding. A newly-created workspace is now born with the
self_serve_onboardingflag ON, so its members land in the connect-your-data funnel instead of the book-a-demo state. Existing workspaces are unchanged, and an operator can still turn it off per workspace. - Direct-add emails no longer mention a password. Adding a member by email sends a "you've been added, sign in" notice pointing at the login page, where sign-in is the usual one-time code. The previous new-user mail asked people to set a password and linked to a reset flow that does not exist in this product; worse, that mail was only sent if a recovery link could be minted, so a failure there created the account and sent nothing at all. The notice now always sends.
- Platform admins can seat someone who has never signed in.
POST /api/admin/workspaces/:id/membersacceptscreate_if_missing: true, which provisions the account for an unknown email (and emails them) rather than refusing withuser_not_found.
Fixed
- The
evalsMCP tool now documents every inputprompt-and-message-evalaccepts.reference_accounts(the companies your copy cites as proof) andartifact_typewere always forwarded but never advertised, so a caller reading the tool surface could not discover them. - Eval runs no longer fail with
report_unavailablewhen the judge returns a well-formed response carrying no scored dimensions. Both graders now treat that as a bad sample and re-draw, which is what the existing retry was always for. - When an eval run needs scoping, it now names
reference_accountsalongsideaccountif your copy cites several companies as proof. Previously it could only ever sayaccount, which is the company the message is going *to* — a different question, and the wrong field for copy built on named customers. - A banned phrase in quotation marks no longer fails the hygiene check. A message that quotes a word to disown it ("we never say 'revolutionary'") was being flagged for using it; that check now abstains rather than asserting a violation it cannot tell apart from a mention.
- `eval_version` moves to `2.31.0`, and rule-grader scores must not be pooled across it. A hygiene check that failed on a quoted mention now abstains, so the grader's pass flag and its fraction can move upward on those runs. The phrase list, the rubric, the bar, the judge and retrieval are all unchanged.
- The
reference_accountshint now finds companies named anywhere in your copy, including in long documents and regardless of capitalisation. It previously guessed at capitalised words near the top of the text, so a one-pager citing customers throughout got a hint built from its own title block, and names like11xanda16zwere invisible to it. No score changes — the hint is reported, never used to retrieve. - Connections no longer allow duplicate workspace integrations. A workspace holds at most one active connection per workspace-owned provider (your CRM, call recorder, Slack, etc.); a second connect of one that is already connected is rejected with a clear conflict, and reconnecting an existing connection still works as before. Personal mailboxes (Gmail / Outlook) and tracked social accounts (X / LinkedIn) stay multi-instance.
- `GET /connections` now returns only active connections by default (connected/syncing or ever-synced), hiding disconnected and never-synced sources. Pass
?include_inactive=trueto include them. - Abandoned and dead connector rows (never synced, no stored credential) are cleaned up, so they no longer linger on the Connections list.
- `data.status` now resolves on every eval read. Submitting a run answered at
data.status, but reading the run back nested it atdata.run.statusand the report read nested it again — so a hand-rolled poller written against the submit shape matchedundefinedforever instead of erroring. Both reads now mirrorstatusat the ROOT alongside the existing nested field, which is unchanged, so this is purely additive.GET /eval-runs/{id}/reportalso publishes a real response schema instead of an empty{}. The terminal success value iscomplete— nevercompleted. - The eval `mode` default is documented correctly again: it is `rewrite`, not `advisory`, and `gate` is a real third mode. The default moved to
rewritein 2.22.0 but three published surfaces still saidadvisory— theevals.runoperation description, the MCPevalstool'srunaction, and the OpenAPI spec generated from the first of those. A caller who took the docs at face value expected anchored suggestions against their own copy and got a full replacement, andgate(grade only what you sent and stop) was undiscoverable from the API reference. - The MCP server instructions no longer advertise a `search` action that does not exist. The standalone
search.runfast lane was retired — its warehouse behaviour issearch.querymodefuzzyverbatim — but the connected-client instructions still routed quick lookups tosearch { action: run }, described amode: blendedweb+news leg, and undercounted the tool set as four. A connected model following that text hit aninvalid_argumentat the moment it tried to act. The routing table now points atsearch { action: query }, the section documents the real lanes plus theasync/job_id/retry_guidanceloop, andevalsis listed alongside the other four tools. - `X-Request-Id` is now echoed on every response, and browsers can send and read it. The API reference promised an
X-Correlation-Idheader that has never been set anywhere in the server. The id that actually joins your request to our audit trail is the inboundX-Request-Id— so it is now returned on every response (your value when you send one, a minted one when you do not), added to the CORS allowed headers so a browser client can send it, and exposed so it can read it back. - Published rate limit corrected to 60 requests per minute, and the 429 body documented as what it really is. The page said 100 in four places while the platform enforces 60 per IP on
/api/platform/v1/*, and it printed a JSON error envelope for a response that is actually plain text. Branch on the HTTP status, not the body. The page also now notes the budget is counted per replica. - `search.query`'s `limit` default is lane-dependent, and the docs and schema both say so. It is
100on the filter and semantic lanes but50on fuzzy — where an unforced plain-language question usually lands — so a single documented "100" quietly halved the rows on the most common path. The async lane (async: true+job_idpolling), themax_subqueriesbreadth knob (5 synchronous, 12 async), andretry_guidanceare now documented too; all three shipped undocumented. - The BigQuery theme-index fallback's fields are documented correctly.
search.mdxnamedcluster_idandscore, which do not exist on that path; the real fields aresource_id,similarity,member_countandinterestingness_score. An integration written against the old text gotundefinedon every fallback row. - Both grader-kind lists now show all eight kinds.
figure_anchored(re-runs every figure against its own system-of-record query) andstructure(checks a report covers every must-cover record and pairs each risk with a next action) ship and are selectable by authored evals, but four places across two pages still said "the six grader kinds". - Eval reuse, the noise floor, and `enrich.company`'s inputs are described accurately. Reuse has two paths (an in-flight join at any age, and completed-run reuse inside a 15-minute window), not the one the docs described in two contradictory ways. The published run-to-run noise figure appeared twice on one page with two different values, both from a retired judge; it is now one attributed, dated figure measured on the live judge. And
enrich.companyacceptsdomain,name, or both —domainwas documented as required. - `search` hydrate now attributes the speaker from the utterance that actually matched. A hydrated row is a grouped turn — several atomic utterances folded into one row — and the pipeline stamps that row with its first member's identity. So whenever a turn spanned two voices,
speaker_name/speaker_email/speaker_title/speaker_typenamed the wrong person, silently. Re-measured live on 2026-08-15: 631 of 5,328 member utterances (11.84%) carry a turn-stamped speaker that is not their own, inside the 5.0–20.7% band published on 2026-08-11. [corrected 2026-08-16] That figure is one workspace (a7c1bd65), and it is the least representative on the fleet for this defect: every one of its multi-voice turns *also* mixes internal with external speakers, which a regroup clears — on every other workspace the reverse holds. The fleet rate, measured the same day across nine workspaces, is 7.70% of members mis-stamped (234,473 of 3,044,300) and 6.11% of turns holding more than one voice (134,510 of 2,201,887). Read the fleet figure as the general rate and the workspace figure as one workspace's. The speaker now comes from the atomicutterancessource for the sentence that matched, resolved in the same bridge read the turn lookup already performs — no extra round trip. - `speaker_attribution` on every hydrated turn-grain row says where the identity came from.
"utterance"is verified from the atomic source."ambiguous"means two matched utterances landed in this row and disagreed on speaker — the speaker fields come back null and nobody is named, because picking one is the defect this replaced."turn"means the atomic source had no row, so the fields are still the turn's first member and are not verified. - `speaker_ambiguous` is a different question, and both can be true at once. It says the row's text spans more than one speaker — 607 of 4,469 turns (13.58%) on the same tenant. [corrected 2026-08-16] That figure is one workspace (
a7c1bd65), and it is the least representative on the fleet for this defect: every one of its multi-voice turns *also* mixes internal with external speakers, which a regroup clears — on every other workspace the reverse holds. The fleet rate, measured the same day across nine workspaces, is 7.70% of members mis-stamped (234,473 of 3,044,300) and 6.11% of turns holding more than one voice (134,510 of 2,201,887). Read the fleet figure as the general rate and the workspace figure as one workspace's. A row can be correctly attributed and still contain another person's words, so quoting the wholecontentas one person is wrong there even when attribution is clean. When it fires,turn_speakerslists everyone in the row. Both fields ship on every turn-grain row including the clean ones: their absence means only that attribution did not run. - Stakeholder columns are dropped rather than left describing someone else.
role_level,is_champion,is_economic_buyer,champion_scoreandinferred_personaare derived from the turn's speaker (thedeal_stakeholdersjoin is keyed onspeaker_email). When a row is re-attributed to a different speaker they are set to null, because a retained value reads as a fact about the person now named in the row. - `corpus.speaker_ambiguous_rows` and `corpus.speaker_unattributable_rows` give the rates without scanning the rows. Emitted whenever attribution ran, including as 0.
- Still not fixed: reading the
interactionssurface directly —data.query, thesearchfilter lane, the NL→SQL lane — returns the turn stamp exactly as before. This change is scoped to the hydrated semantic path. The field catalog says so on each affected column. - `corpus.collapsed_matches` closes the hydration accounting.
row_countis normally lower thanmatch_count, and we told you that difference wasunresolved_matches. It mostly isn't. Several matched utterances routinely land in the *same* conversational turn and share one row — grouped turns hold 2.88 atomic utterances on average and 78.3% hold more than one, so about a quarter of matches collapsing is ordinary. Measured live: a 200-match search returned 149 rows withunresolved_matchesat 0, and nothing on the wire explained the other 51. They were not missing; they shared rows with matches you already had.match_count = row_count + collapsed_matches + unresolved_matchesnow reconciles. - `unresolved_matches` and `collapsed_matches` are emitted whenever `hydrate` ran, including when they are 0.
unresolved_matcheswas previously omitted on zero, which made "nothing was unresolved" indistinguishable from "this build predates the field" — so a real gap read as unaccounted-for. Absence of these fields now means only one thing: hydration did not run. - The identity can still leave a remainder when the gated warehouse read itself withheld rows — an
audiencepredicate, an excluded interaction, or yourlimitbinding.truncatedandaudience_appliedtell you when that happened. - `hydrate: true` now returns the utterance that actually matched. It bridged at call grain, so a hydrated result was the matched utterance's whole conversation under a row limit — and because those rows come back in no particular order, the matched turn was usually not among them. Measured on five semantic matches at limits 5, 50 and 200: zero of the five matched utterances present, every time. The rows it did return were real turns, from the right calls, carrying real labels, so the output looked correct while being unusable for the one thing hydration exists for. If you built citations on hydrated output, they may quote a sentence unrelated to the match — worth re-checking.
- The bridge is now turn-grain: one row per match, and that row contains the matched sentence, with full
contentpluscompany_name,speaker_titleand the rest of the label columns.corpus.row_countis therefore at mostcorpus.match_count, so comparing them is now meaningful. - `corpus.hydrated_grain` tells you which bridge ran.
turnis the row-per-match answer.callmeans the turn lookup itself failed and the rows are arbitrary turns from the right conversations — flaggeddegraded: true, and to be read as context rather than as the matches. We kept that fallback because returning the conversation beats returning nothing, but it is never silent. - `corpus.unresolved_matches` counts matches whose utterance belongs to no grouped turn and therefore has no warehouse row at all — 1% to 12% of external utterances depending on the workspace. Those are reported rather than back-filled with a neighbouring row, so nothing you cite is a substitution. When no match resolves, the response says so and skips the warehouse read entirely instead of falling back.
- `corpus.truncated` now also fires when your `limit` binds on the returned rows, not only when the 200-id bridge cap does. It was inferred correctly on the filter lane and simply not reproduced on the hydrated path, so a result cut by
limitread as complete. source_row_idstill does not join the filter lane, and that has not changed: it is an atomic utterance id, one level below the grouped turninteractionsis keyed on. The turn id that would join is whathydrate: trueresolves for you. We do not resolve it on every semantic call on purpose — it costs an extra warehouse read, and the un-hydrated lane exists to answer from the vector mirror alone in well under a second.- Outbound drafts — when the message generator judges a lead unfit for outreach, its refusal is no longer stored as a sendable draft awaiting approval. Previously the writer had no way to decline, so its reasoning ("this recipient is a student with no work history") landed in the message body at
review_status: 'pending'— one approval click from being delivered to the person it described. The generator now returns an explicit decision; a declined lead landsreview_status: 'skipped'with the reason onrejection_reason, and the lead is disqualified. Skipped drafts never appear in the review queue and cannot be approved or edited into the dispatch queue. - A rate limit on `POST /search/query` and `POST /data/query` is now an HTTP `429`, not a generic error inside a `200`/`500` envelope. It carries a
Retry-Afterheader with the exact seconds until the window rolls, therate_limitederror code, anddetails.retry_after_seconds/limit/window_seconds. Previously it surfaced as a flatoperation_error, indistinguishable from a malformed query — so a client could not tell "fix your SQL" (retrying is pointless) from "you are going too fast" (retrying is the entire fix), and any retry logic keyed on HTTP status never fired. If you built a workaround that detects the limit by matching the error message, you can now delete it and branch on the status. - The per-endpoint query budget is now discoverable. These endpoints carry their own budget of 10 queries per minute per user, underneath the 60/min account budget that
GET /mereports — pacing against 60/min was spending a budget of ten. Every response now advertises the real one inX-RateLimit-Limit/-Remaining/-Reset, so you can pace from a successful call instead of discovering the cap by tripping it. The rate-limits guide documents it; it was previously listed as unreleased roadmap. - **A query that is malformed *and* rate limited now reports the syntax error, not the throttle.** The rate-limit check ran inside the validation-failure branch, so the real error was discarded and callers backed off and retried the same broken SQL.
- Warehouse failures on `POST /data/query` are now `500`, not `400`. Every thrown error on that route was reported as
400, so a BigQuery timeout told you your request was bad and suppressed the retry that would have worked. The three cases are now distinct:400 invalid_sql(change the SQL),429 rate_limited(wait),500 query_failed(retry with backoff). - `POST /context/ask`, `/context/substrate/query` and `/context/remember` now return `500` for server-side faults instead of reporting everything as
400. Genuine input errors (a badintent, an over-longtext) remain400. - A failed cluster read on `POST /data/clusters/search` now returns `500` instead of a `200` that looked identical to "this workspace has no themes." A workspace with zero clusters, and a query that matched none, are still honest
200s. - On these endpoints the
X-RateLimit-*headers now describe the per-endpoint budget rather than the account budget — the binding constraint for that call. Same header names and the same Unix-secondsX-RateLimit-Resetformat as before, so no client-side change is needed. - An agent run that saved its work to the knowledge base no longer reports itself as failed. When an agent finishes a turn it can hand back the id of the deliverable it produced. That id is stored in a column that only accepts *artifact* ids, so an agent whose deliverable was a knowledge-base document — every living-document workflow — handed back a perfectly real id the column could not hold. The database rejected the write, and because that write is the same one that records the run as finished, the whole thing was lost: the run had already done its research, saved its document and sent its email, and was then recorded as an error with a raw database message attached. The id is now checked before it is stored and simply dropped when it does not name an artifact in your workspace, so the run finishes and is recorded as complete. Nothing about the work changes — the document, the email and the run's own summary were always correct — and the id remains visible in the run's tool history either way.
- `SOC 2` now finds rows that say `SOC2`, and an empty answer never hides a spelling it could not search. Full-text search treats
SOC2andSOC 2as different words, so a quoted query for one was invisible to every row written the other way — and came back asempty_reason: "no_match", the one empty value that means "nobody said this" rather than "something went wrong". Thelexicalmode now searches both spellings of any term whose letters and digits can be written apart or together (SOC 2/SOC2,ISO 27001/ISO27001,O365,2FA), and reports what it did undercorpus.spelling_variants: which spellings it searched, how many rows each found, and how many survivors yourlimitcut. When results come from more than one spelling they are ranked by rank fusion, because the per-rowscoreof one spelling is not comparable with the other's. If a spelling could not be searched at all, the reply saysempty_reason: "spelling_variants_unsearched"instead ofno_match— so an empty answer can always be told apart from a mangled query. - A curly-quoted phrase now scopes the search, the same as a straight-quoted one.
did anyone mention “data residency” concernssearched the whole sentence, letting "anyone", "mention" and "concerns" compete with the phrase you actually quoted — the failure straight quotes were fixed for. Anything pasted from a document editor with smart quotes turned on was affected, and it failed by returning plausible-looking wrong rows rather than an obvious empty. - Semantic search was silently returning a fraction of your matches, and small workspaces had it worst. The vector index is shared across workspaces and does not carry the workspace key, so the similarity walk ranked across everything and your workspace filtered the survivors afterwards — at the database default a workspace holding 2.5% of the index recovered a mean of 6.5 of 10 rows against the true nearest neighbours, and 1 of 10 in the worst case. It never over-returned and the top hit was always right, so nothing about it looked wrong. Reported by a caller whose objection theme came back with zero hits and demonstrably exists in the corpus. The search now widens its candidate pool per query: 9.75 of 10 against exact, measured on the same workspace.
HNSW_EF_SEARCHtunes it without a deploy. - This is a mitigation, not a cure. The durable fix is partitioning the index per workspace so each one searches its own graph; that is not in this release. Until it lands, treat a thin semantic result as thin retrieval, not as evidence your corpus lacks something —
mode: "filter"is the lane that answers absence questions. - `search.fields` was advertising incomplete value lists for six coded fields, and none at all for `pushback_type`. A filter on a value that does not exist returns zero rows, which is indistinguishable from a value that exists and matches nothing — so an incomplete list reads as a complete one right up until it quietly costs you results. Corrected against production:
pushback_typenow published (10 values;build_vs_buyandincumbentwere unreachable before),speaker_typegainsunknown(131,505 rows),record_typegainstaskandevent,interaction_purposegainsinternalandunknown,interaction_typegainsEVENTandTASK,deal_stage_categorygainsN/A. - `sample_values` now has stated semantics. A listed value promises the spelling is right and the pipeline can emit it — not that your workspace holds rows carrying it. Its absence is deliberate: fields whose values come from your own CRM configuration (
deal_stage_normalized, stage labels, ids, free text) carry no list, because any list we published would be one workspace's tail rather than a closed set. For those, group by the column and see what you actually have. - Semantic search now keeps up when a conversation stops qualifying, instead of only at the moment it is indexed. Applying your data filters at indexing time fixed what gets collected, but it could never revisit a decision already made — so a conversation that qualified when it was indexed stayed searchable forever, even after it stopped qualifying. That is not a rare event: on one workspace, 71% of its excluded conversations are excluded not by a filter anyone edited but by whether a matching CRM record exists yet, which changes on its own every time CRM data syncs. A filter you switch on today, or a CRM match that lands overnight, therefore left behind results that kept coming back. A new ongoing pass re-checks already-indexed utterances against your current rules and removes the ones that no longer qualify, so the index follows your filters over time rather than freezing at whatever was true on the day each conversation was collected. It also catches utterances whose speaker was later reclassified. The pass is deliberately conservative — it only removes an utterance when your workspace positively reports it as no longer qualifying, never merely because it could not be checked — and it works through each workspace steadily rather than all at once, so a filter change is reflected within a few days rather than instantly. No filter configuration needs revisiting, and nothing about the request or response shape changes.
- Semantic search no longer returns a partial answer without saying so. Results for a workspace were drawn from a shared index and filtered afterwards, so when another workspace's conversations happened to be dense around the same topic, they could consume the whole candidate pool and your search would quietly come back with one or two results instead of ten — at full confidence, with nothing to indicate the rest were missing. Measured on the affected workspace, a colliding query returned 1 of 10 true matches. Those searches now keep looking until they've actually found your matches, so the answer is complete. They take longer in that case (around a second rather than instant); workspaces with their own dedicated index are unaffected and unchanged.
- Semantic search no longer returns conversations you excluded with data filters. Every other read honours your filters, but the index behind the
semanticlane was built without them, so utterances from conversations you had deliberately switched off could still come back as matches — measured at 22.4% of all indexed utterances across the fleet, and roughly 28–30% for the most affected workspaces. New indexing now applies the same exclusion rules as the rest of the product, and the utterances collected under the old behaviour are removed in a follow-up pass. Nothing about the request or response shape changes, and no filter configuration needs revisiting. - Semantic search (
searchsemantic lane,enrich,lookalike themes) now returns materially more of what your corpus actually contains. Similarity reads previously ranked candidates across all workspaces and filtered to yours afterwards, so a smaller workspace received only its share of a fixed candidate budget and quietly lost true matches — measured as low as 5.3 of 10 against an exact scan, with individual queries returning nothing for a topic that was present. Each workspace now searches its own vector index; the same measurements land at 8.6–10 of 10. Nothing about the request or response shape changes. - Utterances whose embedding arrived late are no longer missed permanently by semantic search. The sweep that fills the index behind the
semanticlane walks forward through *event* time, which quietly assumed an utterance is ready to index by the time the sweep reaches its timestamp. Embedding is a batch job, so that assumption fails constantly: measured across the fleet, 70,187 utterances became indexable in a single week and 26,827 of them hung off conversations more than 30 days old — already behind the sweep, and therefore never picked up. A second pass now walks *eligibility* time, so an utterance that becomes indexable long after the conversation happened is still collected. It also covers conversations backfilled late, since nothing can be embedded before it exists. Counter-intuitively this mattered most for workspaces whose indexing was fully caught up — a sweep that is still behind collects late arrivals on its way past, and only a current one can be overtaken. - Semantic search now covers every active workspace, not just ones that had used chat. The vector mirror the
semanticlane reads was populated by a sweep that borrowed its tenant list from an unrelated pre-warm job, which selected workspaces by recent *conversation* activity. That is a sensible rule for deciding whose briefs are worth pre-warming and the wrong one for deciding whose data gets indexed: a workspace that had connected its sources but never opened a chat was skipped entirely, so its first semantic search — and every one after — silently fell back to the coarser theme index instead of matching real utterances. The sweep now selects on the workspace's own active status. Affected workspaces populate on the next sweep with no action needed;corpus.storeon asearch.queryresponse tells you which index answered. - Deal health no longer scores the wrong person. A row on the
interactionssurface is a grouped turn — several utterances folded into one row and stamped with its first member's name — and/deal-healthused toGROUP BYthat stamp, count the groups and score the counts. On a turn holding two voices that credited one person with the other's words and with the champion / economic-buyer signal derived from them. The endpoint now reads at turn grain, verifies every speaker against the atomic utterance table, and aggregates on the verdict. 11.84% of member utterances carry a stamp that is not their own and 13.58% of turns hold more than one voice (measured live 2026-08-15), so scores move. [corrected 2026-08-16] That figure is one workspace (a7c1bd65), and it is the least representative on the fleet for this defect: every one of its multi-voice turns *also* mixes internal with external speakers, which a regroup clears — on every other workspace the reverse holds. The fleet rate, measured the same day across nine workspaces, is 7.70% of members mis-stamped (234,473 of 3,044,300) and 6.11% of turns holding more than one voice (134,510 of 2,201,887). Read the fleet figure as the general rate and the workspace figure as one workspace's. - What `champion_score` means now. Its arithmetic is unchanged; its inputs are narrower and correct.
interaction_countcounts calls the person was verified to have spoken in — which goes up for anyone who habitually shares a turn, because a co-speaker the stamp hid is now visible — andsentiment_countscounts only turns that resolve to that person alone, becausesentiment_primarylabels the turn's whole text and a two-voice turn's text is not one person talking. A turn whose membership cannot be read contributes nothing. Expect scores to move in both directions, and read the new per-stakeholderattributionblock (attributed_turns/ambiguous_turns/unverified_turns) for why. - `is_economic_buyer` now requires a verified identity. It used to be
role_level === 'executive'off a title that came from the turn stamp, so a group's first member could become the deal's economic buyer on somebody else's words. An identity nobody could check is no longer eligible, and its calls are reported separately asunverified_interaction_count. - `speaker_grain` on every deal-health response says whether the atomic source was consulted and how many rows were attributed, ambiguous, or unattributable. If the membership read fails, deal health fails closed:
attributed: false,reason: "bridge_unavailable", no champion and no economic buyer named — the people, titles and companies still come back. Showing a champion nobody verified is worse than showing none and saying so. - Deal-health events see the second voice too. A call where two people shared a turn no longer looks single-threaded:
multi_threadnow fires on it and carriesverified_speaker_countalongsidespeaker_count.executive_entryrequires a verified identity — an unchecked stamp raises a plainnew_stakeholdercarryingspeaker_attribution: "turn". Stakeholders and events are also now derived from one read of the warehouse rather than two, so they are guaranteed to describe the same population. - Outbound stops putting an unverified name in a sent message. The call-mention signal wrote
Mentioned by <name>straight into the generated opener; on a two-voice turn that was the wrong person. It now names someone only when the atomic source verified them, and otherwise says "our team" — true of every row it reads. The transcript enrichment tool returnsspeaker_attributionon each mention and omits the name on an ambiguous turn;mention_countis unchanged, since it counts content matches rather than speakers. - Still not covered: the testimonials people picker, the sales-digest transcript, the listening-lens documents, cluster representative quotes, the people-matching and company-catalog facets, pre-generated insight context, and the public page-embed renderer. They mislabel rather than miscompute, and they are named here rather than left silent.
- Speaker attribution now runs on every gated read of `interactions`, not just hydrated semantic search. A row on that surface is a grouped turn — several atomic utterances folded into one row and stamped with its first member's identity — so
speaker_name/speaker_email/speaker_titlenames the wrong person on 11.84% of member utterances, and 13.58% of turns hold more than one voice (measured live 2026-08-15). [corrected 2026-08-16] That figure is one workspace (a7c1bd65), and it is the least representative on the fleet for this defect: every one of its multi-voice turns *also* mixes internal with external speakers, which a regroup clears — on every other workspace the reverse holds. The fleet rate, measured the same day across nine workspaces, is 7.70% of members mis-stamped (234,473 of 3,044,300) and 6.11% of turns holding more than one voice (134,510 of 2,201,887). Read the fleet figure as the general rate and the workspace figure as one workspace's. The previous release fixedsearch.querywithhydrate: trueand said plainly that reading the surface directly still returned the stamp. It no longer does:data.query, thesearchfilter and fuzzy lanes, the curated fast-lane templates, page-template SQL and blueprint query steps all go through one SQL gate, and the gate now resolves the turn's real membership from the atomicutterancestable before the rows leave it. - What you get back on a turn-grain row.
speaker_attribution: "utterance"means the speaker was verified against the atomic source — a correct stamp is now distinguishable from an unchecked one, which it never was before."ambiguous"means the turn holds more than one speaker: all four speaker fields come back null,turn_speakerslists everyone in play, and nobody is named."turn"means the value is still the unverified stamp.speaker_ambiguousremains the separate question of whether the row's text spans more than one speaker. - Stakeholder columns travel with the speaker.
role_level,is_champion,is_economic_buyer,champion_scoreandinferred_personaare derived from the stamped speaker, so they are nulled alongside it rather than left describing someone else. - Two shapes cannot be repaired, and now say so instead of staying silent. A projection with no `interaction_id` cannot be resolved to its turn, and an SQL-side aggregate (
GROUP BY speaker_name) folded the stamps before anything could check them. Both come back with every row labelledspeaker_attribution: "turn"and an envelope block,speaker_grain, carryingattributed: falseplus the reason (no_turn_key). The label ships on the row, not only in the envelope, so it survives being copied out. Select `interaction_id` alongside your speaker columns and you get verified attribution instead of the label. - `speaker_grain` on `data.query` and on `search.query`'s `corpus` block names the columns at risk, whether the atomic source was consulted, and — when it was — how many rows were attributed, ambiguous, or unattributable. It is emitted whenever a speaker-bearing column is returned, including when nothing was wrong; its absence means only that no such column was in the projection.
- The objection / pain-point / feature-request / success-story / competitive-mention fast-lane templates now select `interaction_id`, which is what turns their "here is a quote and here is who said it" output from an unverifiable stamp into a checked attribution.
- `speaker_type` is unaffected and is left alone. Grouping breaks a turn precisely when
speaker_typechanges, so every member of a turn shares one — a query selecting onlyspeaker_typegets no label and pays no extra read. - Not covered by this change: paths that read the warehouse outside the SQL gate — the
/deal-healthstakeholder endpoint, the testimonials people picker, the sales-digest transcript, the listening-lens documents, cluster representative quotes, the outbound call-mention and transcript tools, and the agent BigQuery tools. They still return the turn stamp. The deal-health endpoint is the one where it compounds rather than merely mislabels: it derives champion and economic-buyer identity from the stamp, so a mis-attributed turn moves a score.
Breaking
- Evals: `overall_score` is now the grade of the copy YOU submitted. It used to be a mean over every applicable grader in the case, and those graders judge two different artifacts — the hygiene rules score the text you sent,
improvement_loopscores the improved version the eval wrote. The mean of the two was a grade on neither, and it was the field at the top of the payload, so it was the one that got quoted: a live run returnedoverall_score: 1.0for a draft that cleared 2 of 5 rubric lines. It now reports the submitted side, on the same[0, 1]axis, read from the same derivation asverdict.headline.submittedandcompare.submitted_score— so those three cannot disagree. Nothing about the grading changed: same rubric, same customer evidence, same judge, same prompts. What changed is what one stored field denotes. The run'spass/partial/failbucket still follows the IMPROVED side, so apassbeside a low score is now the ordinary shape of "your draft did not clear the bar, the rewrite did" rather than a contradiction — gate on the verdict, read the score.nullon a refusal is unchanged (never0, withnot_applicable_reasonbeside it), and an eval with noimprovement_loopgrader is unaffected, because every applicable grader there already judged what you submitted. - Evals: the boundary is `eval_version` 2.14.0, and scores must not be pooled across it.
prompt-and-message-evalmoved to2.14.0with the change above. That version rides the run fingerprint and is stamped on every stored run, so it is what tells you which reading a row carries — the two readings share a name, a type and a range, and nothing else distinguishes them. If you average, trend, threshold or sort `overall_score` across stored runs, filter on `eval_version` first, or read `verdict.headline.submitted.score_15` instead — that field means the same thing on both sides of the line and is present on (or derivable for) every run.GET /eval-runs/{id}/compare/{other_id}does not check versions:score_deltadifferencesoverall_score, so a comparison spanning 2.14.0 differences two different quantities; readsubmitted_score_deltathere, which is derived from the improvement report and is unaffected.GET /evals/<slug>/kpialready tracks the submitted side from the headline and needs no change.
Added
- Eval runs now record and report who fired them and from which surface:
triggered_by_kinddistinguishes console / api / mcp / agent (was alwaysmanual), and run reads carry an additivetriggered_by_name. - Evals: every report now hands back the calls that produced its evidence. The report already told you which quotes backed a claim; it now tells you how to get those quotes yourself.
grader_meta.evidence_provenance.requestsis a list of{ op, params }— the actual reads the run issued (data.cluster_searchwith its query and limit,data.querywith its full SQL) — deduped so N quotes drawn from one leg appear as one call, not N. Run them against your own workspace to reproduce the evidence, change them to explore adjacent ground, or paste them into a prompt so your own agent retrieves the same way. Everything else in the provenance block is a count, and there is nothing you can do with "we retrieved 60 quotes"; this is the part you can act on. Your key supplies the workspace, so no tenant id appears in any request — a call that looked copy-pasteable while naming someone else's workspace would be worse than one that omits it. The field is absent (not empty) on a run that reused pinned evidence rather than issuing calls, because that run genuinely made none. In the console the same list renders under "Show the N calls behind this evidence", each with a copy button. - Evals: a run now tells you which input would have made it better, and what to send. Account tier is the only evidence tier that licenses a claim about what a specific company said — and a run given no
accountcannot reach it, which is why suggested research steps kept coming back with an unfilled{{account_name}}in them. The verdict now carriesrequires_input: the field that was missing, why it matters, and — when a company named in your draft resolves against your owncompany_namerows — the exact value to re-run with, plus how many utterances sit behind it so thin coverage is visible before you spend a second run. It is reported, never applied: nothing on the payload was retrieved using a suggestion, and the run graded exactly what you sent. A wrong account would attribute one company's words to another, so the value is offered and never assumed — only anaccountyou send changes what gets retrieved.suggestedis omitted rather than guessed when nothing verifies, and the whole field is absent when the run had what it needed. There is deliberately no interactive question: a tool that blocks to ask would deadlock every headless caller, so the gap ships as data and your agent decides whether to re-run with it, put it to a human, or accept corpus-tier grading. - Eval runs now hand back the evidence pin instead of expecting you to know it. Every settled run whose quote pool was recorded carries a
regradeblock onGET /eval-runs/{id}andGET /eval-runs/{id}/gate— the pasteableevals.runparams (evidence_from_runplus the eval slug) that grade your edited draft against the SAME customer quotes, so the score delta is your edit rather than a retrieval change. The pool itself is now readable atGET /eval-runs/{id}/evidence(MCP: theevidenceaction on theevalstool): every frozen quote with its id, tier and provenance, exactly what a pinned re-grade is scored against. Pinned runs also stopped losing quote provenance — the stored speaker side, spoken-at date, seniority and deal stage now survive the read, so a pinned report renders the same attribution as the run that first retrieved the quotes. - Evals: `GET /eval-runs/{id}/evidence` now says which pooled quotes the judge actually read. A run retrieves a pool (often ~60 quotes) but only its leading slice reaches the graded prompt, and
grader_meta.evidence_provenancehas always reported both counts — so a smallcitedagainst a largepoolwas explainable but not checkable. Every quote on the pool read now carries `reached_judge`, and the set carries `in_context` (how many leading quotes went into the prompt). Readreached_judgeas THREE states:truereached the judge,falsewas retrieved but never shown, and ABSENT means the run recorded no count — runs graded before this shipped, and the field is omitted rather than defaulted tofalse, becausefalsewould assert that a quote the model demonstrably read never reached it. The count is written by the grader that froze the pool rather than derived from a constant, because graders differ:improvement_loopsends a 20-quote prefix,evidence_judgesends its whole retrieval. - Evals: a run whose only evidence grader is `evidence_judge` now records a pool at all. Previously only
improvement_looppersisted one, soGET /eval-runs/{id}/evidenceansweredevidence: nullfor those runs andevidence_from_runhad nothing to pin. Both graders now record. On an eval declaring both, theimprovement_looppool wins — it is the one the rendered report is assembled from — and the second write never overwrites it. - Evals (wire change, beta endpoint): pooled quotes on `GET /eval-runs/{id}/evidence` are now snake_case.
speakerSide/spokenAt/roleLevel/dealStageAtTimebecomespeaker_side/spoken_at/role_level/deal_stage_at_time, matching the cited quotes in a verdict exactly. The pool read was emitting stored keys verbatim, so one utterance arrived in two different shapes depending on whether you read it from the pool or from the grading — anything rendering both had to reconcile them by hand. Pooled quotes still carryidand carry nostance(a stance is a judgement about a quote the judge cited). - Export your graded runs as training data.
GET /evals/export.jsonl?format=pairs|sft|judgereshapes each line into a training record instead of the stored verdict:pairsis preference tuples (your draft asrejected, the improved version aschosen) for DPO/ORPO,sftis instruction to completion with the improved copy as the target, andjudgeis the grading task itself - artifact and evidence in, per-dimension verdicts out. Read this before you train on it: both sides are model-generated, and the same rubric wrote and graded them, so these are distillation examples - a smaller model learning to imitate this pipeline - and never ground truth for what a human expert would write, nor evidence that the improved copy performs better with buyers. Every record carries that statement on its own face (provenance.both_sides_model_generated), per record rather than in a header, because training pipelines shuffle and split and a header is lost on the first of those. A training file pins one `eval_version` and is refused otherwise, naming the versions you have so you can pick: the rubric and the generation prompt both changed across versions, so "chosen" does not mean the same thing in 2.12 as in 2.20 and a mixed file trains toward the average of two standards while looking like one dataset. Rows we cannot honestly label are left out rather than labelled anyway - a pair whose lift sat below the measured noise floor (the run's own instrument declined to claim a difference), improved copy still carrying a[placeholder]or an<unverified>marker, a "before" the eval wrote itself on a prompt-only run, and the second grading of a message already in the file. - Evals now serve pipelines, not only people.
inputs.mode: "gate"grades ONLY the copy you submitted and stops (no rewrite, no suggestions - one judge call, so markedly cheaper and faster), andGET /eval-runs/{id}/gate(also the MCPevalstool'sgateaction, with the samewait_mslong-poll) returns the machine verdict on every run:gate.passed,checks_passed/checks_total,score_15vsthreshold,basis,simulated, anddimensions_failed. Wire THIS into a send/hold or CI gate - neververdict,overall_scoreorlift, which follow the rewrite the eval wrote and pass ~92% of runs.gateisnullwhile running and on a refusal (not_applicable_reasonsays why); a refusal is not a fail.prompt-and-message-evalbumps to 2.17.0 (definitional; existing modes grade byte-identically). - Evals: the improved message is now checked for people nobody can identify. The rewrite could name a third party it lifted out of a customer quote — "Colin's idea stuck with me too" — and pass every existing check, because it _was_ grounded: a faithful paraphrase of a real retrieved quote. Grounding asks whether cited evidence resolves; it cannot ask whether anyone knows who the person IS. On the run that prompted this, that name appeared exactly once in the whole workspace corpus and matched no contact, no user and no speaker anywhere. The improved-message facet now reports
names_total(attribution phrases found, the denominator),names_unlicensed(those naming someone you never named in your own prompt or draft) andnames_annotated, and an unlicensed name is wrapped in the same<unverified>markers an ungrounded claim already gets. The rule needs no roster: retrieved quotes deliberately carry no speaker identity, so a name lifted from quote text is unverifiable by construction, and your own words are the only place a person is confirmed. Detection is deliberately narrow — it matches attribution phrasing (Dana's suggestion,Sam said,per Alex), so an organisation possessive like "Gong's analysis" is not flagged, and a bare mention with no attribution is not caught. - Evals: `prompt-and-message-eval` is now `eval_version` 2.20.0, and your scores are still poolable across it. Unusually for a bump, nothing about the grading moved: no prompt changed, no model call was added, and the markers are applied after the blinded judge has already graded the clean copy — so
score_15, the dimensions, the lift, the transition andoverall_scoreall describe exactly what they did at 2.19.0. The boundary exists for one reason, and it matters if you post-process the text: if you re-derive anything from a facet's `text`, you must now check `claims_annotated` _and_ `names_annotated`. Since 2.15.0 the rule was thatclaims_annotated: 0meant the field was byte-identical to what the judge scored; a second, independent source can now mark the same field, so that check alone is no longer sufficient. Both counters0(or absent) means the same bytes. Both sources share one marking pass and one marker vocabulary, so stripping markers still recovers the graded text exactly. - Evals: `GET /eval-runs/{id}/drafts` returns the two graded texts side by side. The run grades a pair — the prompt and message you submitted, and the improved versions it wrote — and no read returned the pair: the report card renders scores, findings and quotes but never the texts,
/evidenceis the quote pool,/gateis the machine verdict. The only path was the full-run read, which measures p50 65 KB and p95 84 KB, so a size-capped client could truncate it and drop exactly the copy you submitted while leaving every score intact. The new read is bounded (per-dimension quotes stay on/evidence) and carries each side's score, checks fraction and per-rubric-line reasoning, so a line-by-line comparison is one call. Also available as thedraftsaction on the MCPevalstool. Three fields tell you what you are holding:sourcenames which stored copy answered,annotatedsays whether<unverified>markers were stripped from it, andabsent_reasonsays why a side is empty — a refused run reportsimproved: nullwith a reason rather than an empty string or a zero. - Evals: an oversized read now says so instead of being silently cut. When a read's payload crosses its declared size budget, the response carries a
payload_notenaming the size and the narrower reads that return the same information in a bounded shape.GET /eval-runs/{id}declares one, pointing at/drafts,/report,/gateand/evidence. Nothing is dropped from the payload — the note is additive, so an existing integration is unaffected — but a caller who previously concluded a field did not exist can now tell that the read was too big for their transport. - Take your graded eval runs somewhere else.
POST /evals/exportreturns flat rows for analysis (also theevalsMCP tool and the Anthropic surface);GET /evals/export.csvandGET /evals/export.jsonlstream the same filtered set as a file - CSV a row per run, JSONL the stored verdict verbatim. Filter byeval,eval_versions,from/to,statusesandverdicts. Callcount_only(orGET /evals/export/count) first: it answers over the identical filter and reports the version spread, so you learn your rows were scored by more than one instrument before you pool them rather than after. Two refusals rather than a quiet wrong file: over 5,000 matches is refused with the real total and the remedy (narrow the range, or passallow_truncationto take the most recent 5,000 deliberately), and every number that needs a qualifier ships beside it -overall_scorewithapplicable(a blank cell is never a zero),liftwithlift_reportable, the submitted score withinput_simulated. You export your own runs; a workspace admin exports the workspace.redact_quotesblanks the verbatim customer quotes on the JSONL download; CSV never carries quote text. Scopeevals:read. - Evals: `GET /eval-runs/{id}/improvement` — the before/after report in a shape that fits your transport, because the advice we were giving you did not. A completed run's detail payload runs 65–205 KB, so it attaches a note telling you it may be truncated and pointing at four narrower reads. None of those four carried an `improvement` block. So a caller who did exactly what we told them lost
improvement.facets[]— the per-facet graded pair, with each side's score, per-rubric-line verdicts, cited quotes and not-applicable counters — and reasonably concluded the field comes back empty. It did not: measured on the run that produced this report,facetshad length 2 with both counters populated. The advice was the bug. The new read returns the whole report — every facet, pluslift,transition,suggestions,prompt_patch,research_steps,confidence,coverageand thebefore/aftermirror — and omits exactly two fields, which it names on every response in anomitted[]saying what each was and where it still lives:grader_meta(blinding and evidence-provenance disclosure) andstage_trace(per-stage timings). Those describe how a run executed rather than what it found, and they are ~38% of the block's bytes, which is what lets the rest fit — 41 KB measured on a run whose full payload was 96 KB. It is on REST (GET /eval-runs/{id}/improvement), the Anthropic read surface, and MCP both as the resourceeval_run://<id>/improvementand as a newimprovementaction on theevalstool — the sibling reads the note names all had an action already, so following the advice should not have meant dropping to a raw resource read for the one door carrying the facets. It polices its own budget like every sibling: a run that still exceeds it says so rather than being quietly trimmed. A run that produced no report returns `improvement: null` with an `absent_reason` — never an empty object and never an emptyfacetsarray, because an empty array is exactly what a caller coalescing an absent parent already sees, and the two must not be the same bytes. Separately, the truncation note itself now says what each narrower read carries instead of listing bare URIs and claiming they all hold "the same information" — that claim is what sent the original caller through the wrong door. - Evals: `verdict.headline` now carries `stop_reason`, so you can tell a converged rewrite from a cut-off one without walking the report. The revision loop already recorded why it stopped —
threshold_met,no_critique,deadline,model_call_cap,max_iterations— but only atcases[].graders[].improvement.transition.stop_reason, six levels down. The headline is the projection the docs point gating callers at, and it carried only the English sentence ("The revision loop ran out of time before it converged") buried inconfidence.reasons[], which is not something to branch on. The enum now rides the headline too, read off the same transition block so the two cannot disagree. Worth knowing what it means when you see it: `deadline` is the expected outcome, not a malfunction — at the shipped configuration a run whose first round misses the bar and still has a critique to act on will usually report it. It costs you a further revision pass and no score: the reported grade is the first round either way, andoverall_scoreis the submitted side alone, so a cut loop cannot move a number in either direction. Treat an absentstop_reasonas unknown rather than as converged — the wholetransitionblock is omitted when the unpaired absolute grade is unavailable. The docs also now state where theimprovementblock lives and why it can look empty:facetsis never returned empty, so an empty read means the whole parent was absent, which happens inmode: "gate"and on every refusal (empty_corpus,not_outreach,generate_failed, …) — readnot_applicable_reasonbeside it. The narrower/drafts,/report,/gateand/evidencereads carry noimprovementblock at all, which matters because a large detail payload emits a note pointing you at exactly those. - `GET /console/data-filters/manual-excludes` — read back what you excluded by hand. Excluding a single conversation stores its id and nothing else, so the console could only list raw ids: unreadable, and therefore un-undoable once you forgot which was which. The new read resolves each id into the conversation's title, kind, connector, linked company and date, plus who excluded it, when, and the reason they gave. Also available as
console://data_filters/manual_excludes(scopeconfig:read). What it does NOT do is guess: metadata we cannot resolve comes backnullrather than blank-looking defaults, an id the warehouse has no row for still appears in the list (it is still excluding data),totalcounts the exclusions on the rule rather than the rows that happened to hydrate, and a warehouse outage setsmetadata_unavailable_reasoninstead of failing — so "this conversation has no title" stays distinguishable from "we could not look anything up". An exclusion whose author cannot be established reportsexcluded_by: nullin preference to a name that might be wrong.
Changed
- The eval report card renders plain ASCII hyphens.
GET /eval-runs/{id}/reportreturns markdown built to be pasted verbatim, and it shipped an em dash in its## Eval reportheading on every run plus four more in its own sentences - the one surface in the product whose whole job is to be copied out, demonstrating the punctuation the house style tells every model not to emit. Only the punctuation moved: no field, score, verdict, lift or threshold changed, and theheadlineobject beside the markdown is byte-identical to before. If you match on the report's heading text, match on## Eval report - <slug>rather than the em-dash form. - Authoring an eval now rejects stylised dashes in the text you write. A rubric line, case label, description or field help you author is rendered into the judge's prompt alongside the house style rules, so a curly dash there teaches the model by example while the rules above it say not to.
POST /evals/PATCH /evals/{slug}(and the write-freePOST /evals/validatedry-run) now return avalidation_failednaming the exact field path, and the fix is one character. This checks only the four non-ASCII dashes; quotes, ellipses and other typography in your text are untouched. - Evals over MCP: the `status` action now tells your agent what to do when a run was missing an input.
requires_inputshipped on the verdict a version earlier, but the tool description never mentioned it — so an agent reading the report had a field it had no reason to look at, and the follow-up run that actually uses it never happened. The description now names the field, saysaccountis the usual one, and states the part that decides whether re-running is safe: nothing was retrieved using a suggestion, so the value only takes effect when you send it. Nothing about grading changed and no number moves; this is the signal that makes the loop closeable without a human in it. - Grading changed in five ways at once, and scores from before this release cannot be compared with scores after it. The judge now sees a worked pass example and a worked fail example under each of the five rubric lines, so the same draft is measured against the same stated bar every time. Your draft's own score is now produced by grading it on its own, without the rewrite beside it. On our test set that reads about 0.3 checks lower and varies about three times less between runs, so a lower number after this release is the measurement changing rather than your copy getting worse. The lift between your draft and the rewrite is still a paired comparison and is unaffected. Declaring an
artifact_typenow also tells the writer what it is writing (previously it only told the grader), so for landing copy the rewrite stops adding a single named account and a send/hold rule. A declaredartifact_typealso now settles on its own which rubric lines do not apply, without the grader overriding it. - Export downloads stay limited to one a minute per person, but the limit is now shared across replicas and the refusal tells you exactly when it lifts. A 429 carries
retry_at(an absolute instant) alongsideretry_after_seconds, andRetry-Afteris the real remaining time rather than a flat 60 regardless of how much of the minute had passed.GET /evals/export/countreports the livecooldowntoo, so a UI can show a correct timer the moment it opens and after a page reload - an absolute instant survives a reload where a countdown cannot. Two smaller fixes ride along: a request refused as invalid (a typo in?eval=) no longer costs you the minute, and the limit no longer depends on which server answered - it used to be per-process, so two clicks a second apart could both succeed and two a minute apart could both fail. - The eval-run file exports have no row cap.
GET /evals/export.csvandGET /evals/export.jsonlnow page through the match set with a cursor and write each page to the socket as it arrives, so peak memory is one page whatever the filter matched - a 40,000-run export costs the server the same as a 400-run one. The 5,000-row refusal is gone from those doors: a workspace's own runs are its own data, and the cap was a memory bound rather than a policy.POST /evals/exportkeeps the cap, because it holds every row in one JSON response body; its refusal now names all three remedies (narrow the range, setlimit, or take the file). They also compress - sendAccept-Encoding: gzip(every browser does;curlneeds--compressed) and the body arrives gzipped underContent-Encoding: gzip, roughly a tenth of the bytes, decompressed transparently and saved under the same filename. Paging is keyset rather than offset on purpose:eval_runsreceives writes while an export streams, and an offset counts from the top of a set growing underneath it, so it silently skips rows it never showed and repeats rows it already did - neither visible in the finished file. - New `limit` on every export door - take the most recent N runs. A deliberate top-N, not a truncation: it never needs
allow_truncationand is never refused for matching a larger set. On the file doors it is the only thing that bounds the row set. - New `verdict_counts` on the export count - the per-bucket split (
pass/partial/fail/not_applicable) plusungraded, the runs carrying no verdict at all. Those runs have an emptyoverall_scorecell (a refused run was never graded, and a zero would report the worst possible grade), so averaging that column without knowing how many empties are in it is the easiest way to misread an export - now you learn the number before you download rather than after.ungradedis deliberately kept out ofbuckets: absence of a verdict is a different fact from a verdict ofnot_applicable. - Fixed: the export count could under-report its own version spread.
versionsand the date range were derived from a single read that also carried acount, and PostgREST caps returned rows - so on a large set the count stayed exact while the sample behind the version list came back cut. The one warning that tells you your runs were scored by more than one instrument could itself miss a version, with nothing in the response saying so. The total is now a head count and the version spread is paged, so it is complete rather than sampled. - Fixed: every export download returned a 500. The
SELECTnamed acompleted_atcolumn thateval_runsdoes not have (it isfinished_at), and PostgREST rejects the whole query on an unknown column - so this was not a missing field in the output, it was a hard failure on every.csvand.jsonldownload and everyPOST /evals/exportsince the surface shipped. A tripwire now reads the column set out of the migration that creates the table and fails a build that SELECTs a name the table does not have. - New `ungraded` verdict filter.
verdictsacceptsungradedalongside the four real buckets - the runs carrying no verdict at all, i.e. the ones whoseoverall_scorecell is empty. It compiles to anIS NULLpredicate rather than passing through as a bucket value (no run storesverdict: "ungraded"), so the numberverdict_counts.ungradedreports is now a set you can isolate or exclude rather than one you can only read. - Fixed: the version list sorted lexically.
versionsused a plain sort, so2.11.0came before2.2.0and a workspace spanning eighteen versions got a list that looked shuffled - with the "these runs span N eval versions" warning beside it reading as noise rather than a timeline. It now orders numerically per dot-separated segment. - The eval KPI now publishes a count for each of its two means.
n_scoredhas always been the number of runs behindsubmitted_mean_15, andimproved_mean_15is taken over the runs that carry an improved-side grade, which had no count of its own — so a reader could attribute it to a denominator it was never computed over. Series buckets and both window stats now carryn_scored_improvedbeside it. The improved-side window mean also abstains below 3 runs against its own count rather than against the submitted count, so it can readnullin a window where the KPI itself is reported. - The two window means now abstain independently, so one payload state is newly reachable: a window with fewer than 3 submitted grades and 3 or more improved ones returns
abstain_reason: "thin_window"andsubmitted_mean_15: nullbeside a non-nullimproved_mean_15.abstain_reasonanswers for the submitted side, which is the KPI. Whenever it is present, readsubmitted_mean_15as withheld regardless of what the improved mean reads. - The eval report and the
evalsrun result now lead with the SUBMITTED fraction (the rubric checks your own copy cleared) and the specific lines it missed, instead of the composite 1-5 or the improved-side number. Those figures are still present, they are just no longer the headline: the improved side clears the bar on most runs by design, so leading with it reads as pass/fail when it is closer to a constant. The number that is about your writing is now the first thing you see. - The run result now surfaces the exact quotes the run graded against, alongside a
regradehint: draft from those quotes and re-run withevidence_from_runset to the same run id, so a second draft is scored against the same evidence rather than a fresh retrieval. This closes the case where legitimate personalization was penalized because the draft and the grade drew from different pulls. - The queued run acknowledgement always carries
console_url(a link to the report) and apollblock (thestatusaction, the run id, and thewait_mscap), so reading back an async grade is one obvious step rather than guesswork. - The Prompt and Message Eval now rewrites by default, and generated copy carrying an unfilled placeholder can no longer read as passing. Two halves of one reported defect. `inputs.mode` defaults to `rewrite` (it was
advisory): submit a prompt and you now get an improved prompt back, graded as a blinded pair against yours. The old default was inverted — sending NO prompt returned a full improved one, while sending a prompt, the case where you are asking for it to be improved, returned none, becauseadvisoryinstructs the writer to leave the improved prompt empty.advisoryis unchanged and one input away; it is still the right mode for a living rules document your team is not going to replace. And the improved message is checked for unfilled placeholders —{{token}},[Insert the metric],[Company]— with a non-zero count withholdingtransition.improved_verdict: "pass"whatever the rubric scored, and the offending spans quoted intransition.explanation. A message with a slot in it is a template, and a live report presented one as ready to send while the judge had already scored that line 1/5 for the unfilled bracket. No score moves: the count is taken after grading, soscore_15, the dimensions,liftandoverall_scoreare unchanged — only the pass claim. The newplaceholders_unresolvedcounter ships on the improved message facet (and only there: a reusable prompt is supposed to carry slots).prompt-and-message-evalbumps to 2.21.0; do not poolimproved_verdictortransitiontallies across that line, nor prompt-facet scores for runs that omittedmode.
Fixed
- Evals: a rubric dimension can no longer be marked not-applicable and scored PASS at the same time. A graded facet's
not_applicableflag states that nothing of that kind of artifact could satisfy the dimension however well written — so a candidate that scored PASS on the same dimension in the same grade was direct counter-evidence, and until now nothing reconciled the two. Apass: truenow vetoes a not-applicable claim on that dimension for both the submitted and improved side of the pair (score wins over abstain); the dimension is graded exactly as it always was, and no headline number, verdict, lift oroverall_scoremoves. Everyimprovement.facets[].before/.afterblock now also carriescontradicted_not_applicable, a third reported-only counter alongside the two that already track the caps on this annotation, emitted even when zero. - Evals: the research steps a report suggests were being silently dropped, and now they are not. The report hands back runnable Amdahl calls — "here is the exact query that answers what your message was guessing at" — and every one is validated against the live registry before it ships, so a call you cannot run is never shown. What that validation could not do was tell the model what each call REQUIRES. The kit it was given listed
<id>: <name>and nothing more, so it had to guess parameter names, and the validator then rejected the guess against the very list the model had never seen. Measured over 40 drafts: 156 steps proposed, 107 kept — a 31.4% drop rate, with 38 of 40 runs losing at least one step, every drop for a missing required parameter and 34 of them the same call (lookalike.similar_themes, labelled "Similar Themes" but keyed onquery). The kit now names each call's required parameters, so the steps that were being binned should survive. Nothing about which calls are OFFERED changed — the surface is still exactly what your key can run.prompt-and-message-evalmoves toeval_version2.25.0 and scores are not poolable across it: the kit block sits in the same prompt that writes the improved copy, so the copy can shift as well as the steps. - Eval grading no longer marks a passing check as failed when the judge explains itself with a list. A check whose reasoning said something like "no numbers, outcomes, or account-specific facts are invented" was being read as an admission that the draft was ungrounded, and the pass was dropped — measured at about 3% of passing checks, concentrated on the evidence-grounding line. Scores on affected runs were too low, never too high, so a re-run after this change may read slightly higher.
- Two claims on already-stored eval reports now read correctly everywhere they are shown. A run that graded nothing (every case refused) reported
overall_score: 0- the worst grade on the scale, standing in for the absence of one - and sometimes carried a passing flag beside it; it now reports no score, and the refused case no longer ships pass flags for a judgement it never reached. And an improved message still carrying an unfilled[placeholder]no longer reads "Improved: passes": that gate shipped in eval 2.20.0, but the overwhelming majority of stored reports predate it and kept claiming a pass on a template. No number moved. The per-dimension scores,score_15,liftand each side's own grade are exactly what the judge produced - this corrects the pass CLAIM and the never-graded score, nothing else - and the stored run still records what it concluded at the time; the correction is applied when the report is read, so it reaches reports written before the fix as well as after. - `grade-and-report` skill: bounded the published "improved side clears the bar on 92%" statistic. The
skills/grade-and-report/SKILL.mdguidance (also published verbatim in the publicamdahl-cookbookrepo) now states the eval_version (2.9.0), sample (n = 89 runs / 40 distinct messages, one tenant), and a prior reading (71.0% at eval_version 2.7.0, on a partly different message population) behind the figure, and is explicit that the two readings are not a controlled before/after — the difference between them is not attributed to the version change. The guidance itself (use the submitted fraction, notliftortransition) is unchanged. - Eval verdicts got three honesty fixes: the run verdict now reflects your SUBMITTED draft (not the improved rewrite), a dimension pass whose own reasoning says the claim is unsupported resolves to unreadable instead of counting, and a message below the word floor beside a viable prompt is set aside with a named note rather than scored.
prompt-and-message-evalmoves toeval_version2.16.0. - Evals: the docs now say where `status` actually is when you poll, and the API reference stops returning an empty object for it. Submitting a run answers with
statusat the top ofdata; reading the run back nests the row one level deeper, at `data.run.status`. Nothing said so. There was no JSON example of that response anywhere in the guide, the endpoints page printed the verdict with *both* wrappers stripped directly under the poll request, and the OpenAPI200forGET /eval-runs/{id}was literally{"data": {}}— so every published source a caller could copy from was either absent or wrong, and the one correct implementation was a shell script inside a skill. A hand-rolled poller readingdata.statusthere matchesundefined, which is not an error, so it loops until something else kills it. The terminal value is alsocomplete, nevercompleted— the second spelling belongs to an older, unrelated surface and is a natural wrong guess. All of it is now written down: the real response body is shown with its envelope, a table gives the path tostatuson each of the five reads that expose it (they differ — only the detail read nests), thecompletespelling is called out, andpoll.wait_ms_maxis documented, which is the 30-second server-side long-poll that means you should not be writing a poll loop at all. The API reference now publishes the true response schema, enum included. Two response samples that shipped without theirdataenvelope (/drafts,/gate) are corrected, and the docs check that is supposed to catch that class now recognises these shapes — it was keyed to a marker list that none of them appeared in, so they passed CI while being wrong. - Evals: a run that crashed mid-revision no longer reports that it finished normally, and a run that timed out before grading anything no longer reports that the engine broke. Two values were being reported for situations they do not describe.
transition.stop_reasonis seeded withmax_iterations— "the configured round allowance was simply used up" — and the three exits taken when a LATER round errors were breaking out of the loop without overwriting it, so a run whose second round crashed was indistinguishable from one that calmly ran to its limit. Those exits now report a newround_failed, so the five existing values keep their meanings and the sixth carries the case that had been borrowing one. This matters beyond labelling:stop_reasonis a partition key — comparing two runs withholds the delta onstop_reason_mismatch, on the reasoning that the loops did different amounts of work — so a mislabelled crash read as two runs having done the same work. Separately, when the phase budget expired before ANY round completed, the run returned the samegenerate_failedan actual engine fault returns, and the reader was told "we could not produce an improvement report", which describes a break that did not happen: nothing errored, the clock ran out. That case is now its ownloop_deadlinewith a sentence that says so. The machine-readablenot_applicable_reasonis unchanged (report_unavailable), so there is no new value to handle — the distinguishing detail ridesevidence.reason, where a timeout previously could not be told from a fault at all. If you have been counting how often runs time out, your number was low, by exactly the runs that never got a round out.prompt-and-message-evalmoves toeval_version2.26.0.
Added
- Evals: a graded facet now says how many not-applicable flags the caps threw away. A rubric dimension can be marked structurally inapplicable to the kind of artifact under grade, and two independent caps bound that — one refuses the grader's whole list when it names more than three dimensions on a facet, the other truncates the merged list so at least two rubric lines always stay scored. Both were silent: a flag the budget refused and a flag never raised produced identical bytes, so "how often is this binding" could only be answered by grading again. Every
improvement.facets[].before/.afterblock now carriestruncated_not_applicableanddiscarded_not_applicable, emitted even when zero — absent means the run predates the counters, never that nothing was dropped. Both are reported and never read: no score, verdict, lift oroverall_scoreis computed from them, and the caps themselves are unchanged. - Evals: re-run the judge over a fixed improved version.
evals.runacceptscandidate_from_run— a prior run id whose improved version this run should GRADE, instead of writing a new one. No generate call is made, so a score difference between two such runs is the judge rather than the writer; without it, re-grading the same submission mixed the two and neither could be measured. It also pins that run's customer quotes automatically (the frozen text's citations address the pool its own run froze, so grading it against a fresh pool would silently re-point every one of them) — send the same id asevidence_from_runor omit it; a _different_ id is refused. A run that submits different copy, or a differentmode, than the pinned candidate was written for is refused rather than graded, because the judge scores your submission and the candidate as a pair. The response echoes acandidate_pinblock (source run, when the text was frozen, the mode it was written under) and the report carriesgrader_meta.candidate_scopebeside the existingevidence_scope. Passreuse: "force"when sampling repeatedly — two identical pinned requests share a content address and otherwise join into one run. - Evals: the KPI trend — is the writing improving over time. One run is a report card;
GET /evals/<slug>/kpi(resourceeval://<slug>/kpi, and the MCPevalstool's newkpiaction, which defaults the slug so{ "action": "kpi" }alone answers the ask) aggregates an eval's completed runs into the trend a team tracks like a metric: aseriesof day / week / month buckets,current+priorwindow stats, anddelta_15for the movement. The trend tracks the SUBMITTED-side score (verdict.headline.submitted), never the blendedoverall_score— that blend mostly grades the rewrite the eval wrote, so a trend over it would measure the rewriter rather than the team. Honest by construction: a window with fewer than 3 scored runs returnsnullmeans plus a namedabstain_reason(no_runs/thin_window) instead of asserting a mean over noise, refusals raiserefusal_ratebut never drag a mean toward zero, empty buckets are gaps rather than zero points, and a capped fetch saystruncated. Scopeevals:read; the console renders the same numbers as a band above the compose panel.
Changed
- When an eval run names an
accountit cannot ground against, the report now tells you which of two opposite things happened, and carries the sentence that says what to do about it.account_abstain_reasonused to collapse both into oneno_conversations, so the only actionable part of the answer was missing:not_in_corpusmeans no company matched the name you typed — names are matched as written, so a brand name your CRM records differently (LlamaIndexwhere the record saysRunllama) lands here with a full conversation history sitting under the canonical spelling — whileno_quotable_utterancesmeans we do hold that account and nothing it said clears the citable band, every utterance being internal, under 40 characters, or over 2,000. The first is fixed by retyping the name; the second cannot be fixed at all. A newaccount_abstain_detailcarries the specific half beside the reason — which name found nothing, or the account's real utterance count when none of them were citable — and the same text rides the liveaccount_resolvedprogress step, so it is readable while the run is still going.no_conversationsis no longer emitted, so add the two new values if you switch on this field. The eval version moves to2.8.0, which means a previously cached run of the same content re-grades rather than replaying a verdict carrying the retired label. - Evals: the cited-quote count now says WHICH side cited. A run grades two artifacts, so
quotes_cited— one blended number — was read as customer evidence weighed against YOUR copy. On 65% of measured runs none of it was: the draft cited nothing and every quote was cited while grading the rewrite.verdict.headlineand the report card'sheadlinenow carryquotes_cited_submittedandquotes_cited_improvedbeside an unchangedquotes_cited, the card's footer reads "3 customer quotes cited (0 grading your draft, 3 grading the rewrite)", andoverall_reasoningcloses with "Your draft cited none; the rewrite cited 3." The two figures are stated, never summed — a quote cited on both sides is one quote in the total and appears in both counts, and a quote carried only by a suggestion is in the total and in neither. - Docs: the
verdict.headlinereference published a FLAT shape (submitted_score_15/submitted_checks/improved_score_15/case_id) thirty lines after prose telling you to readsubmitted.score_15. No response has ever carried those field names. All three headline blocks are now the real nested object with its complete field set, includingsentence,what_changedandfacet, which were missing everywhere. The run-comparison section's flat names are unchanged — that surface really is flat. - Evals: when you name an audience or an account, the customer quotes you are graded against are now picked for your draft rather than by date. The account and cohort evidence legs drew the most RECENT utterances, so which quotes reached the judge had nothing to do with what you wrote — and on a cohort-scoped run those are the only quotes that can back a claim about that cohort, because workspace-wide quotes are explicitly not allowed to speak for it. Both legs now rank on the claims your draft actually makes first and recency second, using the same reading of your draft the corpus search already used. Every claim you make is represented, not just your first one — the terms are shared out round-robin across your claims, so a long opening claim can no longer crowd out the evidence for the ones after it — and matching is whole-word, so "act" no longer matches "contract" or "actually". A run that names neither an audience nor an account draws exactly what it drew before, and repeat runs of the same draft still read the same quotes. Every existing guard is unchanged: no single company can fill a cohort's evidence, the same length band applies, and account quotes are still buyer-side only.
eval_versionmoves2.8.0->2.9.0: a scoped run reads a different set of quotes on either side of that line, so do not pool scores across it. - Every workspace now runs the same agent surface. The platform had two code paths behind a per-workspace switch: the current one, and a pre-release one kept for workspaces that had not moved yet. Everyone is on the current path, so the old one is gone — one surface, one set of tools, one set of instructions. If you connect over MCP you get
agents,search,enrich,lookalikeandevals; that has been true for most workspaces for weeks and is now true for all of them. Two consequences worth knowing. A query that would silently return only part of its result is now always refused with an explanation rather than quietly trimmed — you will hear about a truncation instead of acting on a partial answer. And the tool list served byGET /api/mcp/api-keys/toolsnow matches what a session actually exposes; it previously advertised four tools that were not registered and omitted four that were.
Fixed
- A read on a known scheme with an unserved path now names the valid URI shapes for that scheme instead of a bare not-found, and the dispatcher no longer silently routes an unmatched path to a different operation.
- The eval docs described a score scale the grader cannot produce. Every rubric line is a binary pass/fail, so a per-dimension score is
1or5and the headline is1 + 4 × (passed / total)— over a five-line rubric that is exactly six values: 1, 1.8, 2.6, 3.4, 4.2, 5. Published examples showed dimensions at2and4and headlines of1.6,2.4,4.4and4.6, none of which any run can return. The worked example, the annotated response samples and the run-folder templates now all read on the shipped ladder, and state the arithmetic once:1.8means exactly one line passed. Readchecks_passed/checks_total— the fraction says what the number measured. - The two grading-loop skills (
ground-and-draft,grade-and-report) are now published in the public Amdahl cookbook. The guide previously linked them to a repo only Amdahl employees can open, so the documented path to them returned a 404 for every customer. A docs check now resolves everygithub.com/amdahlco/*link a published page carries — the full path, not just the repo root — so this class cannot ship again. ground-and-draft'sground.shnow takes an optional filters argument, so the evidence pull can be scoped the same way the grade will be — buyer-side only (speaker_type), or one account (company_id). It also validates that argument before spending a request, instead of burning four retries on a filter the semantic lane refuses. A cold account correctly has nothing narrower than its cohort, and the skill now says so.- Chat answers no longer skip the
<unverified>check on a figure that happens to touch a letter. A scale word ($450MM), an ordinal (500th), a unit (250bps,450MWh,250000USD) or a missing space (1,240deals) is a number the grounding gate should be checking, not an identifier — only a run id, a uuid, or asnake_casehandle is skipped now. Run ids and backticked ids are still never marked. - A stray code fence or an unmatched backtick in an answer no longer silently switches the grounding check off for the rest of that answer. An unclosed fence and an unbalanced backtick now suppress nothing, rather than everything after them.
- A read 404 now returns the same body whether the row was missing or the id was malformed, so a mistyped identifier gets the valid-URI-shape correction it previously never received. The shape list is deduped to one entry per URI shape (query-parameter spellings of the same path are no longer listed separately), and no longer truncates —
knowledge_base://content/<id>was being dropped from the correction that exists to point at it. - Evals: the workspace-wide quote pool no longer varies run to run — and the ranking behind it is now stable by construction. The previous entry flagged this as not fixed, on the reading that the workspace-wide pool drifted for reasons of its own. Re-measuring against stored runs showed that was the wrong read: those pools were tracking the same per-run reading of your draft that the shared-reading fix had just pinned down, so agreeing the reading once already settles them. Across 129 stored runs, no two runs that issued the same retrieval searches drew a different pool inside 43 minutes; the one case that differed at all was 44 minutes apart, which is your data genuinely having changed. The distinction matters and is easy to get backwards: two runs can start from the same short list of keywords and still search differently, because the keywords are a compressed form of a longer reading of your draft. Runs grouped by the keywords alone DO still show different pools seconds apart - that is the same behaviour the shared-reading fix addresses, not a separate one. Separately, and this one is a real fix: theme ranking could leave two equally-relevant themes in an arbitrary order, because nothing decided ties. Two themes tie whenever they contain an identical piece of conversation, which happens on live data — and a tie landing at the top of the list is exactly where it changes which quotes you are graded against. Ties now resolve the same way every time, in theme search and in theme-by-facet browsing alike.
eval_versionmoves2.10.0->2.11.0. A tie is the only thing this can change, so every other query grades exactly as before; take the version bump as the honest marker that a boundary may have moved rather than as a sign your scores were wrong. - Evals: two runs of the same draft now read the same customer quotes. When you name an audience or an account, the quotes you are graded against are ranked on the claims your draft makes — but the reading of those claims was being re-derived per server, so the same submission could land a different reading, different terms, and a different set of quotes on each run. Measured on live data, one draft drew three different cohort quote sets across six runs seconds apart: two of the six quotes moved, on the only quotes allowed to back a claim about that cohort. That reading is now agreed once and shared, so repeat runs of one submission rank on identical terms and draw an identical cohort set — which is what makes an A/B between two drafts, or an N-sample of the judge, mean anything. Each run's progress trail now says which layer produced the reading, so a future disagreement is attributable rather than a mystery. Not fixed, and worth knowing: the workspace-wide quote pool still varies run to run on identical input — a separate, older behaviour that this does not address.
eval_versionmoves2.9.0->2.10.0. Every individual grade means exactly what it did before, but a repeated measurement of ONE submission no longer re-samples the evidence, so do not pool a multi-run reading taken before that line with one taken after. - Evals: the cohort-evidence rule you are graded against is now written down in full. When a run is scoped to an audience, the quotes it draws are tagged
segmentand license a claim about that cohort. The rule shown to the grader named one edge of that — do not stretch a cohort quote into a claim about the recipient personally — while the grader was in fact enforcing two. The other edge is that a pattern several companies stated is not a single customer anecdote, so writing _"one lead told us…"_ off cohort evidence failed the same grounding check, for a reason nothing told you about. Measured on live runs, a draft doing exactly that failed 3 times in 6 and passed once, which is a rule you could trip without ever being able to read it. Both edges are now stated, and stated as a level to match rather than a list of banned phrases, so you can reason about a sentence neither example covers. No grading rule changed — what changed is that it is legible to the person being graded.eval_versionmoves2.11.0->2.12.0. - Chat: the
resume_urlhandle returned byPOST /chatnow points at the publicagents.resumeroute, so answering a paused turn works for workspace and MCP keys (it previously returned a 403 on the documented answer path). - Chat: a
quick-depth chat now stays quick across follow-up turns instead of re-opening the full toolset when a follow-up omits the depth setting. - Search (
search.queryfuzzy lane):detail.groups[0].internalis now the result object on REST, matching MCP and the docs (it was serialized as the string"[Circular]"). - Search:
escalate_reasonis now populated wheneverescalate_to_chatis true, naming the sub-question the fast lane could not answer. - MCP prompts:
system/guide_me,system/getting_started, andsystem/research_playbookare now readable viaprompts/getfor every workspace key (they were listed but returned a scope error). - Evals:
GET /eval-runsnow acceptsrun_statusas an alias forstatus, so the MCP and REST filter names are interchangeable.
Added
- Read an eval result over MCP. The
evalstool gainedstatus,list_runs,listandget. Firing a graded run returns a run id, and until now reading that run back needed an MCP client that implements resources — so a tools-only client could start a grade and never see it.statustakes the run id fromrunand returns the verdict. - Wait for a grade instead of polling for it.
GET /eval-runs/:idand theevalstool'sstatusaction acceptwait_ms(up to 30 seconds): the call blocks until the run finishes and returns the same body an immediate read would. If the budget runs out first you get the run back with its current status, which is an answer, not an error. search.query(modefuzzy) now reports its own coverage. The response carriesuncovered— one entry per part of your question that produced no answer, each with the reason (breadth_capwhen the question had more independent parts than the lane answers in one call,deadlinewhen a part ran out of time,planner_incompletewhen the question could not be split in time) — and the plain-languagemessagenames those parts so you know what to ask again. Previously an answer covering three of five parts was indistinguishable from one covering everything.search.queryresponses carry atimingbreakdown (plan_ms,fan_out_ms,coverage_ms,total_ms, plus per-sub-question timings on the fuzzy lane'sdetail), so a slow call can be attributed without guesswork.search.querycan run as a background job. Passasync: trueand you get ajob_idback immediately instead of waiting; call the verb again with just thatjob_idto collect the result. A job gets minutes rather than the ~15s a blocking call allows, so a deliberately broad, multi-part question can be answered in full rather than in part.search.querytakesmax_subqueries, so you can ask for a wider split when a question has more independent parts than the default 3. Up to 5 synchronously, and up to 12 on a job.- Every
search.queryreply now carriesretry_guidance: when part of your question went unanswered it names the exact parameters to re-send (raise_max_subqueries,run_async, ornarrow_query) rather than leaving you to work out the remedy from a flag. - You can now grade two drafts against the same customer quotes. Pass
evidence_from_run: "<run_id>"toevals.runand the new run reuses the quotes that run was graded against, instead of retrieving its own. Without it, every run retrieves fresh evidence seeded from what you submitted — so editing a draft and re-grading changed both the copy _and_ the quotes, and the score difference mixed the two. On a real pair we measured, two runs of the same email shared none of their four retrieval queries and only 5 of 12 recorded quotes. Pin the second run to the first and the difference is attributable to your edit. - A new comparison read says whether a score difference means anything.
GET /eval-runs/{id}/compare/{other_id}(eval_run://<id>/compare/<other_id>) returns both verdicts, how much customer evidence the two runs actually shared, and — only when they were graded on the same basis — the score delta. When they were not, it withholds the delta, says how far apart the evidence was, and tells you which run to pin to. A number next to a warning still gets quoted, so it does not ship one. - Reports now say what their evidence was fixed to.
grader_metacarries a newevidence_scope: either{"kind":"run"}(retrieved for this run and held fixed across its revision rounds) or{"kind":"pinned", ...}with the run it came from and when those quotes were frozen. The oldevidence_frozen: trueonly ever meant the first of those, but read as "held fixed for this comparison" — it is still present for compatibility and is now deprecated. A pinned report also carriesevidence_provenance.pinned_from, so you can see you are grading against an older view of your data rather than today's. - The run list now says which runs reused another run's evidence. Rows from
GET /eval-runs(eval_run://list) carryevidence_pinned_from: the id of the run whose quotes they were graded against, ornullwhen the run retrieved its own. It is the pin ORIGIN only, never the quote pool, so the list stays lean; a run history can mark an A/B pair without a detail read per row. - Evals: a report now names what the rewrite made worse.
improvement.regressions[]lists every rubric dimension the improved version scored LOWER than your original, with both scores, the judge's own reasoning, andflipped_to_failwhen the binary verdict flipped. The headline lift is a mean, so a rewrite could gain three points on grounding, drop positioning from 5/5 to 1/5, and still report a rise with nothing on the wire saying so. Absent, not empty, when the rewrite cost nothing.what_changednow names the trade alongside the gain. - Evals: the overall reasoning no longer asserts the opposite of the finding. A case's headline verdict follows the IMPROVED side, so on a run where your draft held up and the rewrite did not, the summary paragraph opened "The message does not hold up against the evidence" - a sentence about writing you did not submit. Both disagreeing directions are now named explicitly.
- Evals: the run-comparison caveat no longer claims the delta measures your edit. Grading two runs against the same customer evidence establishes that the difference is not explained by different quotes; it does not make the score difference an effect of what you changed, because
overall_scoreblends graders judging two different artifacts. The caveat says what was actually established. - Docs: a new "What the score is, and what it is not" section states the three properties that hold on every run - the headline score is mostly about the rewrite, grounding penalizes abstract copy on any corpus size, and the number is a coaching read rather than a measurement. The
cases[]reference now documentsinput_passed/improved_passed/transitioninstead of describingpassedas "every applicable grader passed", which stopped being true when the verdict split shipped. - Eval report quotes now say who said them, when, and at what deal stage. A cited quote can carry
speaker_side(a customer, or your own team), and account-tier quotes additionally carryspoken_at, the speaker'srole_level, anddeal_stage_at_time— the stage the deal was in when the words were said, not the stage it is in now. All four are read from the warehouse row the quote was retrieved from, so the grader cannot assert any of them, and any field the warehouse could not answer for is simply absent rather than filled with a placeholder. Speaker names are deliberately not surfaced. Worth knowing: corpus-wide retrieval does not filter on speaker, sospeaker_sideis what tells you when a quote is one of your own reps rather than a customer. - An eval run now comes back with a link to its report.
evals.runresponses carry aconsole_urlpointing at that run in your workspace console. Previously the reply heldrun_idandeval-run://<id>— both addresses only a machine can follow — so firing an eval from an MCP client (Claude Desktop, Cursor) meant copying an id and hunting for the run in the console to read what you had just paid for. The link works the moment the run is queued: it shows the grading trail live, then the report. It is omitted rather than guessed when it cannot be resolved, so an absent field means "no link", never a broken one. - The grade of your own copy is now one field, not nine levels deep.
run.verdict.headlinestates the submitted and the improved grade side by side and labelled —submitted.score_15,submitted.checks,improved, the threshold, the transition, the lift and its noise gate, and the confidence read. Until now the only number at the top of a verdict wasoverall_score, which is a mean over graders judging two different artifacts — your draft AND the rewrite the eval produced — so it was never a grade on your writing, and a run that returnedoverall_score: 1.0for a draft clearing 2 of 5 checks was reporting both correctly. Your grade was there all along, atcases[0].graders[1].improvement.facets[0].before.score_15. Now it is where the number that gets quoted actually lives. It is the SAME object the report card renders from, so a script and a person cannot read different numbers.overall_scoreis unchanged. - The run list carries your grade too.
verdict_summaryonGET /eval-runsaddsinput_score_15/input_scoreandimproved_score_15, so a run history shows how each draft scored without a detail read per row. The same two numbers are on each case asinput_score_15/input_score. - Comparing two runs now reports how far YOUR copy moved.
GET /eval-runs/{id}/compare/{other_id}addssubmitted_score_deltaandsubmitted_score_15_deltabeside the existingscore_delta, which differencesoverall_scoreand so blends your draft with the eval's rewrite. Both operands ship on each side, so the subtraction is auditable — and both grades stay visible even when the delta is withheld. Comparisons of runs graded before this shipped get the new fields too. They are held to the same bar asscore_delta, including the noise floor: a draft that moved by a single rubric line is inside this judge's measured run-to-run spread, so that comparison reportsdelta_withheld_reason: "inside_noise_floor"rather than a number. - A grade on a draft you never wrote now says so. When you submit a prompt and no message, the eval writes a specimen so there is something to score.
input_simulated: truemarks it on the case, the headline and the run list — previouslyinput_passed: falsethere read as a verdict on your writing. - Reports carry the confidence caveat, not just the level.
improvement.confidence.caveatis the one-line reader-facing note for the level, present whenever the level is belowhigh. - Eval reports now flag rubric dimensions that the kind of writing you sent could never satisfy, and accept an optional
artifact_typesaying what you sent (outreach/landing_copy/objection_response/nudge). Website and landing copy is the case this exists for: the message rubric leads with "is the offer positioned for THIS customer's specific situation", and copy addressed to a market has no such customer, so it failed that line on every run with nothing on the payload saying why — and three of the five prompt-rubric lines have the same problem. A flagged dimension now carriesnot_applicable, a reason, andnot_applicable_source(declared_typefor a deterministic consequence of what you declared,judgefor the grader's own reading). The flag does not move any score yet —pass,score_15,passed/total, the transition andoverall_scoreare all still taken over the full rubric. What ships beside them is what the number would be over the applicable lines only (applicable_passed,applicable_total,score_15_applicable,score_applicable, absent when every dimension applied), so the size of that change is measurable on real runs before the headline moves. Every run also reports the artifact type it resolved to and where that came from; aninferredreading is a label only — it does not pick a rubric, exclude a dimension, or reach the grader, and comparing two runs that resolved to different types now refuses the delta rather than reporting a difference between two different denominators. GET /eval-runs/{id}/reportreturns an eval run already written up — a markdown card to paste, plus the same numbers machine-readable onheadline. It exists because the eval grades two different artifacts (your draft, and a rewrite it produced), so a caller who summarises a run in their own words is exactly where the rewrite's score becomes a claim about your copy: a live run carryingoverall_score: 1.0describes a draft that passed 2 of 5 checks. The card states both sides with their pass fractions, prints a lift only when it clears the measured noise floor (below it, "no measurable change" rather than a small confident number), flags a "before" the grader had to simulate on a prompt-only run, and carries the confidence caveat. It renders on every run state, so a failure or an abstain gets an honest card rather than an empty body. The two skills the grading-loop guide already named —ground-and-draftandgrade-and-report— now ship with Amdahl inskills/, with scripts for grading, A/B-ing two drafts on frozen evidence, and taking a median of N. The A/B path reads the submitted side: on a pinned pair whose true draft delta was −0.8,compare.score_deltareported −0.167.
Changed
- Eval scores stay comparable when we upgrade models. The model that grades your evals is now pinned separately from the model the rest of the product uses, so a platform-wide model upgrade no longer changes the scale your results were measured on. The model that writes the improved prompt and message still tracks the latest available, since that is the part you receive. Every report continues to name both under
grader_meta.blinding. - A model change now re-runs an eval instead of reusing the cached result.
evals.runwithreuse: "cached"previously returned a stored run whenever the eval, tenant and inputs matched, even if a different model had graded it — so after a model upgrade you could be handed a verdict the previous model produced. Which models graded a run is now part of what identifies it. Expect a one-time effect when this ships: the first call for an eval you have run before is graded fresh rather than served from cache, and costs a full run. Repeat calls reuse as before. - Whether a run pinned its evidence is now part of what identifies it.
evals.runwithreuse: "cached"previously matched on the eval, tenant, inputs and models — so a pinned request and an unpinned one with the same message resolved to the same stored run, and you could ask to grade against a specific run's quotes and be handed one that had retrieved its own. Expect a one-time effect when this ships: the first call for an eval you have run before is graded fresh rather than served from cache, and costs a full run. Repeat calls reuse as before. - Polling a run no longer returns the full quote pool.
GET /eval-runs/{id}omits the recorded evidence set, which can run to tens of kilobytes on a surface you poll while a run is in flight. The quotes the judge actually cited are unchanged and still on the report; the full pool is available through the comparison endpoint, which is what needs it. - Eval reports now retrieve as much customer evidence as the question needs, instead of a fixed 16 quotes. Every graded report showed a pool of exactly 16 quotes, on every workspace and every question - the search reliably found far more and the merge threw the rest away. Retrieval now runs in rounds: it searches your conversations with a first batch of queries drawn from your submission, and goes back for the rest of them while the pool is still thin. It stops when the pool is deep enough, when a round returns nothing it does not already hold, when the retrieval budget is spent, or when your workspace has no themes at all. Runs are no slower - the same retrieval budget is now spent across the rounds rather than on one - and a workspace with a thick corpus still finishes in a single round.
- Reports state the two evidence budgets separately.
evidence_provenancegainsin_context(how many of the pool were placed in the judge's context for a single graded call, which stays bounded so a deep pool cannot push one call past its token ceiling) androunds(how many retrieval rounds it took - consistently more than one is a signal your corpus is thin for the questions being asked).poolkeeps its exact meaning: everything retrieved and frozen. When the pool is deeper than the prompt budget, the quotes that reach the judge are sampled evenly across the search queries, so a wider search buys broader coverage of your draft's claims rather than more of whatever the first query matched. - `passed` no longer means two different things in one payload. On a graded facet it was an integer COUNT of rubric lines cleared; on a case, a grader and a dimension it is a boolean VERDICT — and a dimension carried neither, it carried
pass. A consumer walking the report and reaching for the obvious key on a dimension got nothing back and rendered every dimension as a failure. Both spellings are now unambiguous and both ship: counts arechecks_passed/checks_totalon a facet (the olderpassed/totalremain, carrying identical values, and are deprecated), and a dimension answers topassedas well aspass. Nothing was removed, so existing readers are unaffected. - Eval runs are now honest about being a single measurement. Three changes, all visible on the wire. Comparing two runs withholds a difference that is inside the judge's own noise. Controlling the evidence proves the two runs saw the same quotes; it never proved the judge would return the same numbers twice on them, and the measured run-to-run spread on byte-identical input is larger than the deltas that were shipping as results.
comparenow reportsdelta_withheld_reason(evidence_not_controlled·stop_reason_mismatch·not_scored·inside_noise_floor) andnoise_floor, each with the remedy that actually fits — for a delta inside the floor that is more samples, not more control. It also refuses a comparison whose two improvement loops stopped for different reasons, since those runs did different amounts of work. The reported grade is now the first revision round, not the best one (transition.reported_round). How many rounds fit was partly a function of how fast the host was, so reporting the best of them meant the same submission scored higher on a faster machine; the revision rounds still run anditerationsstill reports them, they just no longer move the number. `grader_meta.blinding.sampling` reports what each stage actually sampled at — the configuredtemperature: 0does not reach the current judge or generator models, which reject the parameter, so those calls run at the API default. That was true before and simply undisclosed.
Fixed
- Broad, multi-part questions on
search.query(modefuzzy) no longer come back as a bare failure. A question with several independent parts could exceed the lane's internal budget and returnfailedwithescalate_to_chat, discarding the sub-questions that had already been answered — so narrowing the question worked while asking it in full did not. The lane now finishes inside its budget, and a run that cannot cover everything returns the parts it did answer. - A broad, multi-part question on
search.query(modefuzzy) is now actually split into sub-questions. The step that decomposes your question had a 2s budget that was too small for a question with several parts, so it gave up and ran the whole thing as ONE query — which could match a single clause and return that as if it answered everything. The budget now fits the broad case. - When the split genuinely cannot be done, the answer says so instead of reporting full coverage.
uncoveredcarries aplanner_incompleteentry and the message warns that the result may cover only part of what you asked. - Messages to accounts you are already working with are graded again. The eval that grades your writing was declining to grade a large share of real sales email. Its check for "is this commercial writing" had, in practice, learned to mean _cold first-touch_, so chasing an unsigned order form, pushing a proposal, or negotiating whether a pilot converts came back
not_applicableinstead of a grade. Measured against real seller email, about a quarter of everything it declined should have been graded, and 93% of those were accounts already in play. The check now asks whether the message had a persuasive job — at any stage of the relationship — and still declines correspondence that had none, such as scheduling, receipts and support replies, including when they sit inside an active deal. - A declined run no longer blames your data. When the eval could not grade something, the summary said "the required data surface was unavailable" regardless of the real reason — so a submission that was simply not the kind of writing the eval reads sent you to check your data connection. Each reason now says what actually happened and what to do about it, and a run that declined to grade says plainly that this is not a judgement on your writing.
search.querynow tells you to run a broad question asynchronously when parts of it ran out of time, instead of suggesting a wider split. A sub-question that runs and hits its budget is a timing problem, and widening the split adds more parallel work to a search that was already too slow — so the advice made things worse. It also no longer reports full coverage when sub-questions were cut.- MCP clients no longer need to reconnect to use a newly-added tool parameter. A client caches the tool schema when it connects, so a session that was open before a parameter shipped sends that parameter as text — and the server used to reject it on type alone. Scalar parameters on the
searchtool now accept their string spelling, so an additive change works on already-open sessions. - Every numeric and true/false parameter on the MCP tools now accepts its text form. A client caches the tool definitions when it connects, so a session that was open before a parameter shipped sends that parameter as text and the server used to reject it — meaning you had to reconnect to use an addition. Additive parameters now work on already-open sessions.
- External search is less likely to run past its time limit and return nothing. Three stages inside its latency budget were bounded per network attempt rather than per call, so a slow stage could cost roughly three times its stated budget — in one case more than the entire ceiling on its own. Each now costs what it says.
- A topic enrichment with no matching customer conversations no longer reports a divergence-map error. The divergence map compares what the market says against what your customers say, so when you have no conversations on a topic there is nothing to compare — the map now comes back empty with the reason attached (
not_applicable_reason: "no_internal_evidence") instead of an error, and the call skips the slowest synthesis step entirely, so it returns sooner. - The external_search divergence map no longer comes back empty at random. Sonnet 5 runs extended thinking by default and thinking tokens are charged against
max_tokens, so a pane declaring a 1000-token budget was splitting it between a think and an answer — and when the think ran long the model returned no text at all, on a successful response. The panes now apply the existing thinking guard, as does the sharedllmCompleteseam. A pane that exhausts its budget also says so rather than reporting the same message as a model that returned nothing, and its log records which content blocks came back. LLM token counts are no longer redacted from logs as if they were auth tokens. - Extended thinking can no longer silently consume an LLM call's whole token budget. Sonnet 5 and Opus 5 think by default and thinking tokens count against
max_tokens, so a call with a tight budget could return a thinking block and no answer at all — on a successful response, which callers read as an empty result rather than an error. The shared Anthropic client now applies the existing thinking guard to every request, so the roughly 48 direct call sites are covered without each having to remember. A caller that asks for thinking explicitly, or whose budget can afford it, is unaffected. - Eval runs now cost the time they say they cost. The wall-clock budgets on the evals LLM stages were being applied per network attempt rather than to the stage as a whole, and the client retries twice by default, so a stage declaring a 420-second budget could run for roughly 42 minutes and be killed by the run reaper instead of finishing or failing cleanly. Each stage now shares one deadline across its retries. The industry angle planner had the same problem and is fixed with it.
- Pasting source code into an eval no longer produces a sales-message grade. Submitting a bare function, a JSON blob or a stack trace returned a full graded report that read "your submitted draft did not hold up against the evidence" - the eval had written an improved _sales message_ out of a sorting routine. A submission that is entirely code, configuration or a stack trace is now turned away as not applicable, before anything is spent on it, with a reason that says what was read. A refusal is not a failing grade: nothing was scored. A message that merely quotes a snippet, a config value or an ASCII arrow is still graded normally - that is ordinary technical-sales writing, and the check only fires when the whole submission is code.
- `POST /evals/run` now reports a bad input the same way whichever check catches it. The endpoint validated its request twice, and only one of those checks spoke a language a caller could act on. A non-string on a text field (
{"inputs": {"message": {"body": "hi"}}}) came back as a serialized internal validator error — a nestedunionErrorsarray of per-branch failures under a top-level message of just "Invalid input" — from which you had to work out thatmessagewanted a string. A malformedinputscame back as a clean"$.inputs: must be object, got array". Same endpoint, two vocabularies. Now a wrong type on a declared field is refused by the eval's own input schema, which names the field and the type that arrived, ondetails.input_errors; anything the request shape itself rejects renders as a path-addressed line ondetails.errors. No response carries validator internals. Separately, anumberfield stops coercing non-numbers, so an array is refused rather than quietly becoming a figure the run would grade against. - MCP: an assistant can now actually read the eval result it just asked for. The connected-agent instructions said to poll
eval_run://<id>. Underscores are illegal in a URI scheme, so standards-compliant clients rejected that address outright (-32603 Invalid URL) — the resource is published aseval-run://<id>. An assistant that followed its own instructions could fire a graded run and then had no documented way to collect the report, which is a large part of why models narrated a conclusion instead of showing the before/after. Five more cited addresses carried the identical defect (search_field://, andagent_blueprint:///blueprint_run:///blueprint_schedule:///content_piece://on the pre-agent-v2 surface), plus one that named a scheme with no path at all (notification_sends://, nownotification-sends://list). All corrected to the spelling the server publishes. - MCP: the instructions now say which side the headline eval score is about. They already explained how to read
usageand that a failing draft is the finding, but never thatoverall_scoreaverages graders judging TWO different artifacts — the hygiene rules score the text you submitted,improvement_loopscores the improved version the eval wrote — and that the case verdict follows the improved side. An assistant reading apasscould tell someone their draft was fine when the report said it was unusable. It is now told to quoteimprovement.transition.input_verdictfor the user's own writing and neveroverall_score. - Evals: the tool description now describes the eval that actually runs. It advertised a judge scoring "grounding/specificity/tone" — there has never been a tone dimension, so an assistant reading it looked for a score that is not on the wire and could not discover the five that are (relevant positioning, grounding, verified specifics, differentiation, CTA clarity). It also omitted the
accountandmodeinputs entirely, making account scoping and advisory-vs-rewrite unreachable for anyone reading only the tool surface, and still offered an empty-inputs regression eval that was retired — omitting inputs is now a validation error, not a second mode. It also now says your prompt is graded on its own separate rubric rather than sharing a score with the message. - A refused eval run no longer reports a score of zero. When nothing could be graded — a customer-evidence outage, an engine failure, or copy the rubric declines —
overall_scorenow comes backnullwith a top-levelnot_applicable_reasonnaming the cause, on both the run detail and the run-list summary, so a refusal is distinguishable from a bad grade without reading the prose. The refused case also no longer carriesinput_passed/improved_passed/transition: those answer "did your copy clear the bar", and a refusal never asked the question — previously a run that graded nothing shipped a hard0besideinput_passed: true, which read as a terrible score and a pass at the same time and quietly dragged down anything tracking the number over time. If you poll `overall_score`, handle `null` — drop refused runs from an average rather than scoring them. - The
audienceinput onprompt-and-message-evalnow changes the EVIDENCE, not just the framing. It previously reached the judge as an instruction to assume a cohort while retrieval stayed corpus-wide, so a run scoped to "VP of Marketing" and an unscoped run graded an identical pool. A resolved cohort now gets its own retrieval leg — that cohort's own utterances, drawn across several companies so one talkative account cannot stand in for the group — taggedsegment, the tier that licenses "teams like yours". This was the missing rung: the tier existed in the schema and the grading prompt licensed it, but nothing ever produced it, so segment-level copy had no tier that could back it and either failed grounding as an over-claim or had to be flattened into a general market observation. The remediation the report handed you was unactionable. Account-tier evidence stopped silently for large accounts. The quote lookup did not declare its own row bound, so the query engine refused it outright whenever an account had more than 100 qualifying conversations — the biggest accounts, the ones you are most likely to name — and the report abstained with a lookup failure while quietly grading against corpus themes. Under that threshold the same defect returned up to 100 account quotes instead of the intended 10, which filled the judge's whole context with one tier. Both are fixed, and account and cohort limits are now sized together so a corpus quote always reaches the judge. A run that both pinned another run's evidence and named an account is now refused instead of silently resolved in the pin's favour. It used to retrieve the named account's quotes, discard them, and inherit the pinned run's — which carry an account tier and a different account's name, so the report headed a section with one company over another company's words and handed the judge an account-tier licence for a claim that company never made. The two requests are genuinely exclusive: pinning holds the evidence fixed so a score delta is about your copy, naming an account retrieves what that account said. The run says so and names both ways out. The same check covers the cohort. Tier coverage is now queryable rather than prose-only:account_status,account_quotes,segment_status,segment_quotes,segment_companies,corpus_quotes, andcorpus_onlyride on every improvement report, and the run's progress trail emits its account and segment steps on every run — including one that scoped to neither, because "this run stood on no account evidence" is a finding about what the copy was allowed to claim. Pinned reports also keep the quote provenance (speaker_side,spoken_at,role_level,deal_stage_at_time) they previously dropped on the way into storage, so a pinned report no longer looks less attributed than the run it pinned. - An MCP session you are using no longer expires. Sessions used to be dropped two hours after they were opened, no matter how active they were, so a long working session was guaranteed to break mid-task. The two-hour budget is now measured from your last call rather than your first: keep using a session and it stays alive indefinitely, and it is only reclaimed after two hours of no calls at all.
- A busy session is no longer the first one dropped under load. When a server was at its session limit it made room by dropping the session that had been open longest — which, during a burst of new connections, meant preferentially killing the sessions doing real work. It now drops the session nobody has called in the longest time, so an in-use session is never an eviction candidate.
- `-32000` now tells you whether it was us. The "session not found" error carries a
datablock withserver_uptime_seconds. If that is smaller than the age of the session you were holding, the server restarted and your session could not have survived it — so a deploy is distinguishable from a bug in your client without guessing. The error also echoes your JSON-RPC requestidinstead of always returningnull. - The retry contract is published. Reliability and retries documents how long a session lasts, what ends one, and the single rule for
-32000: reinitialize and replay the call once — not a backoff loop, which cannot help when the session is already gone. - The Evals documentation now describes the eval you actually get. Eight things on the published pages had fallen behind the code:
modewas documented as defaulting torewritewhen the default isadvisory; the hygiene length cap was still the withdrawn 1400-character bar rather than the 3000-character outlier guard that replaced it; four sample payloads carried"eval_version": "1.2.0"when the served eval is2.5.0— a value that rides the run fingerprint, so anyone building cache or comparison logic against it was coding to a string that never appears; the API reference described a case'spassedas "whether every applicable grader passed" when it follows the improved side, so a run could readpasswhile your own draft was unusable; it also said retrieved quotes are never the recipient's, which stops being true the moment you pass anaccountthat is in your data; the run handle'sresourcewas spelledeval_run://when the server returnseval-run://(an underscore is illegal in a URI scheme, so the underscore form throws in a standards-compliant MCP client); two copy-pasteable "run the regression harness" examples were published that the server refuses withinvalid_argument, because no built-in eval ships ingeneratedmode; and the eval builder's rules still named the retiredsearch.runinstead ofsearch.query. The evidence-tier tables now also say plainly thatsegmentis part of the wire vocabulary but is not emitted today, so its absence is not a statement about your data.
Breaking
- `POST /search` is removed. The plain-language fast lane now lives on the routed endpoint: post the same question to
POST /search/querywith"mode": "fuzzy". You get the same planner, the same NL-to-SQL writer, the same multi-question split, and the same envelope — the rows are onresults, the SQL it ran is oncompiled.sql, and the full warehouse envelope you used to read at the top level (internal,groups,message,coverage,escalate_to_chat) is ondetail. Scope is unchanged (data:read). On MCP thesearchtool'srunaction is gone the same way; use itsqueryaction. - The blended web + news pass is gone.
mode: "blended",external_limit, and theexternal/external_omittedresponse fields no longer exist. Amdahl's synchronous search reads your own warehouse only. For market signal, useexternal_search(which also gives you the internal-vs-market divergence map the fast lane never had); for a written answer that weaves both, use Chat. - The `synthesize` headline is gone.
synthesize: trueand thesynthesisresponse field are removed. The search verbs return data, not prose — ask Chat when you want it written up. - Authored evals using a
generatedsubject must settarget.optosearch.queryinstead ofsearch.run, and thehas_citationsdeterministic check is removed (it measured the retired blended leg). No shipped eval used either.
Changed
- The graded report now shows its work. Reviewers kept asking the same questions of an eval report — what does "5/5" mean, what does "scored blind" actually check, and why do three different quote counts appear on one page — so every one of those is now answered on the report itself rather than taken on trust. What the score counts: each graded side carries
passed/totalbesidescore_15, because that is what the score is — the judge returns a pass or a fail per rubric line and the headline is that fraction placed on a five-point scale, so a 5 means "cleared all five checks", not "perfect". How much it is worth: a newconfidence(high/moderate/low, with the reasons) flags a grade backed by thin evidence, an errored retrieval leg, a truncated prompt, or a run that stopped on a budget rather than the bar. It never changes a score; it tells you how firmly to read one. What "blind" recorded:grader_meta.blindingcarries the facts behind the badge — both candidates scored in one call against one rubric and one evidence set, presentation order taken from a hash of the text _you_ sent, which way that landed on this run, and whether writing and scoring were separate calls. Where the quote numbers come from:grader_meta.evidence_provenancelabels the three counts that used to sit unlabelled in three places — retrieved, cited, and resolved — so a page showing 16, 3 and 6 reads as three different true things instead of a contradiction. - Point an eval at the account you are writing to.
prompt-and-message-evalaccepts a newaccountinput. When that company is in your conversation data, their own buyer-side words are retrieved and taggedaccount— the only evidence that licenses an account-specific claim — and the improved message can say "you told us" and mean it. Every quote now carries atier(account/segment/corpus) that bounds what a claim built on it may say, and reaching past a quote's tier is graded as a grounding failure rather than a style note. When the account is not in your data the run says so, and which way it was missing (never named, could not be resolved, no conversations, or our lookup broke) — it does not quietly grade you on cohort evidence under an account heading. - The prompt edits now come as one block you can paste. The prompt half of the report is the half that compounds — a fixed message helps one send, a fixed prompt helps every send after it — so the prompt suggestions are now also assembled into a single copyable
prompt_patch. It is composed from the same itemized suggestions, so it can never claim an edit the list does not, and the report renders one or the other rather than saying every edit twice. prompt-and-message-eval(now version 2.5.0) no longer fails a drafted message for being longer than 1,400 characters. Thebasic-hygieneupper bound is now 3,000 characters and is documented as an outlier guard — "this is a document, not an email" — rather than a style bar. The old bound was an unmeasured opinion: tested against 127,872 labelled outbound emails across four workspaces, the relationship between message length and getting a reply reverses sign between workspaces once you compare a sender against themselves. In one, cold openers over 1,400 characters replied 32 points _better_; in two others they replied 5-7 points worse. At the old bound the check was failing 8-24% of the messages those teams actually send, and in the first workspace it was failing the ones that performed best. Because runs are content-addressed over the eval version, previously graded messages re-grade on their next run rather than serving the old verdict. The lower bound (30 characters) is unchanged and is supported: messages below it are 0.3-0.6% of real sends. If you want a tighter house style bar, fork the eval and set your ownlengthcheck.
Removed
POST /search(search.run) and therunaction on the MCPsearchtool.- The standalone Search guide, folded into the Search endpoint guide.
Fixed
- External search now degrades instead of going dark when the search provider is unavailable, and it tells you which happened. A provider outage used to come back as
sources_timed_out, so a blendedsearchor anenrichbrief returned zero citations with no way to tell an outage from a slow source — reading, incorrectly, like the product had no web access at all. Two changes: an unusable provider now falls back to the secondary web-search backend rather than losing the source, and the blended result carriesexternal.source_failures[]({ source, reason }, wherereasoniserrorfor an outage vssource_timeout/deadlinefor slowness).sources_timed_outis unchanged and still lists every source that did not complete. - The grader is now told who you are writing to. The message rubric grades your ask on whether it suits the audience, and the judge was never given the audience — it was passed to the half of the run that WRITES the improved message but not to the half that SCORES it, on every run, while the report still said an audience had been resolved. It now reaches both, rendered identically, so that dimension is scored against a real referent. When no audience could be scoped, the judge is told that explicitly rather than left to guess one from whichever draft happened to name a cohort.
- A hygiene check no longer fails your message for phrasing its ask in an unusual way. The call-to-action check matched about a dozen stock phrasings and marked everything else as having no ask — which failed the whole basic-hygiene grader on roughly half of real seller emails, over asks as plain as "Register for the Feb 19 webinar" or "Move forward with signature to close before quarter end". Measured against a frozen corpus of 753 real messages, it wrongly failed 389 of them. A word list can show an ask is there; it can never show one is absent, so a miss now ABSTAINS instead of counting against you, and the grader scores over the checks that actually decided. On that same corpus the check now decides positively on 601, abstains on 152, and fails none; the hygiene grader as a whole goes from 109 passing to 340. Nothing was loosened — a banned buzzword or a length violation still fails exactly as before.
- Schema guidance for
sentiment_primaryandbuyer_pushbacknow reflects the 2026-07 objection-recall pass. The neutral rate is reported per word-count band instead of one corpus-wide figure, the recall improvement is scoped toobjectionon turns of 10+ words (other labels and shorter turns are unchanged), and subject-precision figures are restated against the current population.
Added
- Salesforce email comms filter. The SOFT CRM comms filter now supports Salesforce in addition to HubSpot: keep only external / rep-involved email (from the EmailMessage object) and drop internal-only messages before they're ingested. Configure it via
connections.set_comms_filteror thecomms_filterparam onconnections.connect; account, contact, and opportunity records are untouched. - The natural-language SQL helper (
data.ask) can now identify and join on a specific conversation turn, not just the whole call. A newgrouped_utterance_idcolumn exposes each utterance/turn's own id — the same value previously reachable only under the confusingly call-scopedinteraction_idname. Pairs with the existingsequence(turn order) andparent_interaction_id(call id) columns for turn-level analysis, e.g. tracking a seller's statement against the buyer's immediately-following response. Correction (2026-08-14): this column was originally described here as a "stable id". It is stable *within* a single read, but it is not durable across a pipeline regroup — regrouping re-mints the id for any turn whose group boundaries change. Join on it freely, but do not store it as a long-lived key in your own systems; derive it at read time, and useparent_interaction_idwhen you need an id that survives regrouping. buyer_pushback/pushback_typeon theinteractionsdata surface. Two new columns are now queryable viadata.query,data.ask, and MCP schema discovery, answering "did the buyer push back on adopting our solution" directly instead of the coarserobjectionsentiment label (which also fires when a speaker is describing their own business, not ours).buyer_pushbackis a tri-state flag —NULLmeans not yet judged,FALSEmeans judged and not pushback,TRUEmeans judged pushback — andpushback_typenames the kind (pricing, timing, proof burden, and others) whenTRUE. Coverage is a narrow, growing slice of conversations, so a low or zero count means "not yet judged," not "no pushback" — pair it with cluster search or a content search when you need full coverage.- Evals is available in every workspace. Grading a prompt or a message against your own customer quotes no longer depends on which agent surfaces your workspace has turned on - the Evals page, the run history, and the REST and agent-tool surfaces are open to everyone. Authoring your own evals stays in limited beta; reach out if you want in.
- Internal groundwork for validating the eval against human judgement. Platform staff can now label eval runs blind — per rubric dimension, with a pass / fail / can't-tell verdict and a one-line reason — and the resulting inter-rater agreement (Cohen's kappa per dimension) is computed against the human majority. This is the external anchor the grade has not had: until now the rubric was scored by a model and checked by a model, with nothing outside the system agreeing with it. Staff-only and non-tenant-facing. No change to how any workspace's runs are graded or reported.
- Evals - grade + improve content against your own customer data. The flagship
message-gradereval takes a prompt AND/OR a drafted outbound message (send one or both) and hands back a before→after report built around relevant positioning for the specific customer: it grades what you sent, then produces AND grades an improved, reusable prompt (a template that uses Amdahl to position, verify, and keep the messaging relevant to that customer - not generic, not just voice) + a relevantly positioned message - with an overall score and plain-language reasoning, per-dimension scores (relevant positioning, grounding, verified specifics, ...), verbatim customer quotes that back or contradict each side (retrieved from your data - the model can only cite them, never invent them), a score lift, and a one-line "what changed." Send a message and it also suggests a reusable prompt; send only a prompt and it simulates a draft to grade; the improved message is always a suggestion, never mandated. Every case declares a subject:provided(you pass in the content, like a message) orgenerated(the eval runs a question and grades the answer - thegtm-defaultregression harness).POST /evals/runover REST or theevalstool'srunaction over MCP validates your inputs against the eval's schema, starts the grading job, and returns a run id immediately; polleval_run://<id>for the verdict - an overall score, apass/partial/fail/not_applicablebucket, and a per-case, per-grader breakdown carrying the before→afterimprovementreport. A workspace with no customer data yet comes backnot_applicable, never a false fail. Six grader kinds ship (rule+improvement_loopover a message,evidence_judgefor a grade-only pass,deterministic/sor_anchored/judgeover a generated answer); browse them viagrader_kind://list. Author your own eval - configure the inputs, cases, and graders - withevals.create/update/delete, and dry-run a draft withevals.validate. Runs are read-only (they grade your data, never change it) and grade current data (internal-only in v1). Scopes:evals:executeto run +evals:writeto author (both editor + the customer-agent key bundle),evals:readto browse, validate, and poll (viewer). See the Evals guide.
Changed
- A long prompt submitted to
prompt-and-message-evalis now read in full. Previously the middle of a large document was dropped and the grader answered existence questions ("does this prompt require verification?") against a partial read, which could report a missing instruction the document actually contained. Every section is now indexed and the grade runs over a whole-document digest; coverage reportsmap_reducewith nothing omitted. - Prompt and Message Eval:
mode=advisory(anchored suggestions against what you already have) is now the default; sendmode=rewritefor a full improved prompt and message. Themodeinput field declares its default on the wire (input_schema.fields[].default) so run forms can preselect it. - Eval runs: a not-applicable verdict's
overall_reasoningnow states the actual reason (too-thin submission vs no customer data vs engine failure) and includes the abstaining grader's own explanation. - Eval run list: lean rows'
verdict_summarynow carries the primary case'stransition/input_passed/improved_passedwhen graded, so run histories can show the fail-to-pass story without fetching full rows. - API keys are now accepted as
Authorization: Bearer <key>in addition toX-API-Key— one key works on every transport (REST, MCP, and the team endpoints). - Self-serve API key creation (Settings → Developer) now offers a permission-bundle picker: any member can mint Read only or Customer agent (the default) keys; the elevated Internal agent / Full admin tiers require workspace admin.
- Eval run progress steps now carry structured per-stage detail (quote counts, suggestion titles, per-dimension outcomes) so run timelines can show the work behind each stage.
- Eval runs now show the work, not just the word count. Expanding a stage in a live run shows the claims it pulled out of your draft, the queries it searched with, and the retrieved quotes themselves with the same q-ids the finished report cites. The evidence pool also went from 20 candidates to 100, so the panel can show everything the grader had to choose from rather than only what it picked. What the grader reads is capped separately and unchanged, so a wider pool does not move your scores.
- Evals in the agent + a clearer eval list. The in-app agent (chat) can now run a graded eval — grade a prompt and/or a drafted message against your workspace's own customer evidence and read the result back — without leaving the conversation; it can discover which evals exist and dry-run an eval definition, while authoring an eval stays on the Evals page. The eval read now also reports each eval's
origin(builtin= Amdahl-shipped,tenant= authored by your workspace) so the console can label the list "By Amdahl" vs "By your team", and it always returns a valid case subject now, so the eval builder no longer fails to open an eval whose case had no configured subject. - The `audience` on a Prompt and Message Eval run is now evidence-gated. It used to be free text that reached the grader as prose, so a run could report confidently on "positioning for VPs of Engineering" whether or not your workspace had ever spoken to one. Now it resolves to a seniority and is checked against your own corpus: a cohort scopes the report only once it clears three floors (3+ distinct people, 25+ utterances, 2+ distinct companies — the last so a single talkative account cannot stand in for an audience). When it does not, the run still grades your prompt and message against your whole corpus and says which of five things happened, including telling you plainly when the check failed on our side rather than implying you have no data.
- Suggested Amdahl calls are bounded by what your key can actually run.
research_stepswere already validated against the live operation registry; they are now also narrowed to the calls your own scopes cover, so a suggestion you would get a403for is never shown. - Authoring your own evals is temporarily limited. While evals are in beta, creating, editing, and deleting evals is restricted — over the API, MCP, and the console alike. Contact the Amdahl team to request access. Running the shipped eval, reading results, and validating a definition are unaffected.
- Three rails so the report cannot assert more than it measured. A submission that is not commercial outreach is now refused rather than graded. The rubric scores craft — positioning, grounding, verified specifics, differentiation, the ask — and a short factual notice satisfies several of those for free by being concrete and having a clear next step, so it could out-score a real sales draft. The check rides on the intent call that already runs, and only a confident judgement refuses: an unsure or degraded read still grades. A prompt the system generated for you is no longer given a score when there is nothing to compare it against. The text still ships — on a message-only run the reusable prompt is the point — but grading our own draft told you nothing and returned full marks almost every time. A lift smaller than the instrument's own measured run-to-run spread is now flagged as not reportable, so it can render as "no measurable change" instead of as a confident figure. The raw value is still carried for analysis.
- Eval grading now runs at temperature 0. Every LLM call an eval made was sampled at the provider default of 1.0, so the same submission could score differently on a re-run for no reason connected to the writing. Graders also now emit their reasoning before their score, so the number is conditioned on the argument rather than rationalized after it.
- Eval reports now read like a person wrote them. Every sentence an eval shows you - the reasoning, the critiques, the suggestion titles and details, the what-changed line - is written against one house style: lead with the finding instead of clearing your throat, name the actor and let them act, cut the adverbs, be specific, and never close on a pull-quote. Plain hyphens only, everywhere.
- Re-submitting the same prompt or message to an eval now grades it again instead of returning the earlier report. The dedup claim previously held for the life of a completed run, so a submission graded once could never be re-graded against a customer corpus that had since moved. Concurrent duplicate submissions still join a single run. Reports now also retain the full candidate quote pool, not only the quotes that were cited.
- Prompt and Message Eval now grades blind, and grades your prompt separately from your message. The before/after report used to be written and scored by the same pass, so the reported lift was self-assessed. Generation and grading are now separate calls, and the grader scores both candidates in ONE pass without being told which is yours — so the number is a comparison, not an opinion about its own work. Your prompt and your message are now graded on their own rubrics with their own reasoning, cited quotes and worked examples; they never share a score. Retrieval is seeded from what you are trying to do and from each specific claim you make, rather than from the draft you asked us to fix. New:
mode: "advisory"returns anchored, surgical suggestions (keep/add/strengthen/remove/reorder) against the prompt you already have instead of a rewrite — every anchor is verified to be a literal line from your document. Long prompts are sectioned rather than truncated and the report states how much was graded. Produced artifacts carry ausagefield: an improved message isillustration_only— evidence the prompt is better, not a message to send. Your draft failing is now reported as the finding it is (input_verdict/improved_verdict/transition) instead of as an eval failure. - The improvement report now records WHY the revision loop stopped, alongside how many rounds it ran.
transition.stop_reasonis one ofthreshold_met,no_critique,deadline,model_call_cap, ormax_iterations.iterations: 1was previously produced by two opposite situations — the first round cleared the bar and no revision was needed, or the phase deadline expired before a second round could start. The reported score is the best round, so the two mean different things about whether that score is final. The field is optional, so a run stored before this change reads unchanged. - Evals: one shipped eval, renamed for clarity. The default eval is now Prompt and Message Eval (
prompt-and-message-eval) — it grades two artifacts, a prompt and a message, and it is not tied to a channel. Its message input is nowmessage(wasoutbound_message). Both retired slugs (outreach-eval,message-grader) still resolve and the retiredoutbound_messageinput key is still accepted, so existing integrations keep working. Thegtm-defaultregression-harness eval has been retired; thegeneratedsubject mode it used is unchanged and still available to evals you author. Docs now carry a high-level Evals endpoint page plus a page per shipped eval. evals.runnow meters and caps like the other synchronous verb families: every dispatch records a usage row, and an operator-setmax_evals_calls_monthhard cap returns the standardquota_exceeded(429) envelope when exhausted. Unset caps (the default) change nothing.- Everything Amdahl writes for you now follows one house style. Chat answers, research summaries, content drafts, living documents and eval reports all compose the same rules: lead with the substance, name the actor, cut the adverbs, be specific, vary the rhythm, and use plain hyphens rather than em dashes. Previously only eval reports did.
- Amdahl now runs on the current Claude model family. The general-purpose default moves to Claude Sonnet 5 and the deep-reasoning tier to Claude Opus 5; Haiku 4.5 is unchanged. This reaches everything that composes on your behalf, including the synthesis behind
external_searchand the deep-search briefs, which previously ran on a two-generation-old pinned model. Two consequences worth knowing: these models reason before answering by default, so answers can take slightly longer and the response budget was widened to leave room for it; and their sampling behaviour is fixed by the model rather than configurable, so any per-agent temperature setting you have stored is now advisory on those models. Agents pinned to an older model keep their existing behaviour.
Fixed
- MCP OAuth discovery now advertises the host you actually connected on. The
/.well-known/oauth-authorization-serverand/.well-known/oauth-protected-resourcedocuments previously returned a hardcoded origin, so a client reaching the API on a newer Amdahl domain was handed anissuer,authorization_endpoint,token_endpoint, andregistration_endpointpointing somewhere it could not reach — breaking the OAuth handshake. Every public URL we hand out (the OAuth issuer, the MCP endpoint, console deep links, and connector callback URLs) is now resolved from deployment configuration. Existing integrations need no change; if you had pinned the old discovery URLs by hand, re-run discovery instead. - Console and pricing links we hand back now point at a domain that resolves. The artifact-embed prompt returned a
console.amdahl.codeep link and the seat-pricing constants named the old pricing page, both left over from the move toamdahl.ai. Internal notifications, connector OAuth callbacks, and the API's CORS allowlist were fixed at the same time: the callback list only offered the apex host, so a connector registered against the app host could fail token exchange withredirect_uri_mismatch, and the CORS allowlist could only ever name one domain at a time. Both domains are now accepted for the duration of the move, so links already shared keep working and nothing has to be re-registered. GET /eval-runsnow rejects an unrecognisedstatusfilter with400instead of silently returning every run unfiltered, and echoes the appliedfilterson the response. A malformedlimit/offsetis rejected too; an out-of-range one is still clamped to the allowed bound.POST /evals/runnow rejects a non-string value for a text input instead of stringifying it — an object no longer arrives at the grader as"[object Object]".- Eval runs no longer report a graded scorecard when the customer-evidence retrieval failed. A fan-out that returns zero quotes because the underlying reads errored is now refused as
evidence_unavailable— kept distinct fromempty_corpus, so "we could not load your customer evidence" is never presented as "your message is not grounded in customer evidence". Evidence reads are also bounded process-wide, so a burst of concurrent runs no longer exhausts the shared BigQuery embedding-function slot pool that caused those failures. - Fixed: eval run resources are now readable over MCP (
eval-run://andgrader-kind://wire aliases), eval run history lists return lean rows (full verdicts via?include=full), live per-stage progress streams while an improvement report is being written, verdicts now state when the improved version passes but the submitted draft did not, and an identical submission completed within the last 15 minutes reuses its report instead of re-grading. - Chat turns, scheduled routines and agent runs no longer fail immediately with
400 ... "temperature" is deprecated for this model.Anthropic removed the sampling parameters (temperature/top_p/top_k) from its newer models, and a request carrying one is rejected in full before any work happens, so an affected run ended on its first turn having used no tools and produced no answer. Every surface that talks to a model now decides whether those parameters are deliverable from the model the request is actually sent to, rather than sending them unconditionally: the agent runtime, the shared completion path behind roughly two dozen internal callers, and the voice-analysis, follow-up-question and structured-output paths. Where a model accepts the parameters, the caller's requested value is honoured as before. - The API and MCP endpoints are reachable at
app.amdahl.aiagain, and the OAuth discovery document's documentation links now resolve. A production deploy briefly reverted the hostname because the deploy re-applies the committed App Platform spec, which had not yet been updated. Sign-in redirects now return you to the domain you signed in from. No data was affected. - Eval reports now render generated prose with plain ASCII hyphens instead of the em/en dashes the grading models write, on every surface (console, REST, MCP). Retrieved customer quotes, the evidence pool, and the text you submitted (including suggestion anchors into it) are never rewritten - a quoted utterance stays byte-identical, and anchor offsets into your draft remain valid.
- An eval whose improvement report could not be produced now abstains instead of reporting a manufactured neutral score. Previously a failed generate or grade call returned 0.5 out of 1 with no dimensions and no quotes behind it, which read as a genuine middling grade. Separately, a report cut short by its output budget is now recovered rather than discarded, so a rich submission keeps the improved prompt and message that were written before the cut.
- Eval evidence retrieval now retries a failed read instead of giving up on the whole fan-out. A submission rich enough to make several distinct claims plans one retrieval leg per claim, and those legs run in rounds — but the shared deadline was sized for a single round, so the better and more specific the writing, the more likely the entire fan-out was cut and the run refused to grade. The deadline now covers the rounds plus retries, and a leg whose read errors is retried with jittered backoff. A read that genuinely matched nothing, or a workspace with no customer themes yet, is still answered honestly rather than re-asked, and a sustained outage still refuses to grade rather than scoring against no evidence.
- A grader that returned no scored dimensions for a candidate no longer publishes a 1 out of 5. Missing dimensions were filled at the floor, so a judge that scored nothing produced a confident bottom verdict a reader could not tell from a real one — and because the submitted side is the likelier of the two to come back sparse, it inflated the reported lift. Reports now also record how many rubric lines were genuinely assessed.
- The MCP
dataschema guidance no longer suggests filteringsentiment_primarytoobjection(or the other negative labels) as a way to find or rule out objections and risk. Measured against production data,sentiment_primaryis 95.2% neutral on buyer turns while a hand-coded sample of the same population found roughly 23% objection-or-stall content, so a filter on this column misses on the order of 8x of the negative content a person would flag, and a zero or low match count is not evidence an account is risk-free. The guidance now points atcluster_searchand a direct search over the utterance text instead, and a structural test guards against the same guidance regressing.
Highlights
- More trustworthy revenue analysis. Declare whether your CRM deal values are monthly, quarterly, or annual, and every report states the basis instead of assuming annual.
- Compliance-first call ingestion. Filter or exclude calls before their transcripts are ever fetched, so a call you do not want in Amdahl is never pulled over the wire.
- Faster GTM intelligence. Common questions about competitors, objections, champions, and pipeline now return faster and more reliably.
- More capable developer platform. New search modes, enrichment, lookalikes, embeddable living-doc widgets, and improved OAuth discovery.
Breaking
- Saving CRM Mappings with a
deal_amount_tiers,segment_rules,default_segmentoruse_crm_segmentkey is now rejected. Existing saved settings still load — the retired keys are ignored — but a caller that builds its own payload must drop them.
Added
- Deal amount unit. No CRM records what period its amount field is in — Salesforce
Amount, HubSpotamountand Pipedrivevalueare bare numbers — so a workspace that tracks deals monthly had its deal values read as though they were annual. You can now declare what your amount means, per CRM source, under Settings → CRM Mappings (monthly,quarterlyorannual), along with its currency. Deal and interaction queries carry that declaration beside the value asdeal_amount_unit/deal_amount_currency, and the assistant reads it — so an answer says "$3,750 monthly" rather than a bare "$3,750". Your numbers are never touched:deal_amountstill shows exactly what your CRM shows. We surface what it means instead of converting it, because there is no honest conversion — a one-time implementation fee isn't a rate, and only you know whether your amount is a subscription or a whole contract. Until you declare a unit it readsunspecified, so a figure is reported as unknown-basis rather than assumed annual. Onboarding now offers this as a quick (skippable) step, and your home-view setup checklist keeps a reminder open until every connected CRM has a unit — so it is easy to set later if you skip it. We never guess the value for you: it stays "Not declared" until you choose one. - `firmographic_segment` on interaction and deal queries: SMB / mid-market / enterprise from company headcount and revenue. It abstains rather than guess when a company's size straddles a band boundary.
- New
knowledge_base.get_node_widgetoperation (GET /knowledge-base/:id/node/:node_key/widget) returns one living-doc node's figure as a standalone single-visualizationPageSpec, so a pinned chart or stat can render as an embeddable widget card — regenerated on read to stay in sync with the doc. - Pre-sync call filters (compliance control). A compliance-sensitive workspace can now ensure that non-qualifying call transcripts are never fetched into Amdahl in the first place — not filtered out after the fact, but never pulled over the wire. On a supported calls connector (Fathom, Fireflies, Gong, Grain), open the connection's Call filters section and choose the rules a call must match to be ingested: _external only_ (the call has at least one participant outside your own email domains) and/or _includes a rep_ (one of the people you list attended or hosted it). A call is ingested only if it matches every rule you set; a call that matches none is never fetched. Your own domains are added automatically, so you never type them in. A calls source stays paused until you save a filter — it will not sync a single call until the rule is in place — so ingestion can only ever begin with the control applied. The filter is inclusion-based and fails safe: if a call's roster can't be determined, it is excluded. (Granola support is coming in a follow-up; it is not offered as filterable yet.)
- Personalized first wins in developer onboarding. New developers now start from a short list of first-win cards computed from the workspace's own data — the top account by conversation volume, the loudest conversation theme, and a named competitor from the company profile — instead of a generic template. Picking a card opens Search or Chat with the ask prefilled, so the guided tour begins on a real question about your own pipeline. Workspaces whose data has not landed yet get safe generic cards, so onboarding never blocks on thin data.
- `external_only` on `context.query_substrate`. Restricts returned evidence to what the other side of the conversation said — customers, prospects, partners — and drops your own reps' messages and internal email. Set it for voice-of-customer, ICP, persona and positioning questions, where an internal message is evidence of what you say, not of what a buyer thinks. Defaults to
false, so existing calls are unchanged. - Call filters can now EXCLUDE calls, not just include them. The pre-sync call filter gained an exclusion carve-out:
exclude_participants(emails) andexclude_domains(whole domains, suffix-aware) onconnections.set_call_filtersand the atomicconnections.connectcall_filters. Any call where a blocked email or domain is an attendee/host is never ingested — its transcript is never fetched — and exclusion is evaluated before the include rules, so a blocked party always wins over an otherwise-qualifying call. Exclusion is a valid standalone choice ("ingest every call except anything involving@sensitive-client.com") and composes with the existing external-only / rep rules. It keeps the same fail-closed guarantee: an unresolvable roster still drops the call (we can't prove the blocked party is absent). Existing filters are unchanged. - Connecting a source now checks the credential against the provider — and shows you what it found. Until now, connecting with an API key stored the key and marked the source connected without ever calling the provider, so a wrong or expired credential stayed silent until a sync failed. Connecting Salesforce, HubSpot, Gong, Salesloft, Aircall, Fireflies, Fathom or Grain now makes one read-only call to the provider and records what it saw. The connection's summary (
GET /connections/:id/summary) shows that evidence right away — "12,400 deals, latest yesterday" — instead of an empty card while the first sync runs, and the account name the credential opened, so a valid key pointed at the wrong account is obvious. The numbers are exactly what the provider reported, marked as such: a provider that will not give a total shows its date range rather than a made-up number, an approximate or capped figure is labelled approximate, and a real zero is shown as zero. Once your data lands, the summary switches back to your own synced data. Nothing is read that was not already read by the sync, no message or meeting content is stored, and a failed check never blocks connecting. - Fast Search is faster and more reliable on the common GTM asks. Recurring questions — competitor mentions by account, the rival landscape and win rate, objections, pain points, feature requests, champions, economic buyers — now run over the signal your pipeline already computed (so "which accounts mentioned a competitor in the last 60 days" is a sub-second lookup, not a slow text scan that timed out). Anything outside that set still writes SQL for you, but that call is now bounded and tolerant, so a stray bit of model output no longer aborts the search.
- Every Search result now carries a `coverage` object —
latest_event_at,latest_ingest_at,total_rows,days_behind,is_stale— so you can show how current your workspace data is. When your data is materially behind "now" and a recent-window ask comes back empty, the resultmessagesays so in plain language, so an empty answer reads as a data-freshness gap rather than a mysterious failure. - Map your CRM stages while the first sync is still running. Connecting a CRM now fetches its reference objects — pipelines, stages, owners, field definitions — within seconds, so the stage-mapping review is ready immediately instead of waiting for the full sync to land. A new
GET /console/crm-metadatareturns the catalog, and mappings are seeded from it as candidates you review rather than facts we assume. Seeded entries never count as confirmed until a human has actually looked at them, and a follow-up pass picks up any stage that appears on old deals but no longer exists in the CRM's current catalog. - OAuth discovery now teaches agents how to onboard themselves. The authorization server metadata at
/.well-known/oauth-authorization-servernow includesservice_documentationand anagent_authblock (per the emerging auth.md convention): where to register a client (open dynamic registration), the identity and credential types to expect (anonymous registration, bearer access/refresh tokens), where to revoke, and pointers to the step-by-step recipe athttps://amdahl.ai/auth.mdand the agent-skills index athttps://amdahl.ai/.well-known/agent-skills/index.json. An AI agent pointed at either domain can now discover the full register-authorize-connect flow without human help. Purely additive — existing OAuth clients and tokens are unaffected. - New per-user pinned-figure operations —
pins.list(GET /pins),pins.pin(POST /pins),pins.unpin(DELETE /pins/:document_group_id/:node_key) — let a user pin individual living-doc figures (bydocument_group_id+node_key) to their Home, each rendered standalone via the node-widget read. Personal to the caller (gated by the newpins:read/pins:write/pins:deletescopes); console + REST + Anthropic only. - Structured search modes on the API:
POST /search/queryroutes each ask onto typed filters, plain-language search, or meaning-based (semantic) matching over your conversation data — force a lane withmode, or let Amdahl pick. Discover the filterable fields atGET /search/fields. - Enrichment endpoints:
POST /enrich/company,POST /enrich/person, andPOST /enrich/topicreturn a cached brief instantly when one is fresh, your own first-party evidence while a full refresh runs in the background, or the complete fused brief inline withmode=full. - Lookalikes:
POST /lookalikefinds the companies or deals most similar to a seed account, andPOST /lookalike/themesfinds the customer-conversation themes closest to any question — both ranked by similarity over your own corpus. - Revenue Review living document. A new grounded, always-refreshed report on new-business revenue: closed-won revenue and quarter-over-quarter growth per quarter, plus win rate by deal-size band. Every figure is a computed value the assistant reads back verbatim — the growth rate is calculated in the query itself rather than derived in prose, and each dollar amount carries its CRM period (annual / monthly / …) so a total is never silently re-scaled. It abstains where a band's sample is too small to give a stable win rate instead of reporting a swingy percentage.
- CRM recency window. When you connect a CRM (Salesforce, HubSpot, or Pipedrive) you can now choose how much activity history to import — a month window (e.g. the last 12 or 24 months) or all history — so you decide up front how far back emails, meetings, calls, notes, and tasks are pulled. Your account, contact, and deal records always land in full; the window only bounds the activity/communications data. It defaults to the last 12 months, and you can change it any time on the connection — widening re-pulls the older activity, narrowing drops what falls outside the window, and reference records are never touched.
- CRM comms filter (external / rep-involved). For HubSpot connections you can now filter which email activity is imported to only communications that involve an external party (a customer/prospect) or a specific rep — internal-only or off-topic emails are dropped before they ever land in Amdahl. Your account, contact, and deal records are never affected. It's a soft filter (when a message can't be classified it is kept), it's off until you configure it, and changing it re-pulls just the affected email activity to apply the new rule.
- The assistant's learned data-access rules now reach the fast search-to-SQL writer and connected MCP clients, not just the living-doc author. When Amdahl learns a workspace-specific access rule (e.g. a field substitution or a grain caveat that keeps a query correct), that rule is now projected into every place SQL is written, so answers stay consistent across the console's fast search, the MCP surface, and living documents. Inert until a rule is learned; no change to existing behavior.
Changed
- Connect a call recorder and set its filter in one step. Connecting a call recorder (Fathom, Fireflies, Gong, Grain) now lets you choose what it ingests in the SAME action as connecting it — one request instead of connect-then-configure. On the API,
POST /connections(connections.connect) accepts an optionalcall_filtersfor an api-key recorder: pass an enabled filter to keep only matching calls, or{ "enabled": false }for the "ingest all calls" opt-out. The filter is fully validated before anything connects, so an invalid choice never leaves a half-configured source, and the recorder starts syncing with your decision already in place. Omitcall_filtersand nothing changes — the recorder stays paused until you set a filter later, exactly as before. Setting a filter still requires workspace admin. - Call recorders: choose what you pull, right when you connect. Connecting a call recorder (Fathom, Fireflies, Gong, Grain) now asks you to decide up front what it ingests: set a pre-sync call filter to keep only calls that match (e.g. calls with an external participant and/or a named rep — everything else is never fetched), or pick Ingest all calls to bring in everything. The source stays paused until you choose, so a workspace that must filter for compliance can't accidentally start pulling calls before it's configured, and a workspace that doesn't want filtering is one click away. Already-connected sources are unchanged, and their manual Sync now works as before.
- Customer-voice living documents (persona voice deep-dive, voice of customer, cross-persona divergence, proof & ROI) now surface genuine customer and prospect voice. Utterances from non-sales relationships that share a prospect's email domain — investors, advisors, press/media — are filtered out, while a real buyer on such an account is kept via their attached deal. Each voice panel now spreads across many speakers and accounts instead of over-representing whoever happens to have the most high-scoring quotes.
data.cluster_searchgains an optionalfacet_filterso themes can be sliced by account relationship (account_relationship) or deal context (deal_status/deal_stage/deal_presence). - Deal amounts now carry their period in your reports. The living GTM reports that total or average deal value — Win/Loss, Pipeline Health, ICP Signal, the GTM Health Report and the Weekly GTM Digest — now state the native period of every deal-value figure (e.g. "$1.2M open pipeline (monthly)") and never annualize it, reading the
deal_amount_unityou declare per CRM source under Settings → CRM Mappings. A workspace that tracks deals monthly no longer has its report figures read as though they were annual. Where no unit is declared the report calls the basis unknown rather than assuming annual, and where you connect more than one CRM with different units it states both rather than summing across them. Your numbers are untouched —deal_amountstill shows exactly what your CRM shows; the report surfaces what it means instead of converting it. - Ask about pipeline value in chat and the answer now states the period too. When you ask an Amdahl agent — or your own MCP client — for pipeline or per-stage value from the ready-made funnel rollup (not just the living GTM reports), it now reads the
deal_amount_unityou declared per CRM source under Settings → CRM Mappings and states the native period next to the figure (e.g. "$3.3M in the POC stage (monthly)") instead of presenting a monthly figure as though it were annual. A workspace that tracks deals monthly now gets an honest basis on every ad-hoc pipeline answer, matching what Win/Loss, Pipeline Health and the rest of the living reports already do. Your numbers are untouched — the figure is exactly what your CRM shows; the answer surfaces what it means. - Workspace domains are active the moment you add them. Adding a company email domain under Settings → Team → Domains used to leave it "Pending verification" with no way to complete the verification — so turning on domain self-join did nothing and eligible teammates never saw your workspace in the join picker. The verification step is gone: every domain you add (and every domain already sitting at pending) is active immediately, and with Domain self-join turned on your teammates can find and join the workspace from signup or the workspace switcher right away. The guardrails are unchanged — public mailbox domains (gmail.com and the like) still can't be claimed, self-join stays off until you opt in, and the workspace owner is emailed on every join.
- Fast Search (
POST /search, thesearchMCP tool) now handles multi-intent asks. A question that carries more than one independent data intent — "the objections we hit from 11x, and how are deals with Acme going?" — is planned into separate sub-questions, each queried and returned as its own entry in a newgroups[]field (the flatinternalfield is retained as the primary group for existing callers). When part of an ask is not a warehouse question (advice, "what should I say"), the response now carries anescalate_reasonpointing to Chat while still returning the data groups it could answer. - The public API and MCP surface is now Search + Agents. External callers see two surfaces: Search (
search.run) — a synchronous query over your customer conversations, optionally blended with a quick web + news pass — and Agents — the multi-turn Chat ask-door, a reusable agent library, and cron-driven Routines. The MCP server exposes exactly thesearchandagentscoarse tools, and an external credential — a platform API key OR an MCP/SDK OAuth token — can call only the operations behind them over REST; other endpoints now return403 not_on_public_api. Nothing changes inside the Amdahl console — every workspace surface stays available there. - Meaning-based (semantic) search now ranks individual customer utterances, not whole calls. A "sounds like…" / "about X" query on
search(query) previously matched at the conversation level and returned one representative snippet per call; it now retrieves the specific buyer-voice utterances closest in meaning to your query, each carrying its own verbatim text and naming the parent call it came from (interaction_idon every match). Matching is scoped to the customer/prospect side of the conversation, so a voice-of-customer search surfaces what buyers said rather than your own reps. Results are drawn from a fresh rebuild of the semantic index at the finer grain — brand-new coverage fills in over the first sync after release. - Enrichment briefs (
POST /enrich/{company,person,topic}) now stay fresh. Fast mode still returns a cached brief instantly, but it also kicks off a background refresh on every hit, and cached briefs now expire after 24 hours (previously 7–14 days). You get the same fast answer while the underlying brief is continuously kept current. - Revenue now reads as true ARR where your CRM records it. The Revenue Review living document and the assistant's revenue math now prefer native annual recurring revenue (from HubSpot's ARR field) and fall back to the deal amount only where ARR is not set — instead of summing the CRM deal amount, which overstates revenue for multi-year contracts (their whole-contract value is not annual). Every figure states its basis (ARR vs deal amount / total-contract value), and the assistant is steered to the ARR-preferring measure when it writes revenue queries. Where ARR is not yet populated the totals are unchanged, and they sharpen automatically as the CRM ARR passthrough fills in.
Removed
- Deal amount tiers. The hand-authored amount bands under Settings → CRM Mappings (and the
deal_amount_category/company_segment_normalizedvalues they produced) are gone. They bucketed a deal amount whose unit was never declared, so they were wrong for any workspace tracking deals monthly. Usefirmographic_segmentfor company size, and readdeal_amountwith its newdeal_amount_unitfor deal value. - `deal_size_band` is no longer returned by deal and interaction queries.
Fixed
- The `data` tool no longer mistakes your own company for an account. The schema guidance now states that
company_nameis the EXTERNAL party on each interaction (the prospect / customer you're talking with), never your own business — every query is already scoped to your workspace. This stops the SQL-writing agent from adding acompany_name LIKE '%<your company>%'filter to "find our calls," which silently returned zero rows even when the conversations were there. - `context.query_substrate` now honours your `query` on the broad intents. On
research_overviewandsummarize_researchthe query text reached theme ranking but not the evidence ranking, so those two intents returned a spread across every account in the session rather than the ones matching what you asked — the wider your workspace's data, the more off-topic the result looked. Both now rank evidence by relevance to your query while still favouring a spread of accounts over many quotes from one. The other intents are unchanged, as is a call with noquery, which is still a deliberate broad scan. - An abandoned connect no longer sits in "Connecting" forever, and no longer reports itself as healthy. When an OAuth handshake did not finish — you closed the provider's consent screen, the provider rejected the request, or the authorization link expired — the connection was left in a state only the completed handshake could clear: nothing retried it, no sync could reach it, and
connections.get/listreported itshealthashealthyeven though no credential existed and no data was flowing. Two changes: a connection with an unfinished authorization now reportshealth: "needs_reauth"(and, where connection health alerts are switched on, can raise one), and an authorization left unfinished for over an hour — past the point its authorization link can still be used — is now released tostatus: "disconnected", which is the stateconnections.reconnectrestores from in place. No connection that holds a credential is affected, and an in-flight connect is never interrupted. - `search` now answers the question you asked. A negative-sentiment ask ("pain points", "complaints", "concerns") was being rewritten to "objections" before it ran, and a company or account name in the ask ("objections from Acme") was silently dropped — so the answer came back mislabeled or scoped to everyone. The planner now keeps the exact sentiment word you used (and invents none you did not), preserves every entity name verbatim, and the curated fast-search templates route on your original wording rather than a paraphrase. An entity-scoped ask now scopes to that entity, and an aggregation or "themes" ask falls through to the query writer that groups correctly instead of dumping raw rows.
- `search` meaning-based (semantic) matching now spans your whole corpus. Results were collapsing to a single conversation with identical similarity scores because the semantic index only ever populated a thin sliver of it. The index now builds fully, so a "sounds like…" search ranks across every relevant conversation. An entity-scoped, filter-shaped ask is also routed to the exact-filter path rather than semantic matching, where a name match is what you want.
- `search` accepts a pure distribution query. A
group_by+ metric aggregation with no filter ("distribution of sentiment across everything") is now accepted instead of rejected as an empty request. - `search fields` lists the allowed values for enum fields. Low-cardinality fields (
sentiment_primary,record_type,outcome_band, and similar) now carry theirsample_values, so you can discover the vocabulary from the field catalog alone. - `enrich` returns your first-party evidence for real customers. A cold lookup on a company, person, or topic was timing out its evidence read and reporting zero matches even when the account had thousands of conversations; the read now has room to complete.
enrichon a company also accepts anamewhen you have nodomain, resolving it to the account in your workspace instead of erroring. - `lookalike find` works by domain. Passing a company
domainnow resolves it to the seed account before the similarity lookup — the by-domain path previously reported that centroids were not built. The "not found" reason is also split into distinctdomain_unresolved/entity_not_found/centroids_not_materialized/errorcases so you can tell what actually happened. `lookalike themes` now honourslimit, returns the real theme id and match score, and de-duplicates repeated themes. - Meaning-based (semantic) search now respects per-member data-scope. When a workspace restricts a member to specific accounts (a data-access rule), a "sounds like…" / "about X" query on
search(query) now returns only customer utterances from the accounts that member is allowed to see — the fast semantic index applies the same company scope as structured search and the rest of the data surface. A member with no restriction, and an admin/owner, are unaffected. - Quarter and date-range totals are now correct at the day boundary. When a question or report filtered a date column with an inclusive "through <date>" bound (e.g. deals closed _through the last day of the quarter_), any record whose timestamp fell later that same day was silently dropped — so a quarter total could omit a deal that closed at 5pm on the final day and overstate the quarter-over-quarter change. The assistant now compiles every such bound to the correct end-of-period boundary automatically, on every data surface, so a "through June 30" total includes all of June 30. Your queries need no change; explicit end-of-day timestamps are untouched.
- Breakdowns abstain instead of inventing a chart over sparse data. A rate or breakdown (e.g. win rate by industry) is only shown when the grouping field is populated on enough of the population, and a per-group rate is only reported once its sample is large enough to be stable — so a chart is never drawn over a field that is a fraction of a percent populated, and a "win rate" is never computed from a handful of deals.
- Restored the pre-sync call-filter safeguard for meeting/call connectors (Fathom, Grain, etc.). An internal query referenced a column that no longer exists, so the gate that pauses an unconfigured call connector until you pick a transcript filter was erroring on every check and falling open. The gate is enforced again — a new call connector stays paused until you choose a filter or explicitly opt into ingest-all.
- Semantic search (
POST /search/querywithmode: "semantic") no longer returns unlinkable duplicate rows. A small set of customer utterances in the source data carried no parent conversation id and shared a placeholder embedding; a query that happened to match them returned a cluster of near-identical results with a nullinteraction_idand no way to open the underlying call. Those rows are now excluded end to end, so every semantic result names the conversation it came from.
Added
version_provenanceonknowledge_base.upload. When you append a version of a living document, you can now record which of the run's data queries each section was written from, as[{ heading_anchor, query_keys }]. Those sections become live: the console underlines them and shows how the underlying query has moved since the last version. Optional — a section you declare nothing for is simply left unbound, and a key naming a query the run did not execute is discarded.
Breaking
- Chat and Routine runs now default to `deep` investigation depth. A run that does not set
depthexplicitly previously usedstandard; it now usesdeep— which runs on a more capable model, allows more turns, and turns on external web search and the divergence view by default. Deep runs are more thorough but slower and cost more. To keep the previous behavior, passdepth: "standard"(or"quick") explicitly on the chat/routine config.
Added
- Data Filters can now exclude specific CRM deals, not just calls and emails. An excluded deal drops from the
dealsdata surface, win-rate, and pipeline value, and its linked interactions are removed from the corpus — reversible by removing the rule. The filters preview also shows a per-object breakdown (deals, companies, contacts). - Your workspace agents can now build and manage other agents and routines from inside a Chat. The Master (and any sub-agent it dispatches) can create, edit, and retire workspace agents (
agents.create_agent/update_agent/delete_agent) and scheduled routines (routines.create/update/delete/run_now) directly, instead of only drafting them for you to save. A created agent runs under the same permissions as the agent that made it, and the assistant asks for confirmation before deleting an agent or a routine.
Fixed
- "Apply" on CRM Mappings now reliably re-normalizes your data, even when the corpus has silently drifted. The pipeline only re-normalizes when the saved mapping version differs from the applied version, so a workspace whose data drifted from a stale "applied" stamp (e.g. an old backfill that marked mappings applied without fully normalizing) could show thousands of pending rows in the impact preview yet have "Apply" do nothing. Apply now reopens that gate before triggering the pipeline, so the re-normalization actually runs.
Added
- CRM stage mappings are now auto-suggested for a workspace that has none. Opening CRM Mappings on a workspace that has never configured them now generates a first-pass set from your own pipeline data — each of your CRM's deal stages classified into a standard funnel stage — so the page starts with editable suggestions instead of blank. Suggestions are marked as auto-generated and are never applied to your normalized data until you review and save them.
- Answers now bind claims to their evidence inline. During synthesis the agent hyperlinks a specific factual or qualitative claim to the citation / table / chart / metric block that backs it, right in the prose, using the closed
amdahl:citelink grammar — a markdown link whose destination isamdahl:cite?block=<id>(blockrequired — the id of a presented evidence block;fuoptional — a follow-up question). Rich clients render the bound phrase as an evidence chip; every surface (Chat READ, terminal answer frame, live SSE) carries it. A cite that points at a block the answer never presented, or that fails the closed grammar, is unwrapped to plain text, and the flattenedanswer_textfor MCP consumers / logs strips the scheme entirely so prose reads clean. Orthogonal to the existingamdahl:qfigure-exploration links — the two never clobber each other. - A historic living-doc version can now show its provenance.
knowledge_base://<id>/suggestions?resolved=1(RESTGET /knowledge-base/:id/suggestions?resolved=1) returns the suggestions APPROVED into that version or DENIED against it — accepted suggestions matched by the version they produced, dismissed ones by the version they were proposed against — each carrying itsstatus,resolved_at,resolved_by_user_id, andresolution_note. The default (proposed) read is unchanged. - Living docs now have a per-doc "Review before publish" toggle. A living-doc workflow tagged for auto-update publishes each refresh straight to
current; flip a doc to "review before publish" and each new refresh instead lands as aproposedversion for a human to promote, keeping the approve/decline review meaningful on docs that otherwise auto-supersede pending suggestions. Read the current mode withGET /knowledge-base/:id/review-mode(resourceknowledge_base://<id>/review-mode) →{ document_group_id, review_before_publish }; set it withPOST /knowledge-base/:id/review-modebody{ review_before_publish: boolean }. Default is unchanged (auto-update) for every existing doc. The setter is console/REST + Copilot only (not on the MCP coarse tool). - A successful SQL-shaped tool call now carries a bounded preview of the rows it returned. The
tool_completeagent event (live SSE, persistedstep_data.events, and the Chat run READ?include=eventsreplay) gains an optionalresultSample—columns, up to 25rowskeyed by column, and atruncatedflag when the real result had more rows or columns than shown. Present only for tabular successes (data.query,data.ask,search.run); absent for non-tabular, empty, or failed calls. Lets a client render the actual returned data (table or chart) instead of just a row count. - The CRM Mappings settings tab now opens with a deal-grain summary of your pipeline — total deals, open pipeline, and won / lost value — plus a per-stage deal count on every mapping, so you can map your CRM stages against the numbers you already know.
Changed
- Cluster-driven figures in Chat answers are now citable. A count that comes from a customer-conversation theme — "349 SMB deals were lost", "541 executive conversations" — comes from a
data.cluster_search/data.cluster_detailread, not a warehouse row, so it previously had no openable evidence to link to. Answers can now present a theme as acluster_findingblock (its title, insight, narrative hook, and member count, plus representative rows) and bind the figure to it withamdahl:cite, so clicking the number opens the actual theme behind it. - Every backable figure links reliably, with no fixed per-answer cap. The linked-data-phrase guidance is now coverage-based — link every figure a presented block backs, not a favored two or three — and a deterministic pass at the answer boundary wraps any bare figure that exactly and unambiguously matches a presented block's headline value (a metric value, a theme's member count) in a cite to that block. A figure the answer wrote as plain prose still carries its evidence link; it can never bind to the wrong block.
- "Continue" after a stopped answer keeps its context. Stopping an answer mid-stream and then sending "continue" no longer cold-starts as a fresh session — the stopped turn's question and partial answer stay in the conversation's context, so the agent picks up where it left off instead of asking you to restate what you were doing.
- Chat asks a sharp clarifying question when — and only when — it matters. When a request is ambiguous in a way that would genuinely change the answer (Enterprise vs SMB, win-rate vs velocity, "recent" as 30 vs 90 days) and guessing wrong is costly, the agent now pauses with one question, preferring a quick multiple-choice pick when the options are enumerable. Otherwise it answers and states the assumption it took as a one-click steerable fork — never a wall of clarifications, never a forced gate.
- Verbatim customer quotes in Chat answers now render as citation evidence, not quoted prose. When an answer leans on a customer's exact words, the agent presents the quote as a
citationblock (kindutterance— an attributed, openable evidence card) and binds the claim to it inline withamdahl:cite, instead of stacking literal double-quoted lines in the prose. Answers also now bind every specific and qualitative claim to a REAL presented block — atable/chart_speccarrying the underlying query + rows, or the cited quote — so a reader can open the actual evidence behind each statement rather than take it on faith. - Theme-search tool cards now preview their results. An expanded
data.cluster_searchstep in the Chat transparency panel shows a sample of the themes it returned (label, score, representative quote), matching the row preview already shown fordata.query/data.ask/search.run. A degraded or empty theme read shows no preview.
Fixed
- Living-doc suggestions now match the version you're viewing. The suggestion rail + inline redlines on a living document are pinned to the version being rendered, so proposals authored against a superseded version no longer appear mis-anchored on the current document.
knowledge_base://<id>/suggestionsfilters to the requested version by construction. - CRM Mappings settings no longer fail to load for workspaces whose saved mappings predate deal-amount tiers. A mapping record created by an earlier import could be missing its amount-tier section, which surfaced as a server error on the settings page's change-impact preview and left the page unable to load. Older records are now always read as a complete, well-formed mapping, so the page loads reliably regardless of how the mappings were first created.
- Living-doc suggestion rationales no longer overcount related fields. A section-level suggestion's "why" now counts the DISTINCT fields that moved, so it reads "…(+9 related fields)" instead of the absurd "…(+240 related fields)" that appeared when the same handful of fields rolled up once per subsection. The suggestion's stored evidence is likewise collapsed to one row per field.
- Living-doc suggestion rationales are readable now. A refresh suggestion's "why" is a short human phrase describing the primary change (e.g. "Days Since Last Touch (mean) rose from 65.05 to 71.39") instead of a raw dump of dotted field paths and long decimals, and immaterial sub-5% field wobbles are dropped from the evidence at the source so they never crowd out the change that actually moved.
- The data assistant no longer wrongly claims Salesforce workspaces have no email. The
record_type/interaction_typeschema guidance the NL→SQL assistant reads (viadata://schema) stated that email interactions exist only for HubSpot workspaces and that a Salesforce-CRM workspace has zerorecord_type='email'rows. That was incorrect — Salesforce logs emails as Task/Event activities that the pipeline normalizes torecord_type='email', so they are in the corpus and queryable. The assistant now answers email questions on Salesforce workspaces instead of refusing; coverage is still source-dependent (email comes from HubSpot, Salesforce, Salesloft, and Pylon, while Gong is calls-only), so it still checks the livesample_valuesbefore filtering. - The CRM Mappings change-impact preview no longer shows a misleading "0 rows would re-normalize" right after mappings are first generated. On the very first load of a workspace's CRM Mappings settings, the impact preview could race the initial auto-generation and compute over an empty mapping set — reporting 0 even though applying the mappings would re-normalize a large number of rows. The preview now waits for the generated mappings (across app instances) so the number always reflects what an apply would actually change.
- Auto-generated CRM stage mappings classify contracting and closing stages more accurately. A late-stage "Contract" (or legal / signature / procurement) stage is now mapped to Negotiation rather than Proof of Concept, which is reserved for actual technical trials/POCs. This improves how deals in those stages roll up into your funnel view.
Added
- `data.query` time windows without hand-written SQL. A new optional
last_n_daysparameter (operation + MCPdatatool) injects the correct event-time window for you — passlast_n_days: 90instead of authoringTIMESTAMP_SUB/ interval syntax. Thedata://schemaresource now carries asql_dialectblock documenting the warehouse's SQL rules, and failed queries return targeted rewrite hints (e.g. Postgres-styleINTERVAL '90 days'→INTERVAL 90 DAY) instead of raw parser errors. - Chat answers now carry suggested follow-up questions. The answer envelope on every surface (run READ, terminal SSE frame, MCP
chat_status) includesanswer.follow_ups— up to 4 complete, runnable next questions the agent attached to its final answer block. Render them as one-click chips or feed one straight back intochat.start/respond; the field is always present and empty when the agent supplied none. - Chat now shows how it understood your question. Before answering, a fast enrichment phase interprets your plain question against your business context and standing reports, and the run READ carries an optional
intent_brief(original_query,interpreted_intent,expanded_question,hints[]) so clients can render a collapsed "How we understood your question" card. Advisory only — your original question is always answered verbatim, and a skipped or failed enrichment changes nothing. - Suggested queries come from your Living GTM Docs.
GET /console/suggested-queriesserves deep, specific example questions derived from the living-doc catalog, tagged per persona (executive/developer) and surface (search/chat) with the source doc attached. - Charts in answers: four new shapes.
chart_specblocks acceptfunnel,radar,treemap, andgaugealongside bar / line / area / pie / scatter — same{ x, y, series? }encoding; gauge renders the first row (label, value, optional max).
Changed
- Master Chat commits living docs only.
agents.delegatenow requires the newagents:delegatescope (split fromworkflows:delegate, which still gatesagents.start/agents.fork_blueprint). Master no longer resolves the specialist-start tools; lasting saves go throughoutputs.write_doc. Discover valid living-doc slugs viaGET /outputs/output://list. - Blended fast search now enriches the web query with your workspace context. In
POST /searchwithmode: "blended", the web + news citation pass no longer runs your raw ask verbatim: a cached, tenant-aware rewrite folds in your company and industry context first, so ambiguous asks resolve to _your_ market instead of the public internet's dominant sense of the words. When an ask reads as purely a question about your own workspace data, the external pass is skipped with the newexternal_omitted: "not_relevant"instead of returning off-topic links (this never blocks a healthy fan-out — uncertainty runs the search). The result'sexternal.external_query_usedshows the query the web pass actually ran. Also in this release:agents.startwithasync: falseno longer reports a slow specialist as an error — if the run is still working after the ~60s bounded wait, the call returns success withstatus: "running"andstill_running: true(the async contract), and a run that genuinely failed mid-wait surfaces the honestrun_failedcode; anddata.queryresults now return BigQuery date/timestamp cells as plain ISO strings instead of{"value": ...}wrapper objects, on every protocol. - Product feedback Slack pings always attempt delivery (no production-only env gate); still require
INTERNAL_SLACK_WEBHOOK_URL. - Cost accounting is server-internal; runs no longer loop on a finished answer. The Chat run read (
GET /chats/:id/runs/:run_id,chat://<id>/runs/<run_id>, theagentstool'schat_status) now reportsusageas tokens +turns_usedonly —estimated_cost_centsis gone, as iscost_cap_centson chat summaries and theconversation:///agent_run://reads. Themax_cost_centsconfig knob is removed fromchat.start, chat preferences, and routine configs (sending it now rejects as an unknown field); every new chat gets the platform-managed limit automatically. Thecost_cap_exceededpause is renamedusage_limit_exceededwith a unitless payload: reply{ action: "continue" | "stop" }— on continue the platform extends the limit itself. Separately, imperative first messages no longer force tool use past the first model request, fixing runs that re-presented the same answer repeatedly before settling.
Fixed
- Champion & EB Voice Digest no longer reports a phantom "coverage decline." The first time a workspace runs the digest after the champion-scoring fix (which excludes the neutral-default
0.75stakeholders), champion/EB counts step down versus the older, un-gated version. The digest now labels this as a one-time scoring correction and treats the new counts as the baseline — instead of inventing a real-world decline (seasonality, pipeline gaps, "cleaned test data") or recommending an investigation. - Pipeline Health Report now refreshes for workspaces with many pipeline stages. The report's stage-by-stage funnel (and the open-pipeline stage breakdown) were capped at 30 and 25 rows — a complete per-stage aggregate that must not be truncated, so a workspace whose Salesforce pipeline has more stages than the cap (including legacy or
#REF!stage labels) failed the whole run and the living document never updated. The caps are raised to the query engine's row limit, so every stage is included. - Living GTM Docs no longer stall on large workspaces. The ICP Signal Report, Win/Loss Report, Deal Qualification Report, and Pipeline Health Report each ran a few "cover the whole population" queries (e.g. per-industry, per-segment, per-competitor breakdowns) under a hand-picked row cap. On a workspace whose true breakdown exceeded that cap, the scheduled refresh aborted and the document froze at its last version. Those completeness queries now read the full set, so the docs keep refreshing no matter how broad the workspace's customer base, pipeline, or competitor field grows.
Added
- Agents are now a first-class primitive: list the platform agent roster and author your own workspace agents (a named prompt with a stable slug) via the new
GET/POST /agentsAPI family. Amdahl-shipped agents (starting withresearcher) appear alongside your own; platform agents are read-only. - Agents can now take outbound actions — with your permission: the new
POST /actions/invokeAPI (and the matching agent tool) fires one of two outbound actions:email_member(email current workspace members — external addresses are always rejected, sends are capped and idempotent) ornotion_sync(push a knowledge-base document into your connected Notion now). Actions are opt-in per run via anactions_allowedlist that defaults to empty, so an agent that was not explicitly granted an action cannot send anything — it proposes instead.GET /actions(theaction://listresource) shows each action's live availability and remaining send budget, and a failed action is never retried automatically. - Agents can now keep long-term memory honestly: the new
POST /memory/writeAPI (and the matching agent tool) commits durable facts to the workspace context store. Every agent-written entry is automatically taggedauto_detected, and entries a human wrote (manual) are protected — an agent that disagrees with one must add a new flagging entry rather than edit yours. - Agents can now ask a human: the new
ask_a_humanagent tool pauses a run to put one structured question to a person —multiple_choice(2-6 options, with a guaranteed "Other" write-in the platform injects) orfree_form. Only the asking run waits: a sub-agent's question is escalated to its caller's event stream as achild_needs_humanframe while sibling work keeps running, and the answer resumes the waiting run directly through the existing resume API as a{ answer, option_id? }tool result. Runs started withon_question: "auto"skip the ask and proceed on stated assumptions; headless / scheduled runs (on_question: "none") get an immediateno_human_in_looperror instead of a hang. - See exactly which tools a workspace agent can block: the operation catalog read takes a new
blockable_for=masterfilter (operation://list?blockable_for=master, RESTGET /operations?blockable_for=master) that returns just the tools an agent actually runs with — its granted kit, minus reads and the protected loop tools a blocklist can never remove — instead of the full operation catalog, so a tool-blocklist picker only offers tools it can meaningfully block. - Chat reads now tell you what triggered a run: every chat + run read (
GET /chats,GET /chats/:id,GET /chats/:id/runs/:run_id, and theagentsMCP tool's chat reads) now carries atrigger(chat·agent·routine), theroutine({ id, name }) behind a scheduled fire, and theagent_refa run is pinned to — so a routine fire and an agent run are distinguishable from a plain chat without inferring it from the session title. - Agents can now delegate: the new
POST /agents/delegateAPI (and the matching agent tool) dispatches a sub-agent — a roster agent likeresearcher, one of your own workspace agents, or a one-off inline prompt — to investigate a single goal and report back with an answer, evidence, and suggestions. Passtask, plus optionaldesired_outputandcontext, to shape what comes back. Delegation is async (the call returns the child run id immediately; progress streams into the caller's session), sub-agents run with investigation-only permissions (they never write documents, memory, or send anything), and delegation depth is capped at one level. - Multi-agent Routines:
config.agentspicks who a routine employs —"all"(plain Master, unrestricted delegation), one ref (the fired turn runs AS that agent, the pin), or several refs (the Master may delegate ONLY to that roster; other agents and inline prompts are refused byagents.delegatewithagent_not_allowed). Every roster member re-resolves at fire time. The legacy singleagentpin still works; passing both rejects. Theoutput_slug/full_regendoc-target knobs left the writable config — the agent picks its own document slug viaoutputs.write_doc. - Workspace agent controls:
slugis now optional onPOST /agents(derived from the name and uniquified when omitted), and every workspace agent accepts atool_blocklist— operation ids subtracted from the agent's kit at run time (all kit tools are on by default; runner-inline tools likefinishcannot be blocked). - Fast search — data back in one call.
POST /searchis the synchronous fast lane beside Chat: it turns your ask into SQL over your interaction warehouse and runs it, blocking for a single answer (a ~15s hard ceiling) with no agent run. Passmode: "blended"to also fan out a quick web + news citation pass (internalis the default, warehouse only),limit/external_limitto size the result, andsynthesize: truefor a one-paragraph headline. It returns the rows, the SQL that ran, and any citations. Reach for it when you want data now (a dashboard cell, a slash command); reach for Chat when the ask needs a multi-step investigation. Every failure mode is a typed field on the result — an unsupported ask, zero rows, a timed-out source — never a raw error, and a partial answer always beats a 500. Also on MCP as thesearchtool (one action,run). Needs onlydata:read; the blended leg usesexternal_search:executeand degrades to internal-only without it. See the new Search guide. - The
searchMCP tool. Connected MCP agents (Claude, Cursor, ChatGPT) can now ask Amdahl anything end-to-end:startopens or continues a named Session and runs one Master agent turn server-side, returning handles immediately;statuspolls the run (with an optionalwait_mslong-poll up to 30s);respondanswers a paused human question;cancelstops a run. A deep run is never crammed into one tool call — start, check in, respond. Session reads are also MCP resources (session://list,session://<id>,session://<id>/runs/<run_id>). - The
agentsMCP tool: the workspace agent library (list / get / create / update / delete named, reusable prompts — Amdahl library agents are locked) plus Routines in the same tool (create_routinewith a prompt + cron for a standing scheduled ask,update_routine,delete_routine, andrun_routine_now, which fires immediately and returns the same handles a Search start does). Roster and routine reads are also MCP resources (agent://,routine://). - Existing MCP API keys were backfilled with the new scopes automatically — no rotation needed. Read scopes (
conversations:read,agents:read,routines:read) landed on every bundle; write scopes (conversations:write,workflows:write,agents:write,routines:write) on customer-agent keys and up. - Routines are live: a Routine is a cron that fires a Search — each occurrence opens a fresh Session named
"{name} — {date}"and runs one Master agent turn, headless (ask_a_humanfails fast withno_human_in_loop; a routine never parks on a person). Full CRUD plus an off-cadence trigger:POST /routines,GET /routines[/:id],PATCH /routines/:id,DELETE /routines/:id, andPOST /routines/:id/run-now(returns the same watch handles a Search START does). See the new Routines guide. - Verified document commits: the Master agent now refreshes living documents through
outputs.write_doc— never a raw knowledge-base upload. Runoutputs.run_bound_queriesfirst: it executes the vetted query pack behind an analytical document and records the cells; every digit the commit states is verified against exactly those cells. Auto-promotion to the current version is refused without a verified pack (the version lands proposed for human review), and a failed theme read during the run blocks the commit entirely so a degraded read can never overwrite the last good version. - Answers are content blocks: a Chat answer carries
answer_textplus an orderedcontent_blocks[]list (text,callout,citation,table,chart_spec,metric). Data-backed blocks include both the declaredqueryand the snapshotdataon every surface, so clients can render the snapshot instantly, re-run the query live, or diff the two. Agents emit blocks mid-run via the newanswer.presenttool; blocks stream ascontent_blockSSE events and reconcile into the terminal answer in order. - Living-doc suggestion threads + a runnable data-query tree. Two review surfaces for living documents. (1) Suggestion discussion threads — every data-driven suggestion now carries its own comment thread: read it at
GET /knowledge-base/:id/suggestions/:suggestion_id/comments(also theknowledge_base://<id>/suggestions/<suggestion_id>/commentsresource), reply withPOST …/comments(body, optionalthread_root_id), and resolve/reopen withPOST …/comments/:comment_id/resolve. Threads are keyed on the suggestion (not a version block), grouped root + replies, newest-first. (2) Per-node data-query tree — open any node of a living doc and see its numbers live:POST /knowledge-base/:id/nodes/:node_key/runexecutes that node's bound queries through the same interactions engine (tenant- and access-scoped to you) and returns fresh rows per query;POST …/nodes/:node_key/askanswers a question grounded only in that node's bound queries and their latest captured signatures. Reads needknowledge_base:read; the thread writes needversioning:write(editor). Like the existing suggestion accept/dismiss/ask + review-thread writes, the thread writes and the node compute are REST + in-app copilot only (off the MCP coarse tool); the thread read rides theknowledge_base://resource scheme, and an MCP client runs a node's query by reading its SQL fromknowledge_base://<id>/treeand calling thedatatool directly.
Changed
- The agent lane is now Chat (renamed from Search): the multi-turn agentic surface that backs the console's sidebar chat is called Chat everywhere, and the name Search now belongs to the synchronous fast-search endpoint (
POST /search, shipped in this release — see its own changelog entry). What moved:POST /search→POST /chat;GET /search/sessions[...]→GET /chats[...](rename viaPATCH /chats/:id); the start body'ssession_id/session_name→chat_id/name, and responses returnchat_id(list responses returnchats, detail responseschat); op idssearch.*→chat.*(chat.start,chat.list,chat.get,chat.get_run,chat.rename); thesession://MCP resource scheme →chat://. Over MCP the standalonesearchtool is retired — its four lifecycle actions live on theagentstool asstart_chat/chat_status/respond/cancel_chatwith the same check-in contract (START always returns handles; pollchat_statusor readchat://<id>/runs/<run_id>;wait_mslong-polls up to 30s). Routines fire Chats;run-nowresponses returnchat_idand routine reads carrylast_chat_id. See the Chat guide (formerly the Search guide). - Chat `depth` is now a real investigation tier.
quick,standard, anddeepno longer differ in name only — each sets the model, the turn budget, and the toolset for you.quickruns a lean, fast toolset for a focused lookup;standardis the everyday full toolset;deepuses the most capable model with a larger turn budget, forces market search + the divergence map on, and instructs the run to decompose the ask and self-verify every figure before answering. You never pass a model — the tier picks it. - Two Chat config knobs were removed.
evidenceis gone — citations and the query behind every data-backed answer block are now attached unconditionally, so there was nothing to toggle.as_ofis gone from the Chat / routine run config — an interactive Chat always reads your current data; historical "as we understood it on date X" reads belong to workflow backtests (thedata.querytime-travel primitive is unchanged). Sending either field now returns a clearinvalid_argumenterror rather than being silently ignored. - Tool activity now reads in plain language. Each tool a run calls streams a short, human-readable label ("Searching your customer conversations") and a plain-language result summary ("30 results") on its lifecycle events, so the console's tool cards are legible instead of raw operation ids and JSON.
data.querynow refuses a silently-truncated result: when a query's population exceeds the effective row limit, the call returns a structuredresult_truncatederror (with guidance to aggregate in SQL or bridge on an id list) instead of an arbitrary slice. Passallow_truncation: truefor a deliberate bounded top-N (ORDER BY ... LIMIT n).
Removed
- The
blueprintsMCP tool. Connected MCP agents no longer author, run, schedule, or fork workflow (blueprint) recipes over MCP, and the workflow resource reads (agent_blueprint://,blueprint_run://,blueprint_schedule://,blueprint_backtest://,blueprint_output://,step_kind://,trigger_kind://,prompt://) are gone with it. Workflows themselves are unaffected: the console Workflows surface and the full REST lifecycle (authoring, validation, one-shot runs, schedules, backtests, output promotion) keep working unchanged, and scheduled runs keep firing. From MCP, use the newagentstool's Routines for recurring work and the newsearchtool for one-shot deep work. MCP API keys need no rotation; theblueprints:executescope on existing keys is simply inert on the MCP surface. - The
pagesMCP tool. Connected MCP agents (Claude, Cursor, ChatGPT) no longer author, list, or delete workspace Pages, and thepage://+page_template://MCP resources are gone with it. Pages themselves are unaffected: the console Pages surface and the full REST lifecycle (/api/platform/v1/pages— create, validate, render, update, archive, delete, templates, embed links) keep working unchanged. MCP API keys need no rotation; anypages:*scopes on an existing key are simply inert on the MCP surface. See the updated "Pages over MCP" guide for the migration path.
Fixed
- Account Tier Intelligence living document: corrected two accuracy issues — the triage-tier account count now matches the ranked account list, and accounts with no identified champion no longer display a default champion score.
- API-key connections no longer land Disconnected the moment you connect: connecting any API-key connector (Fireflies, Fathom, Granola, Grain, Aircall, Salesloft, Pipedrive, Pylon) created the connection row before its key was stored, and a legacy database trigger that derived status from credential existence marked the row
disconnectedat birth — so the console showed Disconnected instantly and the sync scheduler (which only picks upconnectedsources) never ingested anything. The trigger is retired, andPOST /connections(api_key) plusPOST /connections/:id/reconnectnow order the flow aspending→ store the key →connected, so a connection only ever readsconnectedonce its credential actually exists. Previously affected connections can be restored in place with a normal Reconnect. - Living GTM documents: the Persona Voice Deep-Dive and Champion & EB Voice Digest no longer count or quote accounts that carry only the pipeline's neutral default champion score as champions (matching the fix already shipped for Account Tier Intelligence), so champion totals are no longer over-stated on data-rich workspaces. Separately, the Voice-of-Customer Cluster Report no longer fails on workspaces with more than 200 conversation-theme clusters — every theme's win/loss outcome is now computed.
- Scheduled/headless living-document runs are reliable again: the runner now always reaches the production data endpoint, fixing intermittent runs that halted with a connection error and left some living documents (persona voice, competitive divergence, weekly digest, content calendar) stale.
Added
- Your pipeline funnel is now backtestable. Pass
as_ofto adata.queryover thedeal_funnelsurface to see the per-pipeline/stage funnel as it stood on any past date — deals-ever-entered, progression rate, and median dwell reconstruct from your stage history — the same time-travel that already backeddeals,deal_qualification,clusters, andpositioning_claims. Stages with no history before that date report null rather than a fabricated zero, so a trend never invents numbers. - New API + MCP reads for a living document's data spine — and endpoints to review its suggestions. Every living document now exposes its underlying data over the API and MCP: its signal trends per bound query (
GET /knowledge-base/:id/signals,knowledge_base://<id>/signals), its node → query tree (.../tree), and its pending data-driven suggestions (.../suggestions). Three companion endpoints let a reviewer act on a suggestion — accept it (apply the edit and produce the next version), dismiss it, or ask a grounded follow-up about why it fired — via REST and the in-app copilot. Accepting never overwrites a document whose current version you authored by hand. (Suggestions populate as the automatic refresh engine rolls out; the read + review surfaces are available now.) - Living documents now propose data-driven edits when their numbers move. When a recurring living document refreshes and a bound metric shifts materially since the last run, Amdahl now surfaces a single, evidence-backed suggestion on the affected section — each anchored to the exact number that changed, with the before/after trend behind it. Suggestions are proposed for your review (never applied automatically), so you decide what lands.
Changed
- The Account Tier Intelligence, ICP Signal Report, and Deal Qualification Report living documents now end with a concrete Recommended Actions section — named accounts/verticals paired with a specific next step (or a short prioritized play list) — instead of stopping at diagnosis. Actions stay grounded in the underlying data: they cite each item's own signals (momentum, binding constraint, tier, undercovered dimension) and never introduce invented numbers, deadlines, or targets.
Fixed
- Living-doc suggestions no longer surface a data-availability transition as a GTM change. When a document's only prior generation is a structural backfill seed (e.g. a
deal_qualificationas-of snapshot whose coverage lens is empty), the refresh engine now stays silent instead of narrating spurious "0→N" / "added N records" / "expanded coverage from 1 to 3" movements. A genuine value move against a real prior still emits as before.
Added
- Canonical sales-pipeline metrics. New
pipeline://metrics,pipeline://funnel, andpipeline://qualification-healthreads (MCP resources + RESTGET /pipeline/*) give one consistent, per-pipeline definition of win rate, sales cycle, funnel, and qualification — with the qualification / "stage 0" band ALWAYS shown but NEVER counted in a rate or cycle.data.querycan also read the newdeal_winloss(per-pipeline win rate + sales cycle) anddeal_funnel(per-stage) tables directly. - Canonical funnel + per-pipeline win rate in the living docs. The Pipeline Health Report and GTM Health Report now read the shared
deal_funnel/deal_winlossviews, so the funnel (with the qualification / "stage 0" band shown but never counted in pipeline value), the qualified-pipeline value excluding stage 0, and the per-pipeline win rate + sales cycle (with qualification/stage-0 dwell excluded and reported separately) are all defined one consistent way instead of re-derived per doc.
Changed
- Connect more than one instance of the same integration to a workspace. The API-key connectors (Fathom, Granola, Fireflies, Pipedrive, Pylon, Salesloft, Grain, Aircall) and the workspace OAuth connectors (HubSpot, Salesforce, Gong, Slack, Notion) now support several simultaneous connections per workspace instead of being capped at one. Gmail and Outlook (per-member) also work reliably when several members each connect their own account.
Fixed
- Fixed a "The server ran into a problem" error when connecting an API-key integration where one already existed. The connect now returns a clear, actionable message instead of a generic 500 and never leaves a half-connected source behind.
Added
- Workflow runs now record a per-step audit trail. Reading a run (
blueprint_run://<id>,GET /blueprint-runs/:id, orcontext.review_kit) returns astep_statesentry for every step the workflow executed — its kind, the tool it called, whether it succeeded, how long it took, and a short summary of what came back. When a step degrades rather than fails outright — a theme search that could not reach the warehouse, a query that returned no rows — the reason is now on the run record, instead of having to be inferred from the document the run produced. Step summaries are counts and status codes only; they never include the underlying rows or document text. Populated for workflows that run on the deterministic step executor. - New Actions connector category for outbound integrations — connectors that push your Amdahl data OUT to another tool rather than pulling data in. Notion Knowledge Sync is the first, and the connector catalog + the
connectionsreads now group it underactions(filter withcategory=actions).
Changed
- Living documents now read like reports, not compliance forms. Every recurring living document — GTM health, pipeline health, voice-of-customer, win/loss, ICP, account tier, and the rest — now follows a shared writing standard: it opens with the finding rather than the methodology, states a data caveat once where it applies instead of on every line, reserves bold for the few things you must not miss, and never leaks internal field names or CRM stage codes into the prose. Documents you generate from your own uploads get the same treatment. Every number stays verified against your data exactly as before.
- Any signed-in user can now create a workspace. Workspace creation no longer rejects personal / free-mailbox email addresses.
- You can now set your company domain while setting up a workspace. When the domain matches your own work email, it is verified automatically.
- New discoverability control: choose whether teammates with a verified email on your company domain can find and join your workspace. It is off by default.
- Pick your role during setup — Executive or Developer — and the console tailors where you land and what it shows first. You can change it any time in settings.
Fixed
- A failed theme search now reports an error instead of looking like "no themes found." When the theme index is temporarily unavailable,
data.cluster_searchpreviously returned an empty result that was indistinguishable from a workspace that genuinely has no matching themes. It now surfaces as a tool error, so your agent can retry or tell you something went wrong. A workspace with no themes yet, or a search that simply matches nothing, still returns a normal empty result. - Scheduled living documents no longer publish a report when their theme search fails. If a workflow cannot read your conversation themes, the run now stops and the previously published version of the document stays in place, instead of quietly replacing it with a version missing its qualitative analysis.
- Scheduled living documents no longer crowd each other out — or hold up a run you started yourself. Recurring documents used to all fire at the same moment and compete for the same processing capacity, which could cause a report to come back rate-limited and, occasionally, to arrive with nothing in it. Scheduled documents now drain through their own dedicated lane at a steady pace, so they generate reliably no matter how many are on your calendar. A document you run on demand starts right away instead of waiting behind the day's scheduled batch.
Added
data_access_rule://read resource (data_access_rule://list,data_access_rule://<id>; RESTGET /data-access-rules). Look up the verified data-access rules the platform maintains for the warehouse — field substitutions, surface-grain caveats, and aggregation patterns — so an agent queries your data correctly. Filter with?surface=,?kind=, or?signature=.- New guided walkthrough prompt (
system/guide_me) for MCP clients. Ask "what can this do?" or "guide me" and Amdahl runs a friendly, jargon-free, numbered-menu interview — it offers plain-English goals (meeting prep, customer voice, deal lookup, proof, research, dashboards, recurring reports), you pick a number, and it does the work and answers in plain language. Built for non-technical users who would rather pick from a menu than know which tool to call. - Default GTM living documents now schedule themselves for your workspace. Once a workspace has synced go-to-market data, Amdahl automatically schedules a starter bundle of recurring living documents — GTM health, pipeline health, voice-of-customer, win/loss, and a weekly priority briefing, plus deal-qualification, account-tier, and ICP reports once a CRM is connected — so useful reports start arriving with no setup. Workspaces that have already configured their own workflows are left untouched.
Changed
- Win / Loss and GTM Health Report accuracy on high-volume workspaces. The won-vs-lost MEDDPICC differentiators, the score-calibration curve, and the per-competitor win rates in these living documents are now computed over your _entire_ closed-deal history in a single pass, instead of a recent sample. Workspaces that close thousands of deals per window — where these figures were previously computed on a truncated slice — now get complete, unbiased numbers.
- New queryable fields on the `data` tool.
deal_qualificationnow exposesrealized_is_won/realized_is_closed(a company's realized deal outcome), andcompetitor_mentionsnow exposescompetitor_class(rival vs. own-product / benchmark / model-vendor) plus the account's realized deal outcome. You can now compute score calibration and per-rival win rates in a single query, without bridging across tables, and the competitor win-rate read is backtestable withas_of. data.querynow accepts Common Table Expressions (WITH … AS (…)) and subqueries (inFROMandWHERE). Previously these were rejected; your workspace's tenant scoping is applied to every part of the query, so nested reads stay scoped automatically.- MCP-connected assistants now translate Amdahl's internal terms into plain business language when talking to you — "conversations" instead of "interactions", "topics" instead of "clusters", "a workflow" instead of "a blueprint" — and no longer surface SQL, tool names, or field names unless you ask how something works. The server instructions carry a built-in glossary so this holds across every client.
data.querynow also accepts set-operations (UNION/UNION ALL/INTERSECT/EXCEPT). Previously these were rejected; each branch of the query is scoped to your workspace independently and the row limit applies to the combined result, so you can union or intersect results in a single query.data.explorewith sample values is now much faster on a cold call: the per-column distinct-value lookups collapse into a single scan of your data instead of one query per column (the firstexploreof a session no longer stalls for tens of seconds).data.cluster_detailnow accepts a durablelineage_id(surfaced on everydata.cluster_searchresult) as an alternative tocluster_id. A theme'scluster_idchanges each time topics are recomputed, so a saved handle could go stale; thelineage_idis stable across recomputes and is resolved server-side to the theme's current id, so a drill-in handle you saved earlier keeps working.- The GTM Health Report picks its own title again. A fixed title was briefly pinned onto the report. It was redundant — the report is already barred from describing itself, in its title, any heading, or the body, as covering a lookback period, a rolling window, a quarter, or "recent" deals — so the pin is gone and the report titles itself. The scope guarantee is unchanged.
Fixed
- The grounding discipline behind every living GTM doc now encodes the deal-outcome surface rules directly: deal counts / revenue / win rate come from your CRM deal records, conversation-corpus outcome rates are labeled as such and never conflated with them, won+lost can exceed closed (an account can be both), numbers are stated exactly (no rounding), currency is formatted, and quotes stay in blockquotes. Applies across all ~18 living-doc generators, not just VoC and ICP.
- The GTM Diagnostic's "where deals are lost" stage-leak analysis now works. The query behind it was silently rejected before it ever ran, so on every workspace the report saw an empty result and fell back to reporting cycle-time only (reading the blank as "no stage history"). It now computes the stage each lost deal died in, so the diagnostic can point at where in the funnel deals actually leak.
agents.run_blueprintnow resolves a tenant blueprint by itsidentity.slug, not only by its UUID. Running a blueprint you authored by its slug used to returnnot_found; it now resolves the same way every other blueprint surface does.- Win / Loss Report no longer claims a 90-day window it never applied. The
win-loss-reportworkflow declared alookback_daysinput but none of its queries filtered on it, so a run headlined _all-time_ closed-deal totals under a "90-Day Close Period" title — the figures were real, the scope was not. The report now states its true basis, all closed deals to date, opens with a line giving the closed won/lost counts and noting that the MEDDPICC differentiators and per-rival win rates are computed over the closed-and-scored subset, and is barred from describing itself as covering any rolling window. Thelookback_daysinput is removed: only thedealscut has a close date at all — the won/lost outcome reaches the qualification and competitor surfaces as a flag with no date — so windowing part of the report would have put a 90-day headline next to all-time win rates. Passing the old input is ignored rather than rejected, so existing schedules keep running. The document still refreshes monthly; its version history is the time series. - Champion & EB Voice Digest now actually covers the window it reports. The workflow declared a
lookback_daysinput but none of its queries filtered on it, so the digest quoted stakeholders from your entire conversation history under a heading that implied a recent period. It now genuinely reads the last 60 days of champion and economic-buyer utterances, opens with a coverage line stating how many utterances and accounts fall inside the window out of your all-time totals, and says so plainly when nobody spoke in the window instead of padding the digest. The objection themes come from the theme index, which has no time filter, so they remain all-time and are now labeled as such. Thelookback_daysinput is removed — the window is fixed at 60 days; passing the old input is ignored rather than rejected, so existing schedules keep running. Fork the workflow to change the window. - GTM Health Report no longer implies a lookback period it never applied. The workflow declared a
lookback_daysinput (default 180) that no query filtered or weighted by. The report now states its true basis — all of your conversation and CRM data to date — and is barred from describing itself, or any finding, as covering a rolling window, a quarter, or "recent" deals. Its cuts genuinely cannot be windowed: the stage-transition analysis would silently mis-identify where deals die, the account funnel view would lose every open deal, and the score-calibration and competitor win-rate cuts carry a won/lost flag with no close date. Windowing only the rest would have put a windowed headline beside all-time win rates. Thelookback_daysinput is removed; passing it is ignored rather than rejected. The report still refreshes on a cadence, and its version history is the time series. - The Champion & EB Voice Digest now carries its window in the title. The digest reads the last 60 days, but because it edits the prior version in place rather than rewriting it, a refresh could leave the old heading standing and mention the window only in a subtitle beneath it. The 60-day window is now written into the document's own title on every refresh.
- The GTM Health Report's Home-card score reads reliably regardless of how the report is laid out. The score block that powers the card is now located wherever it appears in the document rather than only at the very top, so a report that opens with its heading can never blank the card.
Security
- MCP OAuth: the scopes an OAuth-connected app receives are now clamped to the standard customer-agent capability set at authorization time. A requested scope beyond that ceiling (for example a wildcard
*:*) is dropped rather than granted, so an OAuth token can never carry more access than the OAuth flow is meant to issue. Apps that request the advertised scopes are unaffected.
Added
- Pages catalog:
LineChartacceptsyMin+yMaxto pin the y-axis range, so a tight series can fit its scale to the data (no forced zero baseline) and show its variance. - Pages catalog: new
Tabscomponent (labelled panels the viewer switches between — each tab carries an inline catalog subtree) andCarouselcomponent (rotate sibling charts one slide at a time), plusQuoteList.expandable+QuoteList.detailFieldsfor a per-quote read-in-place detail dialog with richer attribution. - Workspace README — a one-read orientation map of your workspace. Read
workspace_readme://currentover MCP (orGET /workspace/readmeover REST) to get the workspace's pages, knowledge documents, agent workflows, and callable operation namespaces as name-plus-summary rows, together with a rendered markdown digest. Assembled live on every read, so it is never stale; sections your key lacks scope for are omitted with a reason instead of failing the call. - Salesloft connector. Connect a Salesloft workspace with an API key to sync emails, calls, conversations, transcripts, and meetings into your workspace.
- Every Pages chart now accepts a
heightpreset (sm|md|lg) and an orderedcolorstoken list (chart-1..chart-5,positive,negative,neutral) so agents can size charts and paint series/slices deliberately — tokens resolve to theme variables, so white or raw hex are unrepresentable.RadarChartadditionally acceptsmaxto pin the radial scale (e.g.max: 5for a /5 rubric fills the polygon), andFunnelChartno longer overruns its card border.
Changed
- Living documents generated from an uploaded doc (
knowledge_base.make_living) now author a deterministic, grounded refresh workflow: each regenerated version computes its figures directly from your data and verifies every number, so a living doc no longer states a figure that isn't backed by the underlying data. - Backtest-generated knowledge-base versions are now stamped with the as-of date they reflect. A cumulative backtest sweep of a living document produces a properly dated version history, and each slice builds on the prior one (
knowledge_base.uploaddefaultsversion_as_ofto the run's backtest cut; an explicit value still wins, and live runs are unchanged). - Competitive Divergence Map workflow (v2) now tracks divergence over time. Every run captures a structured divergence snapshot — counts by kind (internal-only / market-only / magnitude / polarity), themes marked new / persisting / resolved vs the prior run, and render health — in three places: the workflow run's outputs (
blueprint_run://<id>), a machine-readable header on the living doc (so the knowledge-base version history is a parseable series), and an accumulating "Divergence over time" chart on an auto-upserted dashboard page. The run also factors in more data: per-rival market research (bounded by the newmax_rivalsinput), your positioning docs from the knowledge base, and the Industry sweep corpus — and the category market read is force-refreshed so a scheduled run never records a cached brief as a fresh snapshot. - The Account Tier Intelligence and ICP Signal Report living documents are now generated by the deterministic step-executor — every figure (close-likelihood scores, engagement momentum, champion strength, win rates by vertical) is computed from your data in SQL/transform cells and number-verified, so the document can no longer state a value that isn't backed by the underlying data. Account tiering (Expand / Nurture / Watch / Triage) is computed deterministically from the real signals rather than narrated by the model.
- Fixed a blueprint-runner bug where a workflow's
transformstep could not read a workflow input via the canonical$inputs.Xreference, which could cause a defaulted threshold to be treated as unset. - The Deal Qualification Report living document is now generated by the deterministic step-executor: every figure (coverage-band distribution, per-dimension MEDDPICC/SPICED coverage percentages, binding constraints) is a verified data cell, the thinnest-coverage deals are named with real company names (bridged from the interactions surface), and dimension coverage is reported as a clear percentage of scored companies rather than an ambiguous count.
- The Win / Loss Report living document is now generated by the deterministic step-executor. The two cross-surface analyses it previously asked the model to compute by hand — the won-vs-lost MEDDPICC coverage spread and the per-competitor win rate — are now pre-computed as verified transform cells, so the report states real, grounded win/loss differentiators and win rates (each gated to ≥8 accounts for reliability) instead of derived figures.
- The Pipeline Health Report living document is now generated by the deterministic step-executor: the open-pipeline total is a verified
totalsquery (rather than the model summing stage rows in its head), the at-risk / long-shot deals are named with real company names, and every stage figure is a grounded cell the number gate verifies. - The Win / Loss Report now handles a thin closed-won sample honestly: when there are too few scored closed deals to compute reliable MEDDPICC win/loss differentiators, the report says so and focuses on the qualitative loss themes instead of reporting shaky percentages, and it no longer derives win-rate or share percentages from the raw deal counts.
- Living-doc workflows now share a canonical data-surfaces map (where each GTM data surface lives, its grain, purpose, scale, and join key) plus the query patterns that keep a computed figure correct under the query row cap — so a generated or hand-authored workflow aggregates in SQL over the full population instead of silently truncating a per-row query.
- Added a truncation guard to the deterministic workflow executor: a
data.querywhose result exceeds itsLIMIT(detected by a peek) is flagged, so a workflow can no longer state a real-but-wrong number computed over a silently-truncated sample (deliberate top-N queries opt in viaallow_truncation). - The Account Tier, Win / Loss, Deal Qualification, and Pipeline Health living documents now compute their cross-surface figures over the FULL population instead of a silently-truncated sample: metrics that need every company (win/loss MEDDPICC differentiators, account tiering, named thin/at-risk deals) use an id-list bridge that aggregates in SQL over the exact set, so — for example — a tenant's win/loss report now reflects all of its won accounts rather than an arbitrary slice.
- The living-doc query truncation guard is now enforced (not just observed): a workflow step whose query returns more rows than its limit fails the run instead of silently producing a document over a truncated sample. When a generated living doc hits this, the self-tuning generator now recognizes it as a truncation failure and revises the recipe to aggregate over the full data (via SQL aggregation or a bounded lookup) — so a generated doc self-heals to correct figures rather than shipping partial ones.
Fixed
- Turning a knowledge base document into a living document now reliably produces a runnable refresh workflow. The generated recipe is constrained to the tools and step types the living-doc engine actually executes, and a malformed draft returns a clear validation error instead of failing the request.
- Living GTM docs (Voice-of-Customer, ICP Signal) now report won/closed counts, win rate, and revenue from your CRM deal records, cleanly separated from the conversation-corpus outcome view — no more conflicting "won accounts" figures between sections. Currency is dollar-formatted, quoted customer figures are no longer mis-flagged, and the reports no longer state industry/segment splits that aren't in the data.
- Living GTM docs now state every figure exactly as computed — the narrative no longer rounds or approximates a number (e.g. "~5,600" for 5,904), so the prose always matches the underlying data.
Fixed
- The divergence map now renders on the `search` action too. A prior fix pinned the synthesis model path for the enrichment briefs but missed the plain
searchaction, sosearchcould still route its internal-vs-market synthesis to a slower back-end that timed out and returned an empty divergence map. All external-search synthesis paths now use the same fast, dedicated model, so the fused internal-vs-market view renders consistently onsearchas well as the company/person/topic enrichments.
Added
knowledge_base.promoteandknowledge_base.dismiss. Approve a proposed living-doc version (promote it to the current, cited version) or dismiss it, from the console or REST.- Living-doc subscribers. Read and replace the members/roles notified when a document has a new version to review (
GET .../knowledge-base/:id/subscribers,PATCH .../knowledge_base/documents/:id/subscribers). - Saved views. Personal named filter presets for the Living Knowledge list:
knowledge_base.list_saved_views/create_saved_view/update_saved_view/delete_saved_view. - Documents now carry an owner and an audience role, and surface whether a proposed version is awaiting review.
- Two new `data.query` surfaces: `competitor_mentions` and `deal_score_weights`.
competitor_mentions(one row per company + competitor) lets you build an internal win-rate-by-competitor read straight from your conversation corpus — no external search — bridging to deal outcomes oncompany_id.deal_score_weightsexposes the learned deal-score model: which signals predict close-likelihood for your tenant (a factor→weight map) and how well it performs (holdout vs baseline AUC). Both are discoverable viadata://schemaand honor data-scope —competitor_mentionsis company-scoped like the deal-grain surfaces, anddeal_score_weightsis a business-level admin/GTM read. POST /agent-blueprints/:id/activate— activate or deactivate a workspace workflow (body{ enabled?: boolean }, defaulttrue; creates the activation row when enabling one that was never activated). Powers the Living Knowledge "Activate" affordance on a living document whose producing workflow is off.- Living Knowledge documents now carry
source_run_status(the producing workflow's most-recent run status) andworkflow_active(whether that workflow is enabled), so the list can badge "Last run failed" and filter "Runs failing". - Two new Page components: `CopyButton` and `Dialog`.
CopyButtoncopies a value (a literal or a bound cell, e.g. a recommended message) straight to the clipboard on click — the one-click "grab and paste" affordance.Dialogadds a trigger button that opens a modal rendering its child components, so a page can tuck supporting detail (quotes, a table, a chart) behind a click-in instead of showing everything at once. Both are client-side only — they run in the browser and invoke no server operation. - Industry: dropped the market-themes and divergence-map reads (
industry://themes/industry://divergence). Added a raw Industry Signals read (industry.list_signals/industry_signal://list, RESTGET /industry/signals) over the collected corpus - the source documents the sweep landed, filterable by domain, source, angle, sweep, and collected/published date, with a free-text search over title and body, and paginated.industry.refreshand theindustry_run://run-history reads are unchanged. - Sync a single knowledge-base document to Notion on demand, and open it in Notion. If your workspace has Notion Knowledge Sync connected, you can now push one document to Notion immediately — instead of waiting for the automatic sync or the hourly reconcile — and read its live mirror state: whether it has a Notion page yet, a direct open-in-Notion link, and whether the mirror is behind the current version. New endpoints
POST /notion-sync/documents/:document_group_id/syncandGET /notion-sync/documents/:document_group_id(plus thenotion_sync://documents/<document_group_id>read resource for MCP). - New `industry.get_signal` operation — read one collected market-signal document in full. The industry signals list now returns a short preview (snippet) per document to keep the feed fast; fetch a single document's complete text on demand by id via
industry.get_signal(industry_signal://<id>on MCP,GET /industry/signals/:idon REST), so the detail view shows the whole article instead of a truncated blurb. - Two new `data.query` surfaces: `deal_stage_history` and `ref_deal_stages`. Query your CRM deal stage-transition history and the stage dimension directly, so you can answer where deals actually die (the stage a lost deal sat in right before it closed) and how long they spend in each stage — questions the current-stage
dealssnapshot can't.deal_stage_historyis one row per stage change (deal_id, the raw CRMdeal_stageid,changed_at);ref_deal_stagesmaps each raw stage id to its human label, funnel position, and won/lost/open class. Readdata://schemafor the columns and example queries. - New starter workflow: Market Intelligence Digest. A weekly living document built from your Industry signals — the market-signal documents the platform sweeps from the web, news, and SEC filings. It surveys the collected feed, drills into the documents most relevant to your business (and named competitors) in full, and writes an executive read + what's-moving + competitor-watch digest that auto-promotes each run. Fork it from the starter workflows and enable its weekly schedule (Monday 08:00), or run it on demand. See the new Industry signals guide (Using Amdahl, under "See what it found") for the full workflow, the API, and how agents read the feed.
- Remove a single document from Notion — and keep it removed. If your workspace has Notion Knowledge Sync connected, you can now remove one knowledge-base document from Notion without turning off the whole sync: it trashes that document's Notion page and mutes the document so no automatic sync, hourly reconcile, or backfill re-mirrors it (the fix for a _living_ document that would otherwise come back on its next update). Re-syncing the document brings it back — Remove and Sync are a clean toggle. New endpoint
POST /notion-sync/documents/:document_group_id/unsync.
Changed
- Faster external search. Market/topic research (
search+enrich_topic) now spends less time ranking and synthesizing results: the relevance-ranking pass is capped to the strongest candidates per source, the source fan-out closes out sooner, and the summary step works from a tighter set of citations — trimming response time while keeping the same broad source coverage. - External search responds faster. The relevance-ranking step in market/topic research (
search+enrich_topic) now works from a leaner pool of top candidates per source, cutting the time spent scoring results. Source coverage is unchanged — every source still contributes — so you get the same breadth of citations, sooner. - External search responses now include per-stage timing. A
searchresult carries an optional_timingbreakdown (query enrichment, source fan-out, ranking, relevance gate, and synthesis — with synthesis further split into clustering and each summary pane). It's additive and safe to ignore, but lets you see exactly where a call spends its time when diagnosing latency. - Every Amdahl workflow now works goal-first and recursively. All shipped workflows — plus any you fork from them, and any generated from an uploaded document via "make it living" — now share one investigation method: anchor on the goal, tie claims to a measurable outcome vs a baseline, and treat a finding as a lead (drill into what causes it and how to act on it) rather than filling a fixed template. The discipline is centralized in one place, so it keeps improving across every workflow at once.
- GTM Health Report is now outcome-first and adaptive. The standing GTM health report was rebuilt to reverse-engineer the questions that matter for _your_ business and test each against a real outcome — win rate, cycle velocity, deal size — instead of leaning on conversation-theme summaries. It now computes the funnel and where it leaks by stage, the drivers that actually predict a win (stakeholder breadth, champion presence, and more), an internal competitive read, and message performance — all from your own calls, emails, and CRM, with named accounts and verbatim quotes behind every finding. Competitive position and messaging no longer come back "not computable" and there is no external dependency to fail on; every dimension is derived from your corpus.
- Living Knowledge provenance. A living document's
source_workflownow carries the producing workflow'snameandblueprint_id, so the "Fed by <workflow>" label can name and deep-link the workflow instead of showing a generic "an Amdahl workflow" label. - Living Knowledge ownership. Backfilled the accountable owner on documents created before the owner field existed (using the workflow's activating user, else the uploader), so the Owner field is populated rather than empty.
- GTM Health Report now reports stage velocity (median time-in-stage, won vs lost, and the stage where deals stall) and win rate _by competitor_ (grouped by rival type, with any win-rate inversion called out) — not just a rival mention list. It no longer emits "TBD" for time-in-stage; when a workspace lacks stage-transition history it says so and falls back to the overall won-vs-lost cycle spread.
- Pipeline Health Report now flags stalled stages using the median age of open deals per stage.
- GTM Health Report now reliably includes stage velocity (time-in-stage, won vs lost) and win rate by competitor — these are now required outputs of the diagnostic rather than optional, so they appear on every run where the data supports them (with an honest coverage-gate fallback when it doesn't).
- Living-doc workflows authored by the "make it living" generator are now data-field-aware: the generator knows the tenant's high-value CRM signals (champion / economic-buyer flags, close-likelihood + binding-constraint scores, per-stage dwell time, and competitor win-rate) and composes queries that use them — so a generated living doc produces the sharp cuts, not a generic theme summary.
- GTM Health Report now always includes a stage-velocity cut (median time-in-stage, won vs lost, and the stage where deals stall) and a win-rate-by-competitor table — computed as deterministic steps so they appear on every run, not just when the agent happens to author them.
- Living documents that are analytical diagnostics now fully regenerate their numbers each run instead of minimal-editing the prior version, so newly added analysis sections take effect immediately (a
full_regen_each_runblueprint setting). - Several Living GTM Doc reports now read high-value CRM signals they previously ignored, so they produce sharper analysis:
- Win/Loss Report — won-deal MEDDPICC differentiators (which qualification dimensions separate wins from losses) and win rate by named competitor.
- Deal Qualification Report — the most systematically undercovered MEDDPICC dimensions, computed directly rather than guessed.
- Account Tier Intelligence — tiers now weigh stakeholder quality (champion / economic-buyer presence, champion score) and 30-day engagement momentum, not just raw activity volume.
- Weekly GTM Digest — flags stalled pipeline stages by median deal age.
- Competitive Divergence Map — names the actual recurring rivals (not just a competition-present count).
- ICP Signal Report — leads with the firmographics of _won_ accounts (who actually buys) and flags ICP drift vs the overall funnel.
- Persona Voice Deep-Dive — the role mix now surfaces champion / economic-buyer counts per role level.
- The natural-language SQL helper (
data.ask) learned two new analysis plays: per-stage dwell/velocity (from the interactions temporal-stage surface) and win-rate-by-competitor (bridging competitor mentions to deal outcomes). - Living documents now self-heal. If a scheduled or on-demand living-doc run is cut short before it writes its document (e.g. a transient rate limit on a large workspace), the platform now automatically re-runs it (bounded) instead of silently leaving the document stale — and a retried run is nudged to work more economically so it finishes within budget.
- External search is faster and its divergence map now always renders. The result-ranking step switched from an LLM scoring pass to embedding similarity, cutting a large chunk of latency off every
search/enrich_topiccall. That freed budget goes to the fusion synthesis: the internal-vs-market divergence map (the tenant-only signal a plain web search can't produce) previously timed out and came back empty on most calls — it now completes and ships populated. - Content Calendar links straight to your drafts. The Content Calendar workflow's page now opens the full drafts from your Knowledge Base — the drafts living document stays the single source of truth, and a "Read the drafts" panel on the page links out to it, so the schedule and the full copy never drift apart. The
knowledge_base.uploadAPI now also returns aconsole_urldeep link to the uploaded document, so an automation can point back at exactly what it just saved. - External search's divergence map now reliably renders (and the call stays safely under the timeout). Follow-up to the previous rerank change: the internal-vs-market divergence map — the tenant-only signal a plain web search can't produce — was still coming back empty because the synthesis panes generated more text than they had time to finish. Their output budgets were trimmed so each pane completes, and the overall time budget was tightened so a call can no longer run up against the 60-second ceiling and return nothing.
- Living GTM documents now tie themes to outcomes, not just volume. Five of the standing GTM living documents got sharper by grounding what they surface in won-vs-lost deal outcomes instead of raw frequency:
- VoC Cluster Report now leads with the themes that show up in _won_ deals and ranks by win-correlation (with a loss-correlated risk section), instead of ranking by how often a theme comes up.
- GTM Proof & ROI Library now leads with competitive-displacement wins — accounts you won _and_ where a rival was named — as the strongest reusable proof.
- Champion & EB Voice Digest now includes a dedicated objections & pushback section mined from what senior stakeholders actually push back on.
- Cross-Persona Divergence Brief now calls out which persona layers, when engaged, predict wins — so multi-voice content leads with the framing that closes.
- GTM Health Report now quantifies the top jobs-to-be-done as coverage on won vs lost deals ("pain quantified on 41% of won deals vs 6% of lost"), instead of a free-text ranking. Each keeps the same honesty discipline — a rate is only stated once a bucket has enough closed deals; below that it reports raw counts and marks the signal thin.
- Read your Content Calendar drafts right on the page. The Content Calendar workflow's page now bakes each piece's full draft into the calendar itself: click any day to open a dialog and read that day's pieces in full (multiple per day supported), with a link to open the canonical, versioned copy in your Knowledge Base. The separate "Read the drafts" button is gone — the content reads in place. The
CalendarandBoardpage components (authored via thepagestool) gained two optional props for this:contentField(the row column holding a piece's full markdown body) anddocUrlField(a per-row deep link to the living doc); when set, each item becomes clickable and opens the read-in-place dialog. - The GTM Priority Briefing now ranks by grounded confidence, not a guess. When the weekly briefing scores each priority by impact × confidence × urgency ÷ effort, the _confidence_ term is now set from real signals: each source document's own stated confidence (the GTM Health Report's score header, per-finding high/medium/low, and "thin/directional/not measured" honesty markers), whether two or more documents independently corroborate the insight, and the knowledge-base retrieval match score. A loud-but-thin signal can no longer outrank a quiet-but-corroborated one, and every priority shows why its confidence was set the way it was.
Fixed
- Issues inbox filtering.
GET /console/issues?category=...now accepts the app-internal issue categories (e.g.proposed_kb_version) alongside the pipeline quality-event categories, so filtering the inbox by one returns results instead of a400 invalid_category. This unblocks the Living Knowledge "needs review" count. - Living Knowledge workflow activation now reflects execution reality. The
workflow_activefield on documents and thePOST /agent-blueprints/:id/activatetoggle read/writeblueprint_schedules.enabled— the cron scheduler's source of truth — instead of theworkspace_workflowscuration badge. Previously the flag under-reported which workflows actually run, and "Activate" moved a badge without starting or stopping any scheduled run. - The Content Calendar page's "Read the drafts" link now points at your real Knowledge Base doc. The deep link back to the full drafts is attached on the same path the scheduled workflow actually runs through, so the page links to your living document instead of a guessed URL. The
knowledge_basetool (both the API operation and the MCP coarse tool) now returns aconsole_urlfor the uploaded document, so any automation can link back to what it just saved. - External search's fused brief (market summary, customer voice, and the internal-vs-market divergence map) now actually renders. The synthesis step was opening streaming connections that never completed in the production environment, so every
search/enrich_topiccall fell back to an empty brief. The synthesis calls were switched to a non-streaming request that completes reliably, so the market summary, your customer-voice pane, and the divergence map come back populated. - External search's divergence map renders more reliably. Follow-up to the non-streaming synthesis fix: the fusion panes now complete, but under concurrent load one pane occasionally ran a hair past its time budget, so the full brief (market summary + customer voice + divergence map) came back complete only intermittently. Their time budgets were given a small amount of headroom and their prompts trimmed slightly so each pane starts and finishes sooner — making the complete brief, including the internal-vs-market divergence map, render consistently.
- External search's divergence map now renders reliably on every call. The final fix in the fusion-synthesis chain: the internal-vs-market divergence map (the tenant-only signal a plain web search can't produce) was completing only intermittently because all three synthesis panes generated at once and competed for throughput. The panes now run two-at-a-time — the divergence map gets a dedicated slot while the two summary panes share the other — so the divergence map finishes and comes back populated consistently.
- Documents, pages, and artifacts produced by a workflow run are now attributed to the person who triggered the run. Previously anything a headless/scheduled workflow authored was credited to the workspace owner (the identity the runner acts under), so a report someone else kicked off showed the owner as its author. Outputs now carry the actual triggering user as their creator, while owner attribution is preserved when a run has no human trigger.
- The Content Calendar workflow can now be forked into your workspace. Forking it previously failed validation because its tool allowlist named an internal coarse-tool action (
context.entries_list) instead of the canonical operation id (context_entry.list); the entry is corrected, soPOST /agent-blueprints/fork(and theblueprintsfork action) now succeed. Behaviour of existing runs is unchanged. A new starter-forkability test pins every shipped starter's tool allowlist against the operation registry so no starter can ship unforkable again. - The market-intelligence divergence map now renders reliably on every search. External search and the company/person/topic enrichment briefs synthesize their internal-vs-market "divergence map" — the view that fuses your own CRM and call corpus against the public web — on a fixed, dedicated model path. Previously that synthesis could be silently routed to a slower back-end that couldn't answer within the call's time budget, so the divergence map came back empty. It now runs on the fast path every time, so the fused view (the part generic web search can't produce) shows up consistently.
- The "open the living doc" link on a Knowledge Base upload response now resolves to the document instead of 404ing. The
console_urlattached to aknowledge_base.uploadresult (on both the operation and the MCP coarse tool) was built from the document'sdocument_group_id, but the workspace KB detail route resolves a document id — so the link opened "Document not found". It now uses the freshly-written version's document id, so a living-doc → page workflow (e.g. the Content Calendar) links straight to the current version of its full drafts. - Five more workflow starters are now forkable. Following the content-calendar fix, the plan-and-draft-window, GTM diagnostic, positioning scorecard, Substack thought-leader newsletter, and content-calendar-routine starters each listed a tool in their allowlist that was not a registered operation (e.g.
web_search,data.explore, and internal run-lifecycle controls), so forking them into your workspace failed the allowlist validator. Their allowlists now name only canonical operations, and a guard test (which was previously a no-op) now actually checks every starter, so each one can be forked. - "Your workflows" now lists every workflow that's actually running. A scheduled workflow only appeared under "Your workflows" if it had been switched on through the in-app activation toggle. Workflows put on a schedule another way — for example a recurring living-document report — kept running on their cron but were shown under "Library" as if they were still just an un-adopted template. The Workflows page now decides what's active directly from the schedules that fire, so anything running for your workspace shows up under "Your workflows".
Added
- `data.query` on the theme clusters surface now supports `as_of` backtests. A SQL read like
SELECT label, velocity FROM clusters …with anas_ofdate now resolves the cluster generation that was live on that date (via the warehouseclusters_as_oftable-function) instead of being refused — so a blueprint that reads themes with raw SQL backtests cleanly. The append-onlytheme_lineagehistory surface still requiresas_ofreads to go throughdata.cluster_search/data.cluster_detail. - Living GTM Docs — 18 workflows + matching pages. Eighteen new Amdahl-shipped workflows generate versioned, human-promoted living documents in your knowledge base on a cadence — VoC cluster report, champion/EB voice digest, pipeline health, deal qualification, win/loss, ICP signal, account tiering, positioning scorecard, persona voice deep-dives, cross-persona divergence, competitive divergence map, ecosystem map, topic research briefs, GACCS content briefs, AEO health, the weekly GTM digest, the GTM proof & ROI library, and the meta GTM priority briefing. Each ships with a live page template (browse via
page_template://list/ author withpages.create_from_template) so the same intelligence is available as a live console dashboard, not just a narrative. - Pages can read clusters and the knowledge base. Page declared queries now support
cluster_searchandkb_searchsources (in addition tosqlandartifacts), so a page can surface conversation themes and reference-library matches live.
Fixed
- External search now draws on every source it advertises, and returns faster. Market/topic research (
search+enrich_topic) now fans out across the full set of registered sources — including the developer and review platforms (GitHub, Hacker News, dev.to, Medium, Stack Overflow, G2, Capterra, TrustRadius, Product Hunt) alongside web, Substack, and YouTube — so those citations actually show up in results. The same change removes a bottleneck in the relevance step that was roughly doubling response time on these calls. - Theme-based Living GTM Docs now backtest correctly. The Voice-of-Customer, Weekly GTM Digest, and GTM Priority Briefing living documents read your conversation themes through the point-in-time theme index, so running one as an as-of backtest reconstructs the themes that were live on that date instead of failing on the theme step. Their scheduled (live) runs are unchanged.
Added
- New industry intelligence surface. A daily sweep gathers what the wider market is discussing (web, news, and SEC filings) and maps it against what your own customers raise on calls. Read it via the new
industry.get_divergence(the four-axis market-vs-customer divergence map) andindustry.list_themes(the clustered market themes) operations, or trigger a fresh sweep on demand withindustry.refresh. - Substack and YouTube are now searched in external research. Founder newsletters, talks, and demos are strong go-to-market signal, so
external_search(thesearchandenrich_topicactions) now covers Substack and YouTube alongside web, news, LinkedIn, and Crunchbase. - More platforms in topic and market search. Topic and market searches now also pull from Medium, Hacker News, dev.to, GitHub, Stack Overflow, G2, Capterra, TrustRadius, and Product Hunt (alongside the existing Substack and YouTube sources), giving broader coverage of practitioner writing, developer discussion, and buyer-intent review sites. New platforms are added through a single registry, so the source set will keep growing without changes to how you search.
agent_blueprint.listentries now includeforked_from_amdahl— true when a workflow was forked from an Amdahl starter (via "Use template"), so you can tell an Amdahl-derived workflow from one you built from scratch.- Trigger and watch an industry sweep over MCP. The new
industrytool'srefreshaction kicks off a "What's Going On In The Industry" sweep on demand instead of waiting for the daily run, and returns arun_id. Poll the newindustry_run://list+industry_run://<id>reads to watch the run to completion and read its per-stage step log; readindustry://themes/industry://divergencefor the clustered market themes and the market-vs-customer divergence map.
Changed
- External search is faster and no longer times out on long calls. Broad web and news results now run on the faster search backend (when configured), removing the slow source that previously gated every fan-out. The per-call
deadline_msis also clamped so the full pipeline (search + synthesis) stays under the tool-call ceiling — a largedeadline_msno longer causes the whole call to time out and return nothing; slow sources just surface as incomplete instead. - External search synthesis is faster and less likely to time out. The market/internal synthesis step now works over a tighter, top-ranked set of sources, so the brief comes back sooner — particularly on broad topics where the wider source mix previously pushed synthesis past its budget.
Removed
- Reddit and X/Twitter removed from external research. Neither has a serviceable data provider, so
external_searchno longer searches them; theenrich_personaction no longer attempts to resolve an X handle.
Fixed
- External search no longer drops sources under load. When many searches run at once (e.g. competitor/market research alongside the industry sweep), requests are now smoothly rate-limited and automatically retried instead of failing, so a burst costs a little extra latency rather than missing results.
- Embedded Pages now load on any site. A live Page embed (the
<iframe>snippet from a Page's Embed button) fetches its data cross-origin, which was being blocked unless the embed was hosted on an Amdahl domain. The public page-embed API now permits cross-origin requests from any origin, so a pasted embed renders wherever you put it. - Fixed the embed signing-secret screen. Creating a signing secret showed a blank value and the secrets list showed an
undefinedprefix, due to a mismatch between the API responses and the console. Both now return the correct shape, so the one-time secret and the prefix display correctly. - `data.query` MCP calls now capture the question that triggered them. Previously, a
data query(raw SQL) call could silently drop the verbatim user message even when your client supplied it, because that action logs through its own path rather than the shared one. Theresources,social,pages,notifications,industry, andblueprintstools now also accept the same optional context so a client populating it gets full coverage across every tool, not justdata/context/knowledge_base/external_search.
Added
- Run and schedule Workflows directly from MCP. The
blueprintscoarse tool gains arunaction (a one-shot headless run) alongsidecreate_schedulefor recurring cron runs, gated by the newblueprints:executescope on the customer-agent key bundle. A workspace MCP key can now launch and schedule its own Workflows without any broader delegation grant. - Author a Page from a template over MCP. The
pagescoarse tool gains acreate_from_templateaction that materializes a vetted, Amdahl-shipped page template bytemplate_slug; browse the available templates via thepage_template://listresource first. - Read drafted content pieces back over MCP. A new
content_piece://read scheme (content_piece://list+content_piece://<id>, gated byartifacts:read) surfaces the content pieces a content-calendar Workflow drafted, so an agent can review a run's output without leaving MCP. agent_blueprint.listnow returnsis_activeandsourceon every entry, so you can tell which workflows are active in a workspace and whether each is an Amdahl-managed default (source: "amdahl") or your own (source: "custom").- New workspaces start with the GTM Health Report workflow active by default.
Fixed
- External search now returns social results again. Reddit, X/Twitter, LinkedIn, and Crunchbase sources are served via web search, so
external_search(searchand theenrich_*actions) surfaces social signal instead of coming back empty.
Added
Actionpage component + thepages.invoke_actionoperation, so a Page can now do something, not just display: an Action button invokes a write/compute operation (e.g.artifacts.update,agents.run_blueprint) as the clicking viewer. A button can only invoke an op the page declares, and the viewer must independently hold that op's scope — so a Page can never become a way to call an operation a viewer couldn't call directly.pages.create_from_templateoperation. Author a new workspace page from a vetted Amdahl page template (a catalog-only spec) by slug — so the in-app copilot can stand up a page (e.g. a content-calendar page) and, paired with a workflow schedule, set it to auto-update on a recurring cadence.{ $row: 'field' }Action input. A page Action button can now act on the row the viewer clicked — its declared inputs resolve from the clicked row, so per-row buttons (move this piece, archive this row) no longer need a hard-coded id.- Page
Actionbuttons can now invoke `workflow` operations (e.g.agents.run_blueprint), not justwrite/computeops — so a Page button can kick off a workflow run, not only edit a row. The rule is now simply: a button may invoke any op that _does_ something (write/compute/workflow); areadstays the page's display channel. The same boundary holds — the op must be declared on the page and the clicking viewer must independently hold its scope (so a "Run workflow" button needsworkflows:delegate). - Page
Actionbuttons can also target external providers via anexternal:<id>address — a pluggable seam for actions that reach out (post to a channel, push to a CMS, fire a webhook), gated by the provider's required scope. A referenceexternal:echoprovider ships so the path is live; real providers are added behind it. - The Content Calendar page template gained two buttons — "New piece" (creates a placeholder
content_piece) and "Run routine" (fires the new `content-calendar-routine` workflow on demand) — plus that workflow itself, a scheduled (weekly, off by default) routine that plans and drafts the upcoming window. Enable its schedule to keep the calendar full hands-off.
Fixed
- Page embed signing secrets now load and manage correctly from Settings -> Embedding in the console. Listing, creating, and revoking a workspace's page-embed signing secrets works again (the surface previously failed with a "business_id is required" error).
Added
CalendarandBoardpage components, plus anartifactsquery source, so a Page can render your workspace content (e.g. content pieces) as a live calendar or kanban board over the Postgres artifact store — not just the warehousesqlsource.- A ready-made Content Calendar page template (
content-calendar) that pairs the two views over yourcontent_pieceartifacts. Fetch it viapages.get_templateand author a Page from it. - Positioning experiments. Read how your positioning claims perform — what buyers echo back versus what actually wins deals — across three new surfaces. Query the data directly:
data.querynow addresses two positioning surfaces,positioning_claims(your claim inventory: text, label, category, status) andpositioning(per-claim performance: leading buyer-reception lift and matched-control lagging win-rate lift), discoverable viadata://schema. Run the new Positioning Scorecard workflow for the labeled "what we say vs what lands vs what wins" read, including the inversion where the story you lean on most is the one that loses. Or author a live Positioning Experiments page from the new template (claim inventory, confidence-tier breakdown, and a reception-vs-outcome chart). The win-rate signal is a matched-control estimate (a correlation, not proof) — the surfaces are built to read claim _ordering_ over absolute numbers and flag a low-coverage signal as directional.
Changed
- Positioning experiments now separate measured messaging from candidate positioning. Every claim carries a
source:mined_llmclaims are pulled from what your team actually says on calls (their lift is real messaging performance), whileseedclaims are hand-authored candidate positioning the engine matches by meaning (their lift is a "would this land" test, not a performance review). The Positioning Scorecard workflow and the Positioning Experiments page now keep the two in clearly separate sections and never present a seeded claim's numbers as measured performance, and thedata.queryschema guidance teaches the distinction so the assistant interprets it correctly in chat too.
Added
- Connection data summaries. Each connected source now exposes a glanceable, per-connector snapshot of its own synced data - counts and totals (deals and pipeline value, meetings, messages, tickets, pages), the most recent items, and a weekly activity sparkline - at
GET /connections/:id/summaryand theconnection://<id>/summaryMCP resource. Computed live from your synced data, it shows what is unique to each connector type.
Changed
- Relationship intelligence now lives directly on the data surface. Questions like "who is the champion or economic buyer on this deal", "what did a specific stakeholder actually say", "who do we keep losing to", and "which accounts are evaluating a competitor" are answered with
dataquery / explore over the stakeholder columns folded onto the interactions surface (is_champion,is_economic_buyer,champion_score,role_level) plus thedeal_qualificationchampion / economic-buyer / competition dimensions — taught in the schema explorer and the GTM routing guidance so the agent reaches for them automatically. - The
datatool's schema catalog now advertises the nativerecord_typecolumn (call/email/meeting) for filtering interactions by category. Category-filter prompts and examples use it instead of the olderinteraction_typeremap, so questions like "emails about pricing" or "calls per company" produce clearer, more accurate SQL.
Removed
- Removed the
include_knowledge_graphoption ondatacluster search. It read from a decommissioned dataset and added no signal beyond the stakeholder columns now folded onto the interactions surface, so the tool no longer accepts it.
Fixed
datatool:cluster_detailnow accepts thesource_idreturned bycluster_searchverbatim. Previously thecluster_idargument only accepted a number, so drilling into a theme failed with "Cluster not found" because theme ids are opaque strings — pass thesource_idstraight through and the drill-in works.
Added
- A public changelog at docs.amdahl.ai/changelog - a searchable, branded timeline of what's shipped, refreshed automatically with each release.
- Notion Knowledge Sync — one-way mirror your Amdahl knowledge base into a Notion database in your own workspace. Connect Notion over OAuth, pick a parent page, and every promoted document syncs into Notion automatically, with an hourly reconcile that backfills and self-heals drift. Configure / monitor it over REST or read its status + activity ledger over MCP (
notion_sync://). - The assistant can now draw on more of your data directly for richer answers: the deal stage as it stood at the time of each conversation, theme insight summaries and headline hooks, theme lifecycle and lineage over time, and additional per-company deal-qualification detail.
- The assistant now answers deal-stage and pipeline-funnel questions at the deal level with human-readable stage names ("Discovery", "Closed Won", …), win/loss outcome, and true funnel ordering — instead of raw internal CRM stage codes — so per-stage pipeline value, win rate, and funnel breakdowns come back clean and correctly ordered.
- The assistant can now name themes directly when tracking how they evolve over time (a stable theme name now travels with each theme's history), and deal-stage / pipeline-funnel questions now return human-readable stage names for Pipedrive-based workspaces too — so theme-trend and funnel answers read cleanly across more CRMs.
Changed
knowledge_base.upload: a document'sdescriptionis now its stable purpose ("what is this document for"). When you append a new version to an existing living document, the purpose is carried forward automatically and anydescriptionyou pass is ignored — record per-version "what changed" notes inversion_changesinstead.- Internal cleanup: retired the legacy V1 cluster-evidence channels behind external research. The customer-voice / divergence-map pane now sources cluster evidence entirely from the V2 pipeline — no customer-visible change, since these channels already returned empty once the V1 pipeline was decommissioned.
Fixed
- The GTM health report now reads correct, human-readable deal stages from your CRM (e.g. "Discovery", "Research", "Closed Won") instead of raw internal stage codes, and no longer splits a single account across multiple stage rows.
- Asking the assistant about themes, deal stages, or deal qualification is now materially more reliable. It reads the correct underlying fields (so theme-lifecycle and stage questions no longer fail on some workspaces), keeps the "how complete is this deal's qualification" and "how likely is it to close" scores distinct, and stops surfacing internal placeholder columns that don't apply to your data.
- Fixed "Make it living" on a Knowledge Base document failing with a schema-validation error. The synthesized refresh workflow now always declares a valid output, so turning a static doc into a self-refreshing living doc succeeds reliably.
- Fixed
external_search(search / enrich_company / enrich_topic) dropping its web results and competitive divergence map. The web source's underlying search call was rejected by the model API; it now succeeds, so the divergence map and web summary come back reliably. - Knowledge base search and other embedding-backed features now reliably use the current embedding model. Previously, newly uploaded documents could fail to become searchable, and semantic search could silently fall back to keyword-only matching.
- Restored the internal-evidence keyword aggregates in external research (the customer-voice / divergence-map pane) after the V1 pipeline decommission — they now read the live V2 unified view instead of the dropped V1 dataset.
Added
- Blueprints - shareable, forkable recipes an agent reads and walks step by step. Author, validate, and fork via the
blueprintstool family and REST; seven starter blueprints ship out of the box. - Connections - a unified connector catalog and connect / disconnect surface spanning CRM, calls, comms, docs, support, and social (X / LinkedIn). Poll connection health and sync-run history over
connection://reads and REST. - External search fuses your CRM + call corpus with live market sources into a single brief (the divergence map). Four actions:
search,enrich_company,enrich_person,enrich_topic, with an opt-in SSE event stream. - Notifications - agents can email workspace members (member-only, rate-capped, idempotent) via the
notificationstool andPOST /notifications/email-member, and read the send ledger. - Pages - workspace-authored, data-backed UIs composed from a catalog of components (tables, charts, stats, signal maps) bound to your own queries. Author + validate via the
pagestool family and REST, publish, and embed.
Changed
- The
performance.*operations were renamed tosocial.*(clean break, no aliases) for social account tracking and engagement reads.
Removed
- Retired the customer-facing artifacts REST API (
/artifacts*) and its public read endpoint. Pages and Blueprints are the structured-output primitives now; artifact webhook events and data models are unchanged.
Added
- Generic Agent Runner. New
agents.*tool family:agents.start,agents.status,agents.resume,agents.cancel,agents.list,agents.profiles_list. - Server-Sent Events stream for agent progress at
GET /api/platform/v1/agents/:session_id/stream. Emitsstateandprogressframes,:pingkeepalives every 15 seconds, auto-closes on terminal status. - Three built-in agent profiles:
content_writer(end-to-end content creation, 25-turn budget, emitscontent_pieceartifacts),researcher(rigorous research with citations, 20-turn budget, emitsresearch_reportartifacts),copilot(general-purpose assistant, 15-turn budget, optional artifact output). - Three pause types on
awaiting_inputsessions:approval,question,continue_or_finish. Each carries a JSON Schema thatagents.resumevalidates against. - Consumer docs tree at
/docs/consumer/with auto-generated API reference covering every tool, scope, webhook event, and data model.
Changed
- Every agent tool call is now written to
platform_audit_logwith the caller's identity and correlation id, matching the audit trail direct REST and MCP calls already produced.
Added
platform_audit_logtable. Every tool invocation across REST, MCP, and agent sessions is recorded with actor id, business id, tool id, inputs hash, status, duration, and correlation id.audit_log.querytool for reading the audit trail. Scoped by business; platform admins can query across businesses.- Response-side redaction layer. Any field classified as sensitive is replaced with
[MASKED]and paired with a<field>_masked: truesentinel in the response body.
Changed
- Error responses now include
X-Correlation-Idon every status code, not just 5xx, so failed requests can be traced end to end.
Added
- Webhook extensions.
POST /api/platform/v1/webhooks/:id/testfires a singletest.pingdelivery without affecting production aggregates.GET /api/platform/v1/webhooks/:id/deliveriesreturns the delivery history with status filtering and pagination. is_testcolumn onplatform_webhook_deliveriesto separate test fires from production traffic.X-Webhook-Test: trueheader on test deliveries so receivers can branch on synthetic traffic.
Changed
- Webhook delivery retry policy codified at 3 attempts with 1s and 10s backoff intervals; after the third attempt the failure is recorded and
failure_countis incremented on the webhook.
Added
scope_modecolumn on API keys and OAuth clients, with per-tool scope enforcement. Tools whose declared scope is not in the caller's effective scope set are silently dropped from the runtime tool list.- Scope reference page at
/docs/consumer/api-reference/scopes.md, auto-generated from the tool registry.
Changed
- Role defaults tightened. New keys default to the minimum scope required for the role; existing keys retain their prior effective scope via the grandfather rule.
Added
- Artifact unification. Bidirectional triggers between
platform_artifactsand the legacywriting_samples,customer_posts,customer_testimonials, andstyle_collectionstables. Reads from either side return identical data; writes propagate automatically. platform_artifact_versionsappend-only version audit trail. Every write toplatform_artifactsinserts a corresponding version row.artifacts.gettool now supportsinclude_versions: trueto return the version history inline.
Changed
- Artifact writes must supply
expected_versionfor concurrent-edit safety. Mismatches return409 version_conflictwith the current version in the details block.
Added
- Platform API v1 initial release. Base URL
https://api.amdahl.com/api/platform/v1. Four-tool architecture:data.*,context.*,artifacts.*, plus the supporting surfaces for sessions, webhooks, scopes, and OpenAPI docs. - OpenAPI 3.1 spec at
/api/platform/v1/openapi.json, regenerated on every release. - Canonical error envelope with
code,message, and optionaldetails. - Global per-IP rate limit of 100 requests per minute on
/api/platform/v1/*. - OAuth 2.0 dynamic client registration per RFC 7591 at
POST /oauth/register, with its own tighter limit of 5 registrations per minute per IP.
Changed
- Nothing; this is the baseline.