Skip to main content

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 (was unit_price), and a statement drops currency. A webhook delivery names its event type, as the envelope it delivers does, field and filter (was event_type).

  • project.paused and project.resumed carry the project record — the Project a GET reads, less role — in place of { project_id, paused_at, pause_reason }. Read id where you read project_id.

  • A message has one shape: id, role, content. A channel transcript's document_id is id, and an inbound row carries the provider_message_id its message.received named; an Assistant message's text is content, on reads and in the send body (--content on the CLI); message.received carries content in place of text.

  • Assistant sessions and messages page by limit/offset and answer { data, total, limit, offset }: GET /v1/assistant/sessions takes offset in place of cursor, and a conversation's messages take limit and offset (its newest page is at total - limit). Page by offset where you passed next_cursor.

  • An Assistant grant names its identity by its address identifier (discord:dm:<user_id>), the grammar every channel surface uses, in place of discord:<user_id>.

  • Plan limits share one vocabulary. 403 plan_limit_reached names resource as projects, channels, triggers, storage or runs and carries the measured figure as used (was storage_gb / runs). GET /v1/users/me/usage limits is keyed the same way and gains runs (top-level runs and runs_included are gone); per project, active_channels is channels and storage is storage: { used, sampled_at }. GET /v1/users/me/billing reports storage: { used, limit, sampled_at } in place of storage_gb, storage_limit_gb and storage_sampled_at, and a statement's runs is { used, limit }.

  • A managed model's price is what credit pays, per token. GET /v1/models pricing is input_usd_per_token, output_usd_per_token and cached_input_usd_per_token, replacing the per-1K fields and currency. The managed price book, /usage cost_usd and the balance's debit carry the same figure, so a cost_usd quota now bounds what you pay. Read prices per token, and expect cost_usd on 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.received carries it in identifier, and opening a channel conversation and its ?identifier= filter take it, answering 400 to a bare phone number or user id. Send the identifier an address or a conversation reads back.

  • Who did it is a principal pair. created_by, resolved_by and acknowledged_by are <verb>_by_principal_type (user, api_key, agent, automation, approval, system) and <verb>_by_principal_id; a task history entry's principal_kind is principal_type. An API key reads as api_key with its own id.

  • One status vocabulary for work that runs. Orchestration runs, playbook executions and exports, trigger firings and formation operations finish completed, not succeeded, and wait as queued, not pending; a running generation reads running, not in_progress; an eval run is cancelled, not canceled. Every status list filter is a repeatable enum, and a value outside it is 400.

  • Every recorded error is { code, message, meta? }, never a string, and a validation refusal's meta is { problems: [{ code, path, message }] }.

  • Time-range filters are from/to: memory-store assertions take them in place of since, approvals expires_from/expires_to in place of expires_before.

  • A session turn's reply is a conversation turn's. Generate, an auto-generating message and tool outputs answer content and model at the top level, and message is the stored message record with its document_id. Read model where you read message.model.

  • Playbook nodes are named by id, not key, and every reference to one is node_id (bodies, ?node_id=, error meta). A stored template whose nodes still say key is refused on its next deploy.

  • Unset tags and metadata are absent from responses instead of null, and a tool_context value that is not a string is 400.

  • Event names a memory rule or event trigger matches are <resources>.<verb>: agents.generation.completed is generations.completed, conversations.message.generated is conversation_messages.generated, eval_run.* is eval_runs.*. Stored rules and triggers are rewritten.

  • Webhook payloads carry the record. A generation.* event's data is the generation as GET answers it; a usage.threshold_crossed event's is the threshold record plus observed_value, so threshold_id is id and window_key is gone.

  • Three operations are renamed where the name no longer said what they do. setAddressAction is setAddress: it sets the actor as well as the action. updateChannelRoute is replaceChannelRoute: the PATCH replaces the whole route. updateUserRoles is setUserRoles. 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.paths naming each one: auth keys, a credential-named header, or a url carrying 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 waiting array is blocked_by, in case reads and in the meta of PLAYBOOK_NODE_NOT_RUNNABLE and PLAYBOOK_NODE_NOT_CONFIRMABLE. A node parked on the task or orchestration run it started reads state: "waiting", not running, and names it in run. Read blocked_by where you read waiting, and treat waiting as in flight.
  • A stored error carries a registered code. UNKNOWN and INTERNAL are gone: an unexpected failure reads INTERNAL_ERROR, a JSON Logic expression that throws EXPRESSION_FAILED, an unreachable http or mcp tool TOOL_UNREACHABLE, and a formation resource records its own code. Running a node, exporting or resuming a case in a paused project answers 409 PROJECT_PAUSED. Match the new codes where you matched UNKNOWN or INTERNAL.