Changelog
Every breaking change, newest first. What counts as breaking, and why there is no notice period yet, is in Versioning. Additive changes are not listed.
The list starts on 2026-10-12; none has shipped since.
2026-10-17
-
A money field says USD in its name. A listing's pricing component carries
unit_price_usd(wasunit_price), and a statement dropscurrency. A webhook delivery names its eventtype, as the envelope it delivers does, field and filter (wasevent_type). -
project.pausedandproject.resumedcarry the project record — theProjectaGETreads, lessrole— in place of{ project_id, paused_at, pause_reason }. Readidwhere you readproject_id. -
A message has one shape:
id,role,content. A channel transcript'sdocument_idisid, and an inbound row carries theprovider_message_iditsmessage.receivednamed; an Assistant message'stextiscontent, on reads and in the send body (--contenton the CLI);message.receivedcarriescontentin place oftext. -
Assistant sessions and messages page by
limit/offsetand answer{ data, total, limit, offset }:GET /v1/assistant/sessionstakesoffsetin place ofcursor, and a conversation's messages takelimitandoffset(its newest page is attotal - limit). Page byoffsetwhere you passednext_cursor. -
An Assistant grant names its identity by its address identifier (
discord:dm:<user_id>), the grammar every channel surface uses, in place ofdiscord:<user_id>. -
Plan limits share one vocabulary.
403 plan_limit_reachednamesresourceasprojects,channels,triggers,storageorrunsand carries the measured figure asused(wasstorage_gb/runs).GET /v1/users/me/usagelimitsis keyed the same way and gainsruns(top-levelrunsandruns_includedare gone); per project,active_channelsischannelsand storage isstorage: { used, sampled_at }.GET /v1/users/me/billingreportsstorage: { used, limit, sampled_at }in place ofstorage_gb,storage_limit_gbandstorage_sampled_at, and a statement'srunsis{ used, limit }. -
A managed model's price is what credit pays, per token.
GET /v1/modelspricingisinput_usd_per_token,output_usd_per_tokenandcached_input_usd_per_token, replacing the per-1K fields andcurrency. The managed price book,/usagecost_usdand the balance's debit carry the same figure, so acost_usdquota now bounds what you pay. Read prices per token, and expectcost_usdon managed models to be the debited amount. -
Every channel identifier on the wire is the address identifier (
whatsapp:dm:<phone>,discord:dm:<user_id>,discord:thread:<guild_id>:<channel_id>).message.receivedcarries it inidentifier, and opening a channel conversation and its?identifier=filter take it, answering400to a bare phone number or user id. Send theidentifieran address or a conversation reads back. -
Who did it is a principal pair.
created_by,resolved_byandacknowledged_byare<verb>_by_principal_type(user,api_key,agent,automation,approval,system) and<verb>_by_principal_id; a task history entry'sprincipal_kindisprincipal_type. An API key reads asapi_keywith its own id. -
One status vocabulary for work that runs. Orchestration runs, playbook executions and exports, trigger firings and formation operations finish
completed, notsucceeded, and wait asqueued, notpending; a running generation readsrunning, notin_progress; an eval run iscancelled, notcanceled. Everystatuslist filter is a repeatable enum, and a value outside it is400. -
Every recorded
erroris{ code, message, meta? }, never a string, and a validation refusal'smetais{ problems: [{ code, path, message }] }. -
Time-range filters are
from/to: memory-store assertions take them in place ofsince, approvalsexpires_from/expires_toin place ofexpires_before. -
A session turn's reply is a conversation turn's. Generate, an auto-generating message and tool outputs answer
contentandmodelat the top level, andmessageis the stored message record with itsdocument_id. Readmodelwhere you readmessage.model. -
Playbook nodes are named by
id, notkey, and every reference to one isnode_id(bodies,?node_id=, errormeta). A stored template whose nodes still saykeyis refused on its next deploy. -
Unset
tagsandmetadataare absent from responses instead ofnull, and atool_contextvalue that is not a string is400. -
Event names a memory rule or event trigger matches are
<resources>.<verb>:agents.generation.completedisgenerations.completed,conversations.message.generatedisconversation_messages.generated,eval_run.*iseval_runs.*. Stored rules and triggers are rewritten. -
Webhook payloads carry the record. A
generation.*event'sdatais the generation asGETanswers it; ausage.threshold_crossedevent's is the threshold record plusobserved_value, sothreshold_idisidandwindow_keyis gone. -
Three operations are renamed where the name no longer said what they do.
setAddressActionissetAddress: it sets the actor as well as the action.updateChannelRouteisreplaceChannelRoute: thePATCHreplaces the whole route.updateUserRolesissetUserRoles. Routes and bodies are unchanged; call the SDK method and CLI command by the new name (naturali set-address,naturali replace-channel-route).
2026-10-16
- A tool credential written as a literal is refused with
400 LITERAL_CREDENTIAL,meta.pathsnaming each one:authkeys, a credential-named header, or aurlcarrying a password or credential query parameter, on a tool or an agent's inline tool binding. A stored literal reads back as{"no_echo": true}. Store the value as a secret and write{{secret:<id>}}instead. - A playbook case node's
waitingarray isblocked_by, in case reads and in themetaofPLAYBOOK_NODE_NOT_RUNNABLEandPLAYBOOK_NODE_NOT_CONFIRMABLE. A node parked on the task or orchestration run it started readsstate: "waiting", notrunning, and names it inrun. Readblocked_bywhere you readwaiting, and treatwaitingas in flight. - A stored
errorcarries a registered code.UNKNOWNandINTERNALare gone: an unexpected failure readsINTERNAL_ERROR, a JSON Logic expression that throwsEXPRESSION_FAILED, an unreachablehttpormcptoolTOOL_UNREACHABLE, and a formation resource records its own code. Running a node, exporting or resuming a case in a paused project answers409 PROJECT_PAUSED. Match the new codes where you matchedUNKNOWNorINTERNAL.