API Changelog
Every change to Symbol's public API surface, classified and dated. See the stability policy for what each classification means and the notice periods that apply. Subscribe via the Atom feed.
- Breakingpreview
Changing a log Type's rollover needs `confirm_rollover` from October 23, 2026
From October 23, 2026, `PUT /api/preview/types/{id}` refuses to change `rollover` on a log Type that already has dated buckets unless the body carries `confirm_rollover: true`. The refusal is 409 `rollover_requires_confirm` and names the bucket count, the current and new rollover, the bucket an entry appended now would land in under each, and that the period in progress will be split across two buckets (existing entries are never moved). Until then the gate applies only to a caller that sends `confirm_rollover` (any value; only `true` confirms), which is how the MCP `update_type` tool and the mobile app already call it. Backed by issue #2259.
- Announced
- October 9, 2026
- Breakingpreview
Deleting a capsule that other capsules mention needs `confirm_citations` from October 23, 2026
From October 23, 2026, `DELETE /api/preview/capsules/{id}` and `POST /api/preview/capsules/batch/delete` refuse to delete a capsule that live capsules mention unless the request confirms it: `?confirm_citations=true` on the single delete, `confirm_citations: true` in the batch body. The refusal is 409 `citations_confirmation_required`, carrying `citing_count` and up to five citing capsules you can read in `citing_sample`. Until then the gate applies only to a caller that sends `confirm_citations` (any value; only `true` confirms), which is how the web app and the MCP `delete_capsule` tool already call it. To migrate, read `POST /api/preview/capsules/batch/delete-impact` first, or handle the 409 and retry with the flag. Sending it when nothing cites the capsule is harmless. Backed by issue #2259.
- Announced
- October 9, 2026
- Breakingpreview
An empty body on a templated Type needs `authored_blank: true` from October 23, 2026
From October 23, 2026, `POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` refuse `content_md: ""` on a capsule whose Type has an `output_template` unless the request also sends `authored_blank: true`, answering 422 with `code` `empty_body_on_templated_type`. Send `null` (update) or omit `content_md` (create) to use the template, send body text, or send `authored_blank: true` to keep an empty authored body. Until then such a write is accepted as before and the response carries a `warnings` entry beginning `empty_body_on_templated_type:` that names the date. Sending `authored_blank: false` opts into the refusal now, which is how the MCP `create_capsule` and `update_capsule` tools already call it; the web app sends `authored_blank: true` for a deliberate blank. Writes to a Type without an output template are unchanged. Backed by issue #2257.
- Announced
- October 9, 2026
- Breakingpreview
Deleting a Type with side effects needs `confirm=true` from October 23, 2026
From October 23, 2026, `DELETE /api/preview/types/{id}` applies the confirm gate to every caller. A delete that affects anything beyond the Type's own capsules (capsules that mention the Type, other Types' reference fields constrained to it, its collections, or its usable share links) and does not send `confirm=true` is refused with 409 `impact_confirmation_required`, carrying a `code` and an `impact` block. On the same date, a delete refused because the Type holds other users' capsules answers 409 with `code` `other_users_capsules` instead of 400. Until then these apply only to callers that send `confirm=true` or `confirm=false`, which is how the web app and the MCP `delete_type` tool already call it. To migrate, read `GET /api/preview/types/{id}/delete-impact` first, or handle the 409 and retry with `confirm=true`, and treat a 409 `other_users_capsules` the way you treated the 400. Sending `confirm=true` when there is nothing to confirm is harmless. Backed by issue #2171.
- Announced
- October 9, 2026
- Breakingpreview
Type responses stop including `guidance_hash` on October 14, 2026
`guidance_hash` is deprecated on `GET /api/preview/types`, `GET /api/preview/types/{id}`, `POST /api/preview/types`, `PUT /api/preview/types/{id}` (including the `details.current` Type on its 409 conflict) and Type upserts in the sync feed. It is still returned, now flagged `deprecated` in the OpenAPI document, and is removed from these responses on or after October 14, 2026. It is an internal value the server uses to bind guidance tokens, a SHA-256 of the `guidance` text the same response already carries, and it appeared only because these routes returned every stored column. Type responses are now built from an explicit list of public fields, so internal columns added later cannot appear. To detect a guidance change, compare `guidance` or `updated_at`; to authorize a guided write, keep using `guidance_token` from `get_type_context`, `POST /api/preview/types` or `PUT /api/preview/types/{id}`. No other field changes. Backed by issue #1586.
- Announced
- September 30, 2026
- Cosmeticpreview
The unreferenced-edit warning fires only when a cited copy exists
`PUT /api/preview/capsules/{id}` used to add a `warnings[]` entry on every edit of a capsule nothing links to, which is almost every capsule. It now adds one only when another live capsule in the same Type, with the same title (trimmed, case-insensitive) and readable by the caller, is the one other capsules link to, and the warning names that capsule by ref. A Type with `hide_orphan_warning`, a log Type and an archived capsule still never warn. The `unreferenced` and `inbound_ref_count` fields on capsule reads are unchanged.
- Additivepreview
Share with named people, link a project, move into a team, and search Discover
New routes, all additive. `GET`/`POST /api/preview/grants` and `PATCH`/`DELETE /api/preview/grants/{id}` share a capsule, Type or Collection with named people at `view` or `edit`, and `GET /api/preview/shared-with-me` lists what was shared with the caller. `project` is now a share resource kind for `POST /api/preview/share` and the exposure and resolve routes; a project is link-only, so making one public answers 422 `not_publishable`. `POST /api/preview/capsules/batch/move-to-workspace` and `POST /api/preview/projects/{id}/move-to-workspace` move or copy personal content into a team, answering 409 `exposure_requires_confirm` until `confirm_exposure` is sent. `POST /api/preview/capsules/{id}/duplicate` accepts `target: "personal"`, and the save routes accept `workspace_id`. `GET /api/preview/public/discover` searches, filters and sorts public content. `POST`/`DELETE /api/preview/public/follow` and `GET /api/preview/user/following` follow creators, and `GET /api/preview/inbox` now carries `updates` for every account, marked read with `PATCH /api/preview/inbox/notices/{id}`. `GET /api/preview/user/likes` and `DELETE /api/preview/user/likes/{capsuleId}` list and remove the caller's likes. Moving a capsule into a project that has a live link now needs the right to share it, as adding it from the project does. Backed by issue #2267.
- Additivepreview
Describe prompt variables from any client, address log entries by number, and rename a field with its data
`POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` accept an optional `prompt_arguments` map, `{ "<token>": { "description"?, "required"? } }`, from any caller including MCP: argument help for the open `{{token}}` variables of the body, merged per token. A token filled from one of the Type's fields answers 400 and nothing changes; a borrowing capsule answers 409. `PATCH` and `DELETE /api/preview/capsules/{id}/entries/{entryId}` also accept the entry's number (the `N` of a `@type/ref#entry-N` citation) as `{entryId}`; numbers are never reused, so a deleted entry's number answers 404. `PUT /api/preview/types/{id}` accepts an optional `rename_fields` map (`{ "old": "new" }`): every capsule keeps its value under the new name, `{{old}}` in the output template and in field-bound capsule body tokens becomes `{{new}}`, and no `confirm_field_changes` is needed. An old name that is not a field, a new name already in use or repeated, a swap or chain in one call, a freeform or encrypted Type, or a `field_schema` without the new name or still declaring the old one answers 422. Backed by issue #2266.
- Additivepreview
Organization invitations and member changes say what actually refused them
`POST /api/preview/organizations/{id}/invitations` and `PUT /api/preview/organizations/{id}/members/{userId}` accept `role: "admin"`, the name the app shows for `editor`, and store and return `editor`; an unknown role answers "Invalid role. Must be one of: viewer, member, admin." Inviting an address that already has a pending invitation answers 409 with `reason: "invitation_pending"` even when every seat is taken, and a seats-full 409 carries `reason: "seats_full"` and `seats` (`limit`, `members`, `pending`). An email whose local part is longer than 64 octets is refused with 422, as no such address can receive mail. The owner changing their own role or leaving answers 409 with `reason: "owner_must_transfer"` instead of 404. A member-visible 403 `detail` now depends on its `reason` and the caller's role, so an owner or Admin is never told to ask an admin; the `reason` values are unchanged. New: `GET /api/preview/invitations/{id}` reads one invitation for the person it was sent to (anyone else gets 404), `POST /api/preview/organizations/{id}/invitations/{invitationId}/resend` resets a pending invitation to expire seven days from now and sends it again (it counts toward the invitation limit), and the organization invitation listing accepts `include=declined,expired`, adding a `state` to each row; without it the listing is unchanged. Invitation rows gain `responded_at`. Backed by issue #2247.
- Additivepreview
Archiving pauses sharing, and restores, deletes and lifecycle changes say what they affect
Archiving a capsule no longer revokes its share links or rewrites its visibility: they pause while it is archived and work again on restore. `PATCH /capsules/{id}/archive` reports what it paused under `suspended` and what a restore resumed under `resumed` (`links`, `visibility`, `skill`, `embeds`); `revokedShareCount` stays and is always 0. A restore that would bring back links, a public page or a published skill answers 403 `code: share_required` unless the caller may manage the capsule's sharing (with the `share` scope for a key), and a restore past the Free plan's cap answers 403 `code: capsule_limit` with `free_slots`. `GET /share` rows carry `suspended`. Archived capsules are now readable by id and by ref for every caller who could read them live; `PUT /capsules/{id}` and adding them to a project answer 409 `capsule_archived` naming them, and appending to an archived log or bucket, or editing or deleting one of its entries (`PATCH`/`DELETE /capsules/{id}/entries/{entryId}`), answers 409 `capsule_archived`. Deleting a capsule that owns a log answers 409 `owned_log_present` with `owned_log` until `confirmOwnedLog=true` (batch: `confirmOwnedLog: true`); the response reports `ownedLog`. Changing a Type's `expire_after_days` needs its owner or an organization editor or owner (403), and setting or lowering it when capsules are already past it answers 409 `lifecycle_confirm_required` with `lifecycle` counts until `confirm_lifecycle_changes: true`; `POST /types/{id}/lifecycle-impact` previews the counts. `POST /snapshots/{id}/restore` is now documented: it accepts schema-version 1 and 2 snapshots, lists what it cannot bring back, adds `resumes_exposure`, `over_cap` and `archived_over_cap`, needs the `share` scope from a key to bring archived capsules back with live reach, and no longer spends the hourly allowance on a restore that does not run. None of the new 409 or 403 bodies uses `affected` or `details.current`. Classified additive: it adds fields and refusals. Backed by issue #2250.
- Additivepreview
Every 429 names the limit that refused and when it resets
A rate-limited request now answers one problem+json shape: `policy`, `scope`, `code`, `retry_after_seconds`, `reset_at` and `window_seconds`, with the headers `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-RateLimit-Scope` and the new `X-RateLimit-Policy`, all from the policy that refused. Sign-in, sign-up, password, email-verification and MFA routes keep stating no reset time. `PUT /api/preview/share/visibility` to `unlisted` mints a share link, so it now spends the same `share_create` limit as `POST /api/preview/share` and can answer that 429, or 503 when the limiter is unavailable. A refused share link or invitation no longer counts toward its limit. `GET /api/preview/capsules/context` accepts `include=collections`, which adds the Type's collections inline, and its query parameters are now declared in the OpenAPI document. `typeId` is declared required there because the route has always refused a request without it (400); no request that worked before is refused now. Backed by issue #2258.
- Additivepreview
Deleting version history and restoring a snapshot need the `delete` key scope
Closed at once as a security fix rather than after a notice window. An API key now needs the `delete` scope, as well as `update`, for `DELETE /api/preview/capsules/{id}/versions/{versionNum}` and `DELETE /api/preview/types/{id}/versions/{versionNum}`. A real `POST /api/preview/snapshots/{id}/restore` needs `create`, `update` and `delete`, because it removes, overwrites and re-adds content; a key missing one answers 403 with `code` `insufficient_scope` naming it, before the restore rate limit is spent. A restore with `{"dryRun": true}` now needs `read` (before, `create`), so a key without `read` can no longer request one. Browser sessions and who may act on a row are unchanged. Backed by issue #2514.
- Additivepreview
Deleting the current version of a capsule or Type now returns 409
`DELETE /api/preview/capsules/{id}/versions/{versionNum}` and `DELETE /api/preview/types/{id}/versions/{versionNum}` return `409 Conflict` when `versionNum` is the subject's newest version. That version is the live state and the baseline the next edit is undone against, so removing it made that edit unrecoverable. Older versions delete exactly as before, and a missing version still returns `404`. Retention changed alongside: a version is pruned 30 days after a newer one replaced it, and the newest version is never pruned by age, so an idle capsule or Type keeps its current version in `GET .../versions` (subject to the existing history floor for teammates). Classified additive: the web app already disabled Delete on the current version, and the refusal protects data rather than changing a field's shape or meaning. Backed by issue #2484.
- Additivepreview
Prompt resolve refuses unknown arguments and returns images and referenced capsules
`POST /api/preview/prompts/resolve` now answers 400 when `arguments` names anything that is not one of the prompt's open `{{variables}}`, with a `detail` such as "Unknown argument(s): topic. This prompt accepts: platform" (or "This prompt accepts no arguments."). Field-bound tokens were never arguments, as the prompts documentation already said; an extra argument used to be ignored, so the prompt came back with its stored value instead of the one the caller meant. The MCP `prompts/get` method answers this as JSON-RPC -32602. `messages` may now hold more than the one composed `text` message: `{{image}}` fields compose, one `text` message follows per referenced capsule the caller may read (at most five), attachment embeds in the prompt and in those capsules become absolute links that load without a session for about an hour, and one `image` message (`{ type, data, mimeType }`) follows per embedded raster attachment, within one payload budget across them all. A reference the caller cannot read, or an encrypted one, is left in the text as an inline code span. Classified additive: the refusal answers input the route never documented, and a client that reads only the first message still gets the composed text, now with working attachment links. Backed by issue #2257.
- Additivepreview
Upload confirm returns the embed to write and the capsule's new version token
`POST /api/preview/attachments/confirm` adds `embed_markdown`, the markdown that places the file in a capsule body (`` for an image, else `[<file_name>](/api/preview/attachments/file/<key>)`), which carries no expiring token. A body that names the file, like ``, is rewritten to that form when the upload is confirmed, and the reply then carries `capsule_updated_at`, the capsule's new `updated_at`, as the attachment `DELETE` already does; it is `null` when the body was not changed. A next write that checks the capsule's version must send this value, or it is refused as out of date. Backed by issue #2257.
- Additivepreview
Both save routes accept a duplicate-copy flag and validate their body strictly
`POST /api/preview/share/{token}/save` and `POST /api/preview/public/save` accept an optional boolean `allow_duplicate`: when you already hold a copy of the capsule, the save answers 409 naming that copy unless `allow_duplicate` is `true`, which makes a fresh one. Both routes now validate the body the same way. A body larger than 4096 bytes answers 413. A key the route does not accept answers the standard unknown-field 422 with `unknown_field`; the token route accepts `capsuleId` and `allow_duplicate`, and the public route also accepts `resourceType` and `resourceId`. A `capsuleId` that is present but not a UUID string answers 400 instead of being read as absent, which on a library link used to answer 404. On the token route, a body that is not a JSON object answers 400; an empty POST still saves a single-capsule link. Classified additive rather than breaking: each new refusal answers input the routes never documented, and a request that sends only the documented keys is unchanged. Backed by issue #2245.
- Additivepreview
A field filter naming a choice that can never match answers 400 instead of an empty list
On `GET /api/preview/capsules` with `typeId`, a `filter` on an `enum` field (`eq`, `in`) or a `multi_enum` field (`contains`, `contains_any`) whose value, or any element of an `in` or `contains_any` list, is neither in the field's `enum_values` nor stored on any active capsule of that Type the caller can read now answers 400 instead of an empty page, and an `aggregate` over such a filter answers 400 instead of `count: 0`. The body names the field and carries `param: "filter"`, `field`, `value` and `allowed` (the field's `enum_values`). A value removed from `enum_values` but still stored on a capsule the caller can read keeps filtering as before. A filter over a Type the caller cannot read still answers the empty page. Classified additive rather than breaking: the 400 only replaces an answer that could never contain a capsule, so no request that returned data changes. Backed by issue #2318.
- Additivepreview
Assistant clients declare themselves, and a presented guidance token is always verified
A Symbol MCP client now sends an `x-symbol-ai-client: mcp` request header that marks its writes as coming from an assistant, so the guidance-token, summary and Type AI-permission rules the hosted connection already enforced apply on every connection. Two consequences a caller may notice: creating a capsule as a declared assistant into an unencrypted Type now requires a non-empty `summary`, answering 422 with `error: "summary_required"` when it is missing (the existing `summary_update_required` already covered body edits); and a request that presents an `x-symbol-guidance-token` header is now verified against the Type's current guidance for every caller, so an invented, forged, expired or stale token answers 422 instead of passing. A request that sends neither header is unchanged: web, Flutter and plain personal-key API calls stay exempt. Classified additive: it only adds refusals, for callers that send these headers. Backed by issue #2241.
- Additivepreview
Filter a capsule list by a reference field by the capsule it points to
On `GET /api/preview/capsules`, a `filter` on a `reference` field (`eq`) or a `multi_reference` field (`contains`) now matches by the capsule the stored value names, not the exact text stored. The value may be given as `@type/N`, `@type/alias`, a bare alias, or the capsule's id, and all of them match the same rows; a target whose alias was renamed still matches, and an archived target is matched too. A value that names no capsule the caller can read is refused with 400. Previously only the exact stored spelling matched, so the same capsule written another way returned an empty list.
- Additivepreview
Direct member add is removed, publishing needs the share right, and organizations can forbid outside links
`POST /api/preview/organizations/{id}/members` is removed and answers 405; `GET` on the same path is unchanged. A person now joins an organization only by accepting an invitation (`POST /api/preview/organizations/{id}/invitations`, then `POST /api/preview/invitations/{id}/accept`). Accepting answers 410 with the existing expired response when the invitation's sender has since been removed or is no longer an owner or Admin, and `POST /api/preview/organizations/{id}/transfer` answers 404 for a target that is not a current member backed by a real user. Publishing to an organization now needs the share right on the capsule (its owner, or an organization owner or Admin for content that is not Private): `PUT /api/preview/capsules/{id}/skill`, `POST /api/preview/capsules/{id}/skill/publish`, `POST /api/preview/capsules/{id}/agent` and `POST /api/preview/capsules/batch/skill` answer 403 with `code` `insufficient_role` without it, and 409 with `code` `private_capsule` for a first publish of a Private capsule in an organization (per capsule on the batch route). `PUT /api/preview/capsules/{id}` with `agent: true` saves the rest of the edit and returns the refusal in `warnings`. An edit or version restore by someone without the right saves, leaves the published skill or agent unchanged, and returns a `warnings` entry beginning "Not republished". Organization API keys hold no share right yet, so they are refused on all of these. `POST /api/preview/capsules/{id}/agent` on an existing agent refreshes what members receive. `GET /api/preview/agents/export` (both targets) and the organization marketplace now render each agent from its published version, and `GET /api/preview/capsules/{id}/agent/markdown` returns the current working copy. A Private capsule in an organization is no longer served as a skill or agent to anyone but its owner, its published skill's attachment URLs answer 404, and capsule reads drop `alsoAvailableAsSkill` for it; sharing it with the organization again restores all of it. `PUT /api/preview/types/{id}` and `POST /api/preview/types/{id}/versions/{versionNum}/restore` answer 403 with `reason` `insufficient_role` when a change to `guidance`, `share_template` or `package_as` comes from anyone but the Type's owner or an organization owner or Admin; other fields are unaffected. `GET /api/preview/capsules/context` adds `type.guidance_updated_by_name` (a display name, or "a former member") and `type.guidance_updated_at`, both nullable. `POST /api/preview/capsules/{id}/duplicate` now needs edit access to the source: a viewer gets 403 instead of 201. `GET` and `PUT /api/preview/organizations/{id}/governance` add the boolean `allow_outside_links` (default `true`). While it is `false`, every share link on the organization's capsules, Types and Collections answers 404 on every share surface, and `POST /api/preview/share` and `PUT /api/preview/share/visibility` answer 403 with `code` `org_policy_forbids_links` instead of minting a link. Setting it back to `true` restores the existing links; nothing is revoked. Personal content is never affected. Classified additive rather than breaking: the removed route and each new refusal let a caller act beyond the authority the product grants, so they close at once as security fixes rather than after a notice window. No Symbol client called the removed route. Backed by issue #2236.
- Additivepreview
Field filters match any of several values for one field
`GET /api/preview/capsules` accepts two new `filter` ops within a Type: `<field>:in:<json array>`, the any-of form of `eq`, and `<field>:contains_any:<json array>`, the any-of form of `contains`. The value is a JSON array of 1 to 100 non-empty strings, for example `agent:in:["gaffer","all"]`; the row matches when its field matches any element, and the entry still ANDs with every other `filter`. Each op is accepted on exactly the kinds its single form is, and every element is validated as a single value would be. A value that is not such an array, or an element its kind refuses, answers 400 with `param`, `field`, `op` and the offending `value`; an empty or over-long list answers 400 with `param`, `field` and `op`. Existing ops are unchanged: their value is still the literal text after the second colon, commas and brackets included. On a `reference` (`in`) or `multi_reference` (`contains_any`) field each element is matched by the capsule it names, as a single value is, and an element naming no capsule you can read answers the same 400 a single value would, with `param`, `field` and that element as `value`.
- Cosmeticpreview
Caller mistakes answer a 4xx that states the reason instead of a 500 or a misleading 404
A malformed id in the path or query of the organization, messages, inbox, snapshot restore and project context routes, in `GET /api/preview/share?resourceId=`, and in the `capsuleId` body of the share save, like and copy routes now answers 400 before any lookup, instead of a 500. An API key that lacks the scope for a write on a resource it can read (for example a `read` key updating, archiving or deleting its owner's capsule, or updating a Type) now answers 403 `insufficient_scope` instead of 404; a resource the key cannot read still answers the uniform 404. Every scope denial now shares one problem+json body with `code: "insufficient_scope"`. Sharing encrypted content now answers 409 `encrypted` with one message on `POST /api/preview/share` (previously 400) and on `PUT /api/preview/share/visibility`, and the refusal now covers encrypted Types and their collections as well as capsules. `POST /api/preview/capsules/{id}/duplicate` answers 403 with `reason: "insufficient_role"` when the caller's role can't create in the capsule's Type, instead of 500. In `POST /api/preview/capsules/batch/move`, a target Type in another workspace is reported in `failed[]` with `code: "workspace_mismatch"` instead of `permission_denied`. Clients that read `detail` need no change.
- Additivepreview
Filter a Type's text field by a scope pattern with the new covers op
`GET /api/preview/capsules` field filters gain a `covers` op on `text` fields. The stored value is read as a scope pattern and matched when it covers the filter value: `filter=scope:covers:GravityKit/GravityView` returns the capsules whose `scope` is `GravityKit/GravityView`, `GravityKit/*`, or `*`. The pattern grammar is a whole-segment prefix followed by `/*`, or `*` alone; matching is exact and case-sensitive, and `GravityKit/*` does not cover `GravityKit` itself. A `covers` filter with no `sort` orders the page by specificity, the most specific matching pattern first; an explicit `sort` overrides that, and the specificity cursor is bound to the filter value, so replaying a page cursor under a different value answers 400 `Invalid cursor`. `covers` on a non-text field, a value with an empty path segment, a value over 32 segments, and a second `covers` filter in one request each answer 400 naming the rule. Backed by issue #2320.
- Additivepreview
Deleting a Type reports what it affects, and reports the aliases a merge drops
`GET /api/preview/types/{id}/delete-impact` reports everything deleting a Type would affect beyond its own capsules: capsules that mention the Type, other Types whose reference fields are constrained to it, its collections, and its usable Type and collection share links, each a count plus a small sample. Every count is scoped to the Type you are authorised to delete; another tenant's rows never appear. `DELETE /api/preview/types/{id}` accepts `confirm`. A caller that sends `confirm=true` or `confirm=false` opts in to the confirm gate now: a delete with any of those side effects and without `confirm=true` is refused with 409 `impact_confirmation_required` (a `code` and an `impact` block in the Problem Details), and a delete refused because the Type holds other users' capsules answers 409 `other_users_capsules` instead of 400. A caller that omits `confirm` keeps the old behaviour until the breaking change announced with this one, "Deleting a Type with side effects needs `confirm=true`", takes effect. The existing `strategy_required` 409 now carries the same `impact` block and a `code`. A delete-migrate response now includes `dropped_aliases`, naming every alias that could not travel to the target Type (its old token still redirects to the capsule). When a migrate already moved capsules before the delete is refused, the 409 also reports `capsules_migrated` and the moved refs and aliases. Backed by issue #2171.
- Additivepreview
Rename a Type's slug, edit fields and tags in bulk, and import or export content
`PUT /api/preview/types/{id}` now accepts `slug`: the Type is re-addressed, and every `@old-slug/N`, `@old-slug/alias` and bare `@old-slug` reference keeps resolving to it. Reference fields on other Types constrained to the old slug follow the rename, and the response carries `slug_rename` naming them. A slug already held by, or still redirecting to, another Type answers 409 (`code` `slug_taken` or `slug_tombstoned`). The slug was previously rejected with 422 and the old "cannot be changed" message is retired. New operations: `POST /api/preview/capsules/batch/edit-fields` sets or clears one field across up to 50 capsules and `POST /api/preview/capsules/batch/edit-tags` adds, removes or replaces tags, both with `dry_run` for a per-capsule count first; `POST /api/preview/tags/rename` renames or merges a tag across the caller's editable capsules and `GET /api/preview/tags/usage` lists the caller's tags with counts; `POST /api/preview/import/analyze` and `POST /api/preview/import` bring CSV, Markdown with YAML frontmatter, or a Symbol export in as typed capsules (up to 1,000 rows per call, charged to its own `import_run` limit rather than the per-capsule create limit; a preview through `analyze` has its own `import_analyze` limit of 60 an hour and answers 429 with `Retry-After` past it); a reference to a capsule a later call creates no longer fails its row: the capsule is created and the value comes back, signed, in `held_references`, which the next call takes unchanged and fills in once the target exists (and only while the field still holds what the import left, so a later edit is never overwritten); `GET /api/preview/export` pages an account, Type or organization out as Markdown files with frontmatter that the importer reads back with references intact. Backed by issue #2104.
- Additivepreview
Adding a capsule to a project, or removing it, no longer changes the capsule's `updated_at`
`POST /api/preview/projects/{id}/capsules` and `DELETE /api/preview/projects/{id}/capsules` change which project a capsule belongs to without advancing that capsule's `updated_at`. `updated_at` is the version a capsule edit sends back for its conflict check, and filing a capsule into a project is not an edit of it, so an edit carrying the `updated_at` read before the membership change now succeeds instead of returning 409. An edit made by someone else in between still returns 409 as before. The project's own `updated_at` still advances, so a listing sorted by capsule recency no longer moves a capsule to the top just because it was filed. Moving a capsule with `PUT /api/preview/capsules/{id}` and a `project_id` is an edit and still advances it, returning the new value. Because filing is no longer an edit, it also no longer renews a capsule's stale or archive clock, and the `updated_after`, `updated_before` and `updated_within` filters on `GET /api/preview/capsules` no longer match a capsule that was only filed or unfiled. A client that keeps an edit form open should send `project_id` on save only when the user changed it: a form that always resends the project it loaded will put the capsule back if someone else changed its project in the meantime, since that change no longer invalidates the form's `updated_at`. The Symbol web and mobile editors already send it only on a change. No other client change is required.
- Cosmeticpreview
Project membership rejects malformed capsule ids with 400, and the capsule edit 409 no longer carries internal columns
`POST /api/preview/projects/{id}/capsules` and `DELETE /api/preview/projects/{id}/capsules` now answer 400 when any `capsule_ids` entry is not a UUID, as the published request schema already required. Previously a malformed id reached the database and came back as a 409 `CONFLICT` whose `details` carried the database's error text. An unexpected failure on the add route is now a plain 500 rather than a 409. The `details.current` object on a `PUT /api/preview/capsules/{id}` 409 `VERSION_CONFLICT` is now shaped like any other capsule response: its `updated_at` and editable fields are unchanged, and the internal search and classification columns it used to include are gone. No client change is required.
- Additivepreview
A Type reports what cites it, and the citing capsules are listable
A capsule body can reference a whole Type, and that reference resolves to the Type's own meta — never to its capsules, so citing a Type of five hundred capsules costs one small object rather than the Type's contents. `GET /api/preview/types/{id}/backlinks` lists the capsules that cite a Type. It pages and budgets like the capsule backlinks read it mirrors — `items`, `nextCursor`, `hasMore`, `total`, `returned`, `content_level`, `overBudget`, `truncatedBy` and `reason` — so a client that already consumes capsule backlinks needs no new handling. `GET /api/preview/types/{id}` carries `inbound_ref_count`, the count of capsules citing that Type, scoped to the Type's own workspace rather than the one the request resolved, and `GET /api/preview/capsules/context` reports the same count for the Type it resolves. It is advisory and never fatal to the read it rides on, so it reports `0` if the count cannot be taken: read it as "no citers found", not as proof of none. It is a scalar meta count and never the citing capsules themselves; fetch those from the backlinks read when they are wanted. It is computed per read on the detail path only, so the create and update responses do not carry it — read the Type back when the count is what you need. Additive throughout — a client that ignores both is unaffected. Backed by issue #1919.
- Additivepreview
A packaged capsule reports whether it would actually ship
A capsule packaged as a skill or an agent could render to an artifact over the distribution limit, or be blocked from packaging for reasons the writer never saw at write time. The write responses of `POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` now carry a `packaging` object answering that at the moment of the write, and the single-capsule reads `GET /api/preview/capsules/{id}` and `GET /api/preview/capsules/by-ref` carry it when asked for the assistant shape (`?shape=mcp`). A plain read does not compute it, and pays nothing for it. It reports `packaged_as` (`skill` or `agent`), `eligible`, and `blockers` — every blocking condition rather than the first, each with a machine-readable `code` to key remedies off and a publisher-facing `message`. Size is reported as `rendered_bytes` measured on the artifact the platform actually distributes (the enriched `SKILL.md`, or the project-file form for an agent), against `limit_bytes` and the softer `warn_bytes`, with `approaching_limit` true from just above `warn_bytes` up to and including `limit_bytes` — a capsule at exactly the limit is both approaching it and still eligible. `rendered_bytes` is `null` when nothing renders, as for an encrypted capsule. `published_state` distinguishes a published artifact left behind by a later edit (`stale`) from a rejected write (`failed`), and is always `null` for agents. The object is absent for a capsule that is not packaged, so on a write response, or a read that asked for the assistant shape, its presence means "this capsule occupies a packaging surface". Absence on a plain web read means only that the read did not ask. Backed by issue #1943.
- Additivepreview
A client that can hold a local copy can seed it and keep it current
Two endpoints let a client that keeps its own store serve reads from it instead of paying a round trip for each one. `GET /api/preview/sync/snapshot` seeds the store: one keyset page of the rows the caller can currently read for a single `entity` (`capsules`, `types`, `projects`, `collections` or `collection_members`), taking `cursor`, `limit` (1-200, default 100) and `ws` (`all`, `personal`, or an organization id within the caller's grants), and answering `{ entity, items, hasMore, nextCursor }`. `GET /api/preview/sync/changes` keeps it current. It answers the settled changes after `cursor` in one `changes` array, each entry tagged `op: "upsert"` or `op: "remove"`: an upsert carries the entity as its own routes serialize it, in the assistant-shape superset, and a remove carries identifiers only. Alongside it come the next `cursor`, `hasMore`, `resync`, `grants`, `server_time` and `retention_days`. A first call sends no cursor; that response, and any call whose cursor predates retention, comes back `resync: true`, and its `cursor` is the watermark to resume from once a fresh snapshot is taken — so a client offline for longer than the feed is kept is told to reseed rather than silently missing a change. Both read the caller's reach as it stands now rather than as it stood when a row changed, so losing access to a capsule arrives as a `remove`. Unknown query parameters are rejected rather than ignored, as on the capsule list route, and errors are problem+json. Backed by issue #1881.
- Additivepreview
Filter a capsule listing by time window, resolved in the reader's timezone
`GET /api/preview/capsules` accepts eleven new time filters. Each of creation, last update and archival takes a named calendar window (`created_period`, `updated_period`, `archived_period`) and an explicit half-open range (`created_after`/`created_before` and their `updated_` and `archived_` siblings, the lower bound inclusive and the upper bound exclusive). Bounds are spelled as an ISO-8601 instant (`2026-09-01T00:00:00Z`) or as an offset-less date or date-time (`2026-09-01`), which is read in the effective timezone rather than silently in UTC. `updated_within` and `archived_within` join the existing `created_within`, and that grammar generalizes from the fixed `24h`/`7d`/`30d` set to an integer 1-9999 followed by `h`, `d` or `w` — the three legacy values still parse unchanged. All four `archived_*` filters are only valid alongside `archived=true`, which is the only listing where an archival date exists to filter on. A named period resolves in the effective timezone: the request's own `tz` parameter (an IANA name) if present, otherwise the caller's stored timezone, otherwise UTC — so "this week" means the reader's week and not the server's. Separately, the same listing accepts `stale=true` to return only capsules that have gone stale, or `stale=false` to exclude them; absent, staleness is unfiltered. `stale=true` is rejected alongside `archived=true`, since an archived capsule is never stale. Every filter here is optional and a request that sends none behaves exactly as before. Backed by issues #1905 and #1906.
- Additivepreview
A caller can read and set the timezone its date filters resolve in
`GET /api/preview/user/timezone` answers `{ "timezone": … }`, an IANA name or `null` when none was ever captured. `PUT /api/preview/user/timezone` sets it from a body of `{ timezone, if_unset? }`: a `null` timezone clears the stored value, and `if_unset: true` writes only when nothing is stored yet, so a client that detects the device timezone on sign-in can offer a sensible default without ever overwriting a choice the person made deliberately. Nothing is backfilled: an account that predates this reads `null`, and a request that finds `null` falls back to UTC exactly as it did before. The stored value is what the named calendar windows above resolve in when a request sends no `tz`. Backed by issue #1905.
- Cosmeticpreview
The last two routes answering `{"error"}` now answer problem+json
`POST /api/preview/capsules` and `POST /api/preview/attachments/draft/confirm` were the last routes on the preview surface whose errors used the pre-RFC-9457 `{"error": "…"}` envelope, so a client reading `detail` — which every other route teaches it to read — got `undefined` at exactly the moment it needed the reason. Every error exit of both now answers `application/problem+json` with `type`, `title`, `status` and `detail`, and machine-readable members stay at the top level: `code` and the rejection's own fields on the capsule-create `422`s, `code` and `storage` on an attachment quota refusal. A rate-limited or over-quota upload keeps its status and its `Retry-After` header, and keeps an `error` member alongside `detail` so a client still reading it is unaffected. On draft confirm, a missing or blank `draftAttachmentId`, `objectKey` or `fileName` now answers `422` naming the field instead of a single `400`; a storage miss keeps answering `404`, which clients treat as retryable, and the object key is compared exactly as issued. Separately, a `reference` field value that is a capsule UUID is now reported as a malformed reference, naming the `@type/slug` or `@type/N` form to use, instead of claiming the capsule does not exist or is not accessible. Backed by issue #1705.
- Additivepreview
Create a capsule by Type slug, and see who produced each version
`POST /api/preview/capsules` accepts an optional `type_slug`, naming the destination Type by the same slug the rest of the product uses in `@type/ref`, so a client no longer has to resolve a Type UUID first. It is scoped to the Types you can read in the resolved workspace, so an unknown or inaccessible slug answers `404` without disclosing whether it exists; sending `type_id` and `type_slug` that disagree answers `422` naming both. `type_id` alone behaves exactly as before. Capsule version reads — `GET /api/preview/capsules/{id}/versions` and `GET /api/preview/capsules/{id}/versions/{versionNum}` — gain `edited_by`, the member who produced that version. It is distinct from `created_by`, which is the capsule owner and storage attribution and is unchanged: in a shared workspace those are different people, and only `edited_by` answers "who changed this". Versions recorded before this shipped carry `edited_by: null`, which is the honest value for authorship that was never recorded rather than a guess. Backed by issue #1705.
- Cosmeticpreview
Hourly request budgets are halved on every plan
The per-hour request budget for `/api/preview/*` is now 250 requests on Free and 500 on Pro and Teams, down from 500 and 1000. The per-API-key sub-ceiling keeps its 80% relationship to the per-user cap and moves with it, to 200 on Free and 400 on Pro and Teams; a key with an explicit `rate_limit_max` override still uses that override unchanged. Nothing else about the budget changes: the window is still a fixed hour, the `X-RateLimit-*` response headers still report the cap actually enforced, and an exhausted budget still answers `429` with `Retry-After`. Read your current caps from `GET /api/preview/user/rate-limit-status` rather than assuming the old figures. No other rate limit is affected.
- Additivepreview
Writes say where they landed, what they promoted, and what they removed
The preview write routes now report what the write actually did instead of leaving the caller to infer it. `POST /api/preview/types`, `POST /api/preview/capsules`, `POST /api/preview/projects` and `POST /api/preview/attachments/draft/presign` read the `?ws=` query parameter as a write destination: sent alone it is honoured, and a `?ws=` that names a different workspace from the request body is refused with `400` rather than silently landing wherever the body pointed. `?ws=all` is a discovery scope and is refused as a destination. `POST /api/preview/capsules` adds an `attachments` response field listing the draft attachments the create actually promoted, and refuses with `422` `draft_attachments_unresolved` — naming the ids in `missing_ids`, with no capsule created — when a named draft cannot be attached, instead of returning `201` having dropped it. `PUT /api/preview/capsules/{id}` adds non-blocking `warnings` entries naming list, table, or whole-field values a `field_values` replacement removed. `POST /api/preview/attachments/confirm` adds a `retention` response object (`days`, `expires_at`, `policy`, `note`) so the caller learns how long the file is kept at the moment it is stored. The new response fields are additive and optional, and no existing response field changes shape or meaning. The REQUEST side does change: the existing `?ws=` query parameter, previously ignored on these writes, now selects the destination workspace. A value that agrees with the body (or stands alone) is honoured; a conflict, and `?ws=all`, return `400` where the write used to succeed against the body's workspace. Backed by issue #1704.
- Additivepreview
A reference can link a capsule without inlining it
A single `~` immediately before a cross-type reference (`~@type/slug`) marks it pointer-only: it resolves, links, and still counts as a graph edge, but the target's body is never inlined when the containing capsule is read. Plain `@type/slug` is unchanged and still inlines, so no existing content behaves differently. This makes hub-and-spoke indexes practical, since a hub that points at ten capsules returns a response close to its own size instead of concatenating everything it points at. Every `resolvedCrossRefs` entry — on `GET /api/preview/capsules`, `GET /api/preview/capsules/{id}`, `GET /api/preview/capsules/by-ref` and `POST /api/preview/capsules/{id}/duplicate` — carries a new `expansion` field, either `"inline"` or `"pointer"`, so a caller can tell what a reference is, and what a read delivered for it, without fetching the target. When the same target is referenced both ways, inline wins. On `GET /api/preview/capsules/context`, a Type whose guidance points at a shared rule with `~` reports that rule in `guidance_references` with the new `reason: "pointer"` and `available: false`, carrying no `content_md` and no `content` envelope: the rule was withheld by the author's choice rather than by a failure, and a client that does not know the reason still reads it as not delivered instead of showing an applicable rule with an empty body. Additive throughout: the sigil is never part of the canonical reference, so `source_ref` and every stored reference edge are byte-identical to an un-sigiled reference, and a client that ignores `expansion` behaves exactly as before. Backed by issue #1764.
- Additivepreview
Decisions carry typed links, and a person can answer in their own words
`POST /api/preview/messages` accepts an optional `links` array on a Decision — up to four `{ label, href }` pairs, each `href` an absolute `https://` URL and each `label` carrying at least one non-whitespace character and at most ten words. They render as anchors with the target host shown; Decision prose is still never linkified, so a real link target has to ride this typed channel. `POST /api/preview/messages/{id}/reply` widens each entry in `responses` to also accept `{ "alternative": string }` (at most 4,000 characters) — the answer a person wrote themselves after rejecting every offered option, readable back as their own words rather than as a picked option. An alternative is only valid where options were offered: sent for a `text` question it is refused with `422`, because that answer is free text already and would otherwise escape the question's own length and word rules. Existing scalar answers are unchanged, both fields are optional, and no existing field changes shape or meaning. Backed by issue #1758.
- Additivepreview
Form capsules and the Inbox (experimental)
New experimental surface: an assistant can ask a person a short structured question set and read the answers back. `POST /api/preview/forms` creates a form capsule from inline sections or from a Type packaged as a form; `GET /api/preview/forms/{id}` reads one; `PATCH /api/preview/forms/{id}/responses` submits answers (latest-wins, partial submissions accumulate); `GET /api/preview/inbox` lists the caller's pending and answered forms with exact counts. `GET`/`PUT /api/preview/user/experimental` read and set the per-user experimental-features opt-in that gates all of the above — without it every form and inbox route 404s. `PUT /api/preview/types/{id}` accepts an optional `form_packaged` boolean (revocable, idempotent) and every Type response now carries `form_packaged_at`. Capsule read responses carry three new fields — `form_schema`, `form_responses`, and `form_answered_at` — which are null on every capsule that is not a form. Additive throughout: no existing request or response field changes shape or meaning, and a client that ignores the new fields behaves exactly as before. Backed by issue #1545.
- Additivepreview
Skill auto-republish default now depends on the caller
The `republish` behaviour on `PUT /api/preview/capsules/{id}` (and the newly body-accepting `POST /api/preview/capsules/{id}/versions/{versionNum}/restore`) now resolves its DEFAULT by caller type when the field is omitted. An explicit `republish` value still always wins. When omitted, a capsule edit or version restore that leaves a PUBLISHED skill stale re-publishes it only for AI/MCP callers; for a plain API key the default is now to leave the skill published-but-stale, and the response `warnings` array says so — send `republish: true` to opt in. This narrows the previous blanket `default true` (issues #1365/#1316) to AI callers, per the clarified policy in issue #1407. `POST .../restore` additionally accepts an optional JSON body `{ republish?: boolean }` (it previously took none); omitting the body is unchanged and additive. Classified additive: no request or response field changes shape, and the only behaviour change is the safer, more explicit default for non-AI callers. Backed by issue #1407.
- Additivepreview
Capsules carry first-class agent frontmatter overrides
`POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` accept an optional `agent_meta` object — `{ name?, description?, frontmatter? }` — that overrides what a capsule packaged as a Claude Code subagent serves in its `AGENT.md`. `name` takes precedence over the capsule's alias and must match the subagent charset (lowercase letters, digits, single hyphens); `description` takes precedence over the capsule's `summary`; `frontmatter` carries the remaining allowlisted subagent keys (e.g. `model`, `tools`) and rejects any key outside that allowlist. Omit the field to leave the stored value untouched, or send `null` to clear it — an all-blank object also clears it, so a capsule with no meaningful overrides renders exactly as it did before this field existed. Additive and optional throughout: no existing request or response field changes shape or meaning, and a capsule that never sends `agent_meta` keeps using the alias/summary fallbacks. Backed by issue #1388.
- Additivepreview
Deleting a Type version in use by capsule history now returns 409
`DELETE /api/preview/types/:id/versions/:versionNum` returns `409 Conflict` when the Type version is still the framing a capsule version's `rendered_md` is composed through. Deleting it would drop those capsule versions back to the Type's current schema and retroactively re-render history — the drift issue #1263 closes. The refusal releases on its own as the referencing capsule versions are deleted or pruned. Unreferenced Type versions delete exactly as before, and a missing version still returns `404`. Classified additive rather than breaking: the 409 is reachable only through a capsule-version-to-Type-version binding, which no row carries until this release ships, so no existing integration's behaviour changes and there is nothing to migrate.
- Breakingpreview
A log Type keeps dated buckets or owner-bound logs, not both
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept a new `log_mode` field on `log`-structure Types: `period` (the default, and today's behaviour — capsules are dated buckets governed by `rollover`/`log_timezone`) or `owned` (capsules are the dedicated logs of capsules in other Types). The two shapes are now exclusive and enforced. On an `owned` Type, `POST /api/preview/log-entries` with `type_id` — which resolves a dated bucket — is rejected with 400; the append must name the owner capsule via `capsule_id`. Previously both calls succeeded on the same Type, so one Type silently produced two incompatible capsule shapes and the shorter call was usually the wrong one. Setting `log_target_type_id` on a Type now also switches its target log Type to `owned`, and is refused when that Type already holds dated capsules; leaving `owned` is refused while any Type still logs there. Existing data is unaffected — log Types that already had Types bound to them are migrated to `owned`, and dated capsules already sitting in such a Type stay readable and directly addressable by `capsule_id`. Reads are additive: `GET /api/preview/types/{id}` and the Type-context read carry `log_addressing` (`mode`, `owner_types`, and a composed `statement`) on log Types, and a successful append reports `addressing.mode` (`bucket`/`owned`/`direct`). Backed by issue #1121.
- Announced
- July 23, 2026
- Additivepreview
Capsule version detail reports the framing it was composed through
`GET /api/preview/capsules/:id/versions/:versionNum` adds a `composition_basis` field to the response. It is `"type_version"` when the version's `rendered_md` was composed through the Type schema/template captured when the version was written — faithful, and no longer re-rendered when the Type's fields or output template are later edited — or `"live_type"` when it falls back to the Type's current schema (versions written before this shipped, or whose captured Type version was pruned). A `type_version` composed payload is immutable and served with `Cache-Control: immutable`; a `live_type` composed payload revalidates, as before. All existing fields are unchanged. Backed by issue #1263.
- Cosmeticpreview
get_type_context structured shared-rule fields match the web view and search index
The structured `content` fields of a capsule transcluded by a Type's guidance are now projected by the same rule the web read view, the read preview, and the search index use. In `get_type_context` output, a stored field value whose runtime shape no longer matches its declared field type — most commonly a value left behind under a field since retyped, e.g. a string under a field that is now `boolean`, an array under one that is now `text`, or a bare string under one that is now `list`/`multi_enum`/`multi_reference` — is omitted rather than printed. Validly-typed scalars, booleans (including `false`), and tables render exactly as before; list items are additionally trimmed and blank items dropped, matching the web read view and search index. Same-capsule, same answer everywhere; no request or response schema changes. Backed by issue #1271.
- Additivepreview
A Type's "Publish as" is now an editable default, and packaging is per-capsule
`package_as` on a Type is now editable after creation and behaves as a DEFAULT for new capsules only — it is never retroactive. Changing it no longer repackages capsules that already exist; each capsule's skill/agent membership is materialized on the capsule and individually overridable. `update_type` accepts `package_as` over REST and MCP, and a new per-capsule agent toggle is exposed at `POST`/`DELETE`/`GET /api/preview/capsules/{id}/agent` (mirroring the existing `/skill` endpoint). Existing published skills and synced agents are unchanged by the migration that backfills the per-capsule rows.
- Additivepreview
Capsule versions capture token bindings, so a restore renders faithfully
A capsule version now snapshots its `token_bindings` — how each authored `{{token}}` was classified (bound to a field vs. left open) — alongside `title`, `content_md`, `summary`, and `field_values`. An edit that changes only the bindings (the editor's open ↔ field toggle) is now a content change that records a version, where before it left no history and was silently lost by any later restore. Restoring a version applies the bindings captured at that version, so a restored body renders through the classification it had when written rather than through the capsule's current bindings. `GET /api/preview/capsules/{id}/versions/{versionNum}` returns a new `token_bindings` field (the map captured at that version); its `rendered_md` is already composed server-side through those bindings, reconciled against the version's own body and the Type's current schema. Existing versions are unaffected: they backfill to an empty `token_bindings` map, which reconciles body-driven to the same name-match default the pre-versioning read produced. Fully backward-compatible: the field is additive and no existing response field changes shape. Backed by issue #1262.
- Additivepreview
Guidance transclusion delivers field-structured capsules and hashes their field content
A capsule transcluded by a Type's guidance is now delivered through the same `content` envelope the capsule read returns: `GET /api/preview/capsules/context` adds an optional `content` object to each entry of `guidance_references`, with `format` naming the one representation (`markdown`, `composed`, `structured`, or `log`). This fixes body-less, field-structured referenced capsules (a Type with `content_md_max: 0`), which previously resolved to an empty body under `reason: "ok"`: their field content now arrives, composed through the referenced Type's `output_template` when one is set and as structured fields when not. `content_md` stays on each entry, byte-identical, as deprecated back-compat. The `reason` enum gains `"empty"` for a resolved, unencrypted reference that genuinely carries nothing to deliver — no body, no log summary, and no field value its Type's schema can render (a value stranded under a removed or renamed field, or left behind under a field since retyped, renders nowhere, so it reads as empty rather than promising content); `available` stays `true`. The guidance token now binds to a referenced capsule's stored `field_values` as well as its title and body, so editing any field of a shared, field-structured rule capsule invalidates in-flight guidance tokens on every dependent Type — the #1198 invalidation guarantee, now honoured for field-structured capsules. Prose references are unchanged byte for byte, including their hash, so tokens minted before this ships keep verifying. Backed by issue #1251.
- Additivepreview
Capsule versions capture structured field values, and version detail returns the composed body
A capsule version now snapshots its `field_values` alongside `title`, `content_md`, and `summary`, so a field-structured or body-less capsule (a Type with `content_md_max: 0`) has a usable revision history instead of a permanently-empty one. An edit that changes only field values is treated as a content change, not a metadata-only edit, so it creates a version; restoring a version restores the field values it captured. `GET /api/preview/capsules/{id}/versions/{versionNum}` returns two new fields: `field_values` (the map captured at that version) and `rendered_md` (the server-composed effective body, or `null` for a plain-markdown version), so a client can display a field-structured version without re-implementing template composition. `content_md` on a version may now be `null` where a body-less capsule was snapshotted faithfully — clients that assumed a string should fall back to `rendered_md`. Existing versions are unaffected: they backfill to an empty `field_values` map rather than to the capsule's current values. One caching note: a version detail response carrying a non-null `rendered_md` is composed against the Type's current schema and template, so it is served with `Cache-Control: private, no-cache` and a body-derived `ETag` instead of the immutable contract. `If-None-Match` still yields a 304; a response without `rendered_md` keeps the immutable contract unchanged. Backed by issue #1246.
- Additivepreview
API-key create and rotate accept an Idempotency-Key so a retry is safe
`POST /api/preview/user/api-keys` and `POST /api/preview/user/api-keys/{id}/rotate` now accept an optional `Idempotency-Key` request header (any client-chosen string, 1–128 characters). Both endpoints return a secret shown exactly once, so a lost response used to leave a client with no safe move: retrying create minted a second key whose predecessor's secret was never seen, and retrying rotate rotated again, discarding the first new secret. With the header, a retry that reuses the SAME key replays the ORIGINAL response instead of re-running the operation — same status, same body, same one-time secret — and every reply carries `Idempotency-Replay: true|false` so a caller can tell a replay from a fresh execution. Keys are scoped per user and per operation, and are retained for 24 hours; after that a reused key simply executes as a new request. Only successful (2xx) responses are recorded, so retrying after an error re-runs the operation as before. Three new error responses are possible only when the header is sent: 400 `IDEMPOTENCY_KEY_INVALID` (blank or over-long key), 409 `IDEMPOTENT_REPLAY_IN_FLIGHT` with `Retry-After` (the original request is still running — retry shortly), and 422 `IDEMPOTENCY_KEY_REUSED` (the key was already used with a DIFFERENT request payload, which is a client bug rather than a replay). Fully backward-compatible: omitting the header preserves today's behaviour byte-for-byte, and nothing is recorded.
- Additivepreview
Types gain a per-Type cap on total field-value content
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept an optional request field `field_content_max` (integer, floor 0, ceiling 90000, default 90000) — the fields-side companion to `content_md_max`. It caps the SUMMED length of one write's `field_values`, in UTF-16 code units: one capsule's field values, or one log entry's. Field values had no aggregate bound before; the only limits were per-field rules that apply solely to fields declaring one. This matters most where fields are the ONLY content channel — every `log` Type (an entry has no body) and any Type running `content_md_max: 0`. A write over the cap is rejected with a 422 on `POST /api/preview/capsules`, `PUT /api/preview/capsules/{id}`, `POST /api/preview/log-entries`, and `PATCH /api/preview/capsules/{id}/entries/{entryId}`. Lowering a Type's cap never strands stored content: on an update the effective ceiling is the greater of the cap and the row's current summed length, so an existing capsule or entry stays editable up to what it already holds — only a fresh create/append gets the plain cap. Value length is measured as the content itself (strings by length, numbers and booleans by their rendered form, lists and table rows summed over their items/cells); field and column names are not counted. `field_content_max` is also readable in a Type's context, and is settable through the MCP `create_type`/`update_type` tools as a 'Limits / guardrails' setting — so an AI caller may change it only when a person grants that class. Backward-compatible: omitting it keeps the default of 90000, which every existing Type is backfilled to, so no stored content becomes unwritable and no existing client is affected.
- Additivepreview
Types can derive a capsule's alias from a field, and text fields can require a slug shape
Two additions for capsules keyed by an external ID. A text field's `rules` may now carry `{ kind: "format", format: "slug" }`, rejecting any value that is not a valid alias (lowercase letters, digits, single hyphens; not purely numeric, UUID-shaped, or reserved; at most 60 characters) with a 422 field error. And a Type may set `slug_source` to the name of one of its declared `text` fields: capsules of that Type then take their alias from that field's value, normalized (trimmed + lowercased), so the retrieval alias can never diverge from the ID the field stores. On such a Type do not send an explicit `slug` — a value that disagrees with the derived one is a 422, and a derived-alias collision is a 409 (a capsule for that external ID already exists). `slug_source` is accepted only on a structured, non-encrypted, non-log Type and must name a declared `text` field. The rule travels inside `field_schema`; `slug_source` is a Type field of its own, optional on `POST /api/preview/types` and `PUT /api/preview/types/{id}` — and on MCP `create_type`/`update_type` — and returned on those routes' `201`/`200` responses, on `GET /api/preview/types/{id}`, and through MCP `get_type_context`. Purely additive: Types using neither behave exactly as before. Backed by issue #1199.
- Additivepreview
Type guidance can transclude capsules, and says which ones it resolved
A Type's `guidance` may now reference capsules with the usual `@type/slug` syntax, and those references are resolved and delivered alongside it instead of being left as inert text — so a rule that applies to several Types can live in one capsule rather than being copied into each Type's guidance. `GET /api/preview/capsules/context` gains an optional `guidance_references` field on its `200` response: one entry per reference, in the order the references appear, carrying `ref`, `capsule_id`, `title`, `content_md`, `available`, and a `reason` of `ok`, `unresolved`, `encrypted`, or `over_cap`. Guidance may reference at most 5 capsules and references are resolved one level deep; a referenced capsule's own references stay inert text. Encrypted capsules cannot be referenced — the server cannot read them — and Type create/update now rejects guidance that references an encrypted or non-existent capsule. Note for MCP clients: the `guidance_token` returned with a Type now covers the referenced capsules too, so editing a referenced capsule invalidates the tokens of every Type that references it; the existing 422 `guidance_token_required` response already tells the client to refetch via `get_type_context` and retry. Purely additive: guidance without references behaves exactly as before, and clients that ignore `guidance_references` keep working. Backed by issue #1198.
- Additivepreview
Response fields these routes already returned are now in the OpenAPI contract
Handler-level response-contract conformance testing surfaced several response fields that preview routes already emit on the wire but the OpenAPI document did not model, so generated clients could not see them. They are now documented (all optional, no behaviour change): `GET`/`POST`/`PUT /api/preview/types{,/{id}}` gain `content_bytes`, `guidance_hash`, `ai_settings_policy`, and `paragraph_rendering`, plus `guidance_token` and `guidance_token_status` on `POST /api/preview/types` (always) and `PUT /api/preview/types/{id}` (when the request edits guidance); `POST`/`PUT`/`GET /api/preview/projects{,/{id}}` gain `content_bytes`; `PUT /api/preview/organizations/{id}` documents the organization fields its response already spread (`description`, `icon`, `logo_url`, `allow_public_profiles`, `require_encryption`, `require_managed_account`, `billing_exempt`, `billing_grace_until`, `deletion_reservation_id`, `deletion_reserved_at`, `deletion_reserved_by`); `POST /api/preview/organizations/{id}/invitations` gains `invited_by`; `POST /api/preview/attachments/confirm` gains `deleted_at`, `unavailable_at`, and `unavailable_reason`; and `GET /api/preview/share/resolve` documents the capsule fields its allowlist already returns (`slug`, `content_excerpt`, `summary`, `language`, `is_prompt`, `prompt_meta`, `field_values`, `project_id`, `workspace_id`, `created_by`, `deleted_at`, `archived_at`). The documented success status of `POST /api/preview/capsules/{id}/duplicate` was also corrected to `201` to match the wire. Purely additive: no route behaviour changed and clients that ignore the new fields keep working. Backed by issue #1188.
- Additivepreview
Types choose how paragraph field values render
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept an optional `paragraph_rendering` field (`plain` | `markdown`, default `plain`). It controls how a Type's capsules display standalone `paragraph` field values on read: `plain` keeps the stored text literal (today's behaviour), `markdown` renders it as sanitized Markdown — bold, inline and fenced code — on capsule detail, previews, log-entry streams, and public share pages. Embedded HTML is sanitized and never executes. Values interpolated into a Type's `output_template` already rendered as Markdown and are unaffected, as are capsule create/edit inputs and table cells. Reads echo the stored value on `GET /api/preview/types/{id}`. Additive and backward-compatible: the field is optional and existing Types backfill to `plain`. Backed by issue #1034.
- Additivepreview
Enabling a public profile without a username answers with a machine-readable code
`PATCH /api/preview/profile/visibility` still refuses `public_profile_enabled: true` on an account that has not claimed a username with 409 Conflict and the same `detail` text; the response now also carries `code: "username_required"`. Clients can key off the code to send the user straight to the username claim form instead of matching on the prose. Additive and backward-compatible: status and `detail` are unchanged, no other enable-time refusal (unverified email, organization policy) gained a code, and clients that ignore `code` keep working. Backed by pull request #1148.
- Additivepreview
Capsules can be duplicated in one call
`POST /api/preview/capsules/{id}/duplicate` creates a copy of a capsule you can read, in the same workspace, Type, and project, owned by you, and answers 201 with the same body shape as `POST /api/preview/capsules`. Copied: title (suffixed `(copy)`), content, field values, tags, categories, project and collection membership, attachments, and gallery images. Reset on the copy: reference id, alias, share links, visibility, and all counters — a duplicate starts unshared and unviewed. A failure part way through never leaves a half-built capsule: every database row is written in one transaction and rolls back together. Object-storage copies are made outside that transaction and are removed by a best-effort compensating delete, so an unsuccessful duplicate leaves nothing you can reach through the API, though a failed cleanup can leave unreferenced stored objects behind. Requires the `create` scope for API-key and MCP callers, on top of read access to the source. Refusals carry a machine-readable `code`: 422 for a source that cannot be duplicated (`capsule_is_encrypted`, `capsule_is_log`, `capsule_flagged`), 413 when the copy would exceed a storage quota, 400 for attachment limits, 403 at the capsule limit. Purely additive: no existing endpoint changed. Backed by issue #1123.
- Cosmeticpreview
Every preview response body is now described in the OpenAPI contract
The OpenAPI document now carries a JSON schema for the success response of every documented `/api/preview/*` operation — previously only three responses were modeled and the rest shipped as schema-less stubs, so generated clients typed those responses as `unknown` and the api-changelog tooling was blind to response-body changes. Two public preview reads that were missing from the document entirely are now present: `GET /api/preview/capsules/by-ref` and `GET /api/preview/capsules/context`. Documented success statuses were corrected to match the wire: resource creates (capsule, type, project, collection, share, API key, invitation, attachment confirm) are `201`, the legacy collection-membership shims are `308` redirects, and organization create is the billing-gated `402`. Six fields have also been dropped from the documented `200` bodies of `PUT /api/preview/capsules/{id}` and `POST /api/preview/capsules/{id}/duplicate` — `share_token`, `share_url`, `share_verification`, `browser_url`, `partial` and `attachment_error`. Those routes have never emitted them: the document was previously wired to the MCP `update_capsule` tool's output schema, and the MCP handler adds those fields client-side (`browser_url` is derived from the client's base URL; the share fields come from a separate share-create call). Removing them corrects the document rather than changing the API. MCP tool output is unaffected. Separately, the documented `maxLength` on the `content_level`, `reason` and `truncatedBy` fields of the reference/backlink page responses narrows from 128 to 64, so those responses now reuse the same delivery-envelope definition the search and context reads already publish; every value those fields can carry is a short fixed keyword, so nothing valid is excluded. This is a documentation-only change — no request or response actually changed on the wire, so existing integrations are unaffected; callers using generated REST types gain accurate response shapes, and anyone who had been reading those six fields off the generated `updateCapsule` type was reading fields the REST response never carried. Backed by issue #1068.
- Additivepreview
Types support a freeform field mode with per-field-type guardrails
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept three optional fields selecting an alternative field-authoring mode. `field_mode` (`structured` | `freeform`, default `structured`) switches a Type from named-slot fields to freeform: in freeform mode `freeform_schema` declares which field types an AI/MCP caller may use (short text / paragraph / list) plus per-type caps (max words / max items / max length), and `field_count_min` (paired with the existing `field_count_max`) bounds how many fields a capsule must provide — the caller names and counts the fields within those guardrails, and templating is disabled. `field_mode`, `freeform_schema`, and `field_count_min` join the AI-protected `limits` settings class, so an AI/MCP caller cannot switch a Type to freeform or widen its bounds. Freeform capsule fields are synthesized on read and included in capsule detail, previews, and public shares (caps/policies never leak). Additive and backward-compatible: every field is optional and existing Types backfill to `structured` with an empty policy and a zero floor. Backed by issue #1036.
- Additivepreview
Types carry a per-Type AI settings-permission policy; MCP can author field schemas
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept an optional `ai_settings_policy` (`{ allow_all: boolean, allowed_classes: string[] }`) controlling which classes of a Type's settings an AI/MCP caller may change (classes: `limits`, `structure`, `identity`, `lifecycle`, `behavior`). When omitted on create, the server materializes an origin default — allow-all for AI/MCP creators, all-classes-except-`limits` for human creators — and persists it. The policy is human-controlled only: an AI/MCP caller that sends `ai_settings_policy` is rejected (403), and an AI/MCP update touching a denied setting class (or the permanently human-only `is_encrypted`) is rejected with a clear reason. The Type read payload and `get_type_context` add a derived `ai_settings_permissions` (`{ allow_all, allowed_classes, denied_classes, statement }`) stating which classes AI may and may not change. Field-schema authoring (`field_schema`, `output_template`) and the per-Type limits (`content_md_max`, `field_count_max`, `guidance_max`) — already accepted on the HTTP surface — are now reachable through the MCP `create_type` / `update_type` tools, governed by the policy above. Additive and backward-compatible: every field is optional and existing Types backfill to the human-created default. Backed by issue #1093.
- Additivepreview
Capsule and Type reads advertise their log binding
Capsule responses — `POST /api/preview/capsules`, `GET` and `PUT /api/preview/capsules/{id}`, and `GET /api/preview/capsules/by-ref` — carry a new optional `log` object when the capsule owns a log or is one: `role` (`owner` | `log`), `bound_type` (the log Type's `id`, `name` and `slug`), `owned_log_ref` (the owned log's ref, or null until a first append materializes it) and `append_via` (the exact append call to make). Previously an owner-bound log was invisible at the point of use: the response carried `owner_capsule_id: null` — null on an owner by design — and nothing about the Type's binding, so a caller could conclude no log existed. Type reads — `GET /api/preview/types/{id}` and `GET /api/preview/types` (full detail) — resolve the binding alongside the raw `log_target_type_id` as `log_target: { name, slug } | null`. Additive and backward-compatible: `log` is omitted entirely for capsules with no log role, so the standard capsule shape is unchanged, and the lean list-summary Type branch is untouched. Backed by issue #1066.
- Additivepreview
Capsule reads return classification and prompt facets; context reads report grand totals
Capsule responses — `GET` and `PUT /api/preview/capsules/{id}` and `GET /api/preview/capsules/by-ref` — now carry the `tags` and `categories` chips (`{ slug, name }[]`) and the `is_prompt` / `prompt_name` facet. All four persisted and were accepted on write, but were absent from every read, so a client could set them and never read them back. Listings additionally carry `is_prompt` / `prompt_name`; the chips stay on single-capsule reads. `GET /api/preview/capsules/context` and `GET /api/preview/projects/{id}/context` add `total` — the grand total of matching capsules across all pages, distinct from the delivered page length, so a caller paging a large Type can report how many exist rather than how many arrived. A 400 for an unknown category slug now names the offending slugs and carries the full valid set as `valid_category_slugs`, so a client can self-correct in one hop. `GET /api/preview/share/resolve` now projects its capsule through an explicit allowlist: it keeps every field the share contract has always carried, and no longer leaks internal columns (`search_vector`, `content_text`, moderation/freshness state) that a row spread had been emitting incidentally. Those columns were never documented, never part of the share contract, and are not covered by the stability policy, so this is classified additive rather than breaking — every documented field is unchanged. Backed by issue #1052.
- Additivepreview
Types accept an optional default `log_entry_order`
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept an optional `log_entry_order` request field for log-format Types: `newest_first` (the default, matching today's behaviour) or `oldest_first`. It sets the order a fresh visit renders the log's entry stream in and seeds the Stream sort toggle; readers can still flip the order per session. Omitting the field leaves it unchanged (defaulting to `newest_first` on create). Additive and backward-compatible: a client that omits the field is unaffected. Backed by issue #1060.
- Additivepreview
Types can bind to a log Type via `log_target_type_id`
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept an optional `log_target_type_id` request field: the id of a log-format Type this Type's capsules keep their log in (owner-bound logs). A capsule in the bound Type then owns a dedicated log capsule, and callers append/read through the owner ref — `POST /api/preview/log-entries` and `GET /api/preview/capsules/{id}/entries` now accept an owner capsule, not only a log capsule. A Type is a log-format Type OR binds to one, never both; sending `null` clears the binding, omitting the field leaves it unchanged. Additive and backward-compatible: a client that omits the field is unaffected. Backed by issue #1056.
- Additivepreview
Types support an append-only `log` format
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept two optional request fields for log-format Types: `rollover` (`daily` | `weekly` | `monthly` | `manual`) and `log_timezone` (an IANA timezone that fixes the rollover boundary). A Type's `structure` is chosen when it is created and is fixed thereafter: it is accepted on `POST /api/preview/types` but not on `PUT` — a log Type keeps its entries in a dedicated table, so the body shape cannot change after creation. Two new endpoints append and read entries — `POST /api/preview/log-entries` and `GET /api/preview/capsules/{id}/entries` — with `PATCH`/`DELETE /api/preview/capsules/{id}/entries/{entryId}` to edit and soft-delete. All request shapes stay backward-compatible: omitting the new fields preserves prior behaviour, and no existing fields or response shapes change. Backed by issue #1050.
- Additivepreview
Capsules accept optional first-class `prompt_meta`
`PUT /api/preview/capsules/{id}` now accepts an optional `prompt_meta` request field carrying a capsule's prompt metadata as first-class data: `models` (slugs from the curated registry served by `GET /api/preview/models`), `howToUse`, `expectedOutput`, and `editPrompt`. Previously this metadata could only be inferred from a Type's structured `field_values`; it is now stored on the capsule itself, independent of the Type's field schema, and drives the public prompt share page. Sending `null` clears the blob; omitting the field leaves it unchanged. Additive and backward-compatible: a client that omits the field is unaffected. Backed by issue #1035.
- Additivepreview
Capsule bodies become the source of truth; `content_md` nullable, new `token_bindings`
`PUT /api/preview/capsules/{id}` now accepts an optional `token_bindings` request field — a per-token map classifying each `{{token}}` in the body as field-bound (resolved server-side from `field_values`) or open (a caller-supplied prompt argument). It is web-editor-facing: an MCP/AI caller that sends it is rejected with 400. Relatedly, `content_md` is now nullable: a capsule that has never written a body BORROWS its Type's `output_template` live (its read `content_md` is `null`), and sending `content_md: null` on this route un-forks an authored capsule back to borrowing. The `content` envelope on capsule reads gains two fields: `source` (`"authored"` | `"borrowed"`) naming where the effective body came from, and `bindings` (the per-token binding map, present for `markdown`/`composed` bodies). Additive and backward-compatible: a client that only sends a string `content_md` and ignores the new fields is unaffected. Backed by issue #998.
- Additivepreview
`output_template` replaces `field_body_template`; capsules gain a `content` envelope
Type write routes (`POST /api/preview/types`, `PUT /api/preview/types/{id}`) now accept `output_template` — the renamed body template for a field-structured Type (a fielded capsule is not always a prompt, so the name is generic). The old `field_body_template` is retained as a DEPRECATED input alias: both are accepted and `output_template` wins if both are sent, so existing clients keep working; the alias will be removed in a future version. Capsule reads (`GET /api/preview/capsules/{id}`, `/api/preview/capsules/by-ref`, and the MCP get_capsule / get_capsule_by_ref tools) additionally return a `content` envelope `{ format: "markdown" | "composed" | "structured", text?, fields? }` whose `format` names the single representation a consumer should use. The flat `content_md` / `content_rendered` / `field_values` fields are unchanged and remain for backward compatibility. Additive and backward-compatible: existing requests and responses are unaffected.
- Additivepreview
Types gain a per-Type Max guidance length cap
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept an optional request field `guidance_max` (integer, floor 0, ceiling 5000, default 5000) — the sibling of `content_md_max` — capping how long a Type's own `guidance` may be. A guidance write longer than the effective cap is rejected with a 422; lowering the cap never rewrites existing guidance. `guidance_max` is also surfaced read-only in a Type's context (the AI reads it to self-limit) but is not settable through the MCP write tools. Backward-compatible: omitting it keeps the default of 5000 (the existing hard cap), so existing Types and clients are unaffected. Backed by issue #904.
- Cosmeticpreview
Field interpolation tokens move to double braces `{{field_name}}`
Read-time field interpolation in capsule bodies and Type body templates now uses double-brace `{{field_name}}` tokens instead of single-brace `{field_name}`. A lone `{` is henceforth always a literal character, so prose and code no longer risk having a stray `{name}` silently substituted. The `field_body_template` and `field_schema[].name` request/response shapes are unchanged — only the interpolation notation in their descriptions moves to double braces — and existing stored content was migrated so rendered output is identical. New body templates authored via `POST`/`PUT /api/preview/types` should use `{{field_name}}`. Backed by issue #980.
- Additivepreview
Per-Type maximum field count; Type guardrails locked for AI callers
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept a new optional request field `field_count_max` (integer, floor 0, ceiling 50, default 50) that caps how many typed fields a Type's `field_schema` may declare. It also appears on Type response bodies and, when tightened below the ceiling, in `get_type_context`. Separately, `PUT /api/preview/types/{id}` now rejects the content-bounding guardrail fields — `field_schema`, `field_body_template`, `content_md_max`, `field_count_max` — with 403 when the caller is an MCP/AI client, so an AI cannot loosen a human-authored Type's limits. Web sessions, the mobile app, personal API keys, and `create_type` are unaffected. Backed by issue #901.
- Additivepreview
Types gain optional icon and color
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept two optional request fields: `icon` — a glyph key from the closed icon library — and `color` — a tint-token key from the closed palette — which set a Type's visual identity. Both are backward-compatible: omitting them leaves the Type with its default glyph / slug-derived tint default, and no existing fields or response shapes change. Backed by issue #957.
- Additivepreview
Capsules can be exposed as prompts; Types gain cover fields
`POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` now accept an optional request field `is_prompt`, which exposes a capsule on the MCP `prompts` surface (its arguments come from the parent Type's fields). `POST /api/preview/types` and `PUT /api/preview/types/{id}` accept two optional request fields: `prompt_default` — the default `is_prompt` value applied to new capsules created under the Type — and `structure`, the Type's cover-page structure. All four request shapes stay backward-compatible: omitting any field preserves the prior behaviour (a capsule stays non-prompt unless its Type opts in), and no existing fields or response shapes change. Backed by issue #954.
- Additivepreview
Collection create/update accept an optional `alias`
`POST /api/preview/types/{id}/collections` and `PATCH /api/preview/types/{id}/collections/{collectionId}` now accept an optional request field `alias` — a tenant-global shorthand that addresses a collection by `@alias` alone, without naming its parent Type, from MCP and CLI. An alias is a lowercase text token (max 60 chars, not purely numeric) and is unique per tenant (personal or workspace), unlike a collection's per-Type `slug`. Real Type slugs always take precedence: an alias that would shadow an existing Type slug is rejected, and resolution tries Type slugs first so a Type can never be shadowed by a collection alias. Renaming or removing an alias leaves a tombstone, so a bare `@alias` already written elsewhere keeps resolving to its original collection and never silently repoints. Both are additive and optional: omitting `alias` on create leaves the collection alias-less, an omitted `alias` on update leaves the stored value untouched (PATCH semantics), and passing `null` clears it. A taken alias or a Type-slug shadow returns `409`. No existing fields or response shapes change. Backed by issue #316 (migration 147).
- Additivepreview
Workspaces accept an `icon` for switcher branding
`PUT /api/preview/organizations/{id}` accepts an optional, nullable request field `icon` — a workspace icon key from a closed set (`building`, `tower`, `briefcase`, `landmark`, `store`, `users`, `rocket`, `box`, `layers`, `globe`) shown in the workspace switcher. Purely additive with PATCH semantics: omit it to leave the stored icon unchanged, or send `null` to reset to the default (`building`). Editable by a workspace owner or editor. No existing fields or response shapes change (migration 145).
- Additivepreview
Types accept a `field_body_template` for field-structured capsules
`POST /api/preview/types` and `PUT /api/preview/types/{id}` accept an optional request field `field_body_template` (string, max 10000) — a capsule body template for a field-structured Type. New capsules of the Type seed their `content_md` from it, and any `{field_name}` tokens render from the capsule's `field_values` at read time. Purely additive and optional: omit it (or send `""`) and the body is derived from the values as before; on update, an omitted field leaves the stored value untouched (PATCH semantics). No existing fields or response shapes change. Follow-up to the typed-fields work (issue #868, migration 144).
- Additivepreview
Types accept a structured `field_schema`; capsules accept `field_values`
Types can now declare optional structured fields with declarative validation. `POST /api/preview/types` and `PUT /api/preview/types/{id}` accept an optional request field `field_schema` — zero or more fields (`text` | `number` | `datetime` | `enum` | `reference`), each with an optional `required` flag (plus a custom `required_message`) and type-appropriate rules carrying per-rule messages. `POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` accept an optional `field_values` object that is validated against the owning Type's schema; all violations are returned together as RFC 9457 `application/problem+json` with an `errors[]` array. `PUT /api/preview/types/{id}` also accepts an optional `confirm_field_changes` flag: destructive schema edits (removing or renaming an enum value or field) return `409` with the required confirmation token and grandfather existing capsules once confirmed. All three fields are additive and optional — a Type with no `field_schema` behaves exactly as before, and on update an omitted field leaves the stored value untouched (PATCH semantics). Field-structured Types are mutually exclusive with end-to-end encryption (server-side validation needs plaintext). No existing fields or response shapes change. Backed by issue #868 (migration 143).
- Additivepreview
Publicly shared prompts are safety-classified
Capsules shared with the `prompt` render template now have their prompt body classified for safety (low/medium/high). High-risk prompts are withheld on the public share page behind a recipient acknowledgment; medium shows a non-blocking caution; low renders unchanged. Classification is best-effort and fails open — sharing and viewing always work even when the classifier is disabled or unavailable. A new GET /api/cron/classify-shared-prompts endpoint (Vercel cron, `Authorization: Bearer $CRON_SECRET`) backfills verdicts. No existing request/response shapes change. Backed by issue #856 (migration 143).
- Additivepreview
Workspaces accept a `logo_url` for switcher branding
`PUT /api/preview/organizations/{id}` accepts an optional, nullable request field `logo_url` — a SAME-ORIGIN workspace-logo path (the canonical serve shape `/api/preview/organizations/{id}/logo/object/{uuid}.{ext}`, png | jpg | webp, at most 256 chars) shown in the workspace switcher. The pattern admits only that anchored path — no scheme, no leading `//`, no traversal, no off-site URL — and the handler re-validates the referenced object against R2 before persisting a server-reconstructed canonical URL, never the raw client string. Purely additive with PATCH semantics: a logo-only save sends just `logo_url` (so it can never overwrite a name changed elsewhere), omitting it leaves the stored logo unchanged, and `null` removes it (revert to the monogram). Editable by a workspace owner or editor. No existing fields or response shapes change. Backed by issue #860; the `logo_url` column ships in migration 140 (added by #822).
- Additivepreview
Capsule write responses surface partial attachment failures
`POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` may now return two optional `200` response fields: `partial` (boolean) and `attachment_error` (string). When a capsule's fields are written successfully but its attachments fail validation, the response sets `partial: true` and carries the validation detail in `attachment_error`, while the capsule fields above remain authoritative — so a client consuming only the structured response can detect the partial outcome instead of mistaking it for a clean write. Purely additive: clients that ignore the new fields are unaffected.
- Additivepreview
Capsule create/update accept an optional `slug` alias
`POST /api/preview/capsules` and `PUT /api/preview/capsules/{id}` now accept an optional request field `slug` — a human-readable text alias that resolves to the same capsule as its permanent numeric ref (`@type/<slug>` works alongside `@type/<N>`). A slug is a lowercase alias (max 60 chars, not purely numeric) and is unique per (type, owner); renaming or removing one leaves a tombstone so `@type/<slug>` references already written elsewhere keep resolving. Both are additive and optional: omitting `slug` on create leaves the capsule alias-less, an omitted `slug` on update leaves the stored alias untouched (PATCH semantics), and passing `null` clears it. No existing fields or response shapes change. Backed by issue #715 (migration 136).
- Additivepreview
Type create/update accept `package_as` and `format`
`POST /api/preview/types` and `PUT /api/preview/types/{id}` now accept two optional request fields: `package_as` (`standard` | `skill`) and `format`. `package_as` controls how a Type's capsules are packaged — setting it to `skill` packages the Type (and every capsule in it) as a Claude Code skill, while `standard` keeps the default behavior. `format` records the export format. Both are additive and optional: omitting them leaves existing behavior unchanged, and on PUT an omitted field leaves the stored column untouched (PATCH semantics). No existing fields or response shapes change. Backed by the skill-packaging work (migration 133).
- Cosmeticpreview
Rapid consecutive saves now always record a version
The 60-second revision interval throttle has been removed. Previously, a same-author save landing less than 60 seconds after the prior version skipped capturing a new version row (the capsule/Type UPDATE still applied), and the PUT capsule/Type and restore responses could carry `version_skip_reason: "interval_throttle"`. Now every explicit save with changed content records a version, so `interval_throttle` is no longer emitted and is removed from the set of possible `version_skip_reason` values. No response shape change — this is a value-set narrowing. No-op saves still skip with `identical_content`, and the size, rate, quota, and capsule-cap gates are unchanged. Backed by issue #667.
- Additivepreview
Workspace scoping for personal API keys
POST /api/preview/user/api-keys accepts a new optional request field `workspaceIds` (array of strings). Each entry is either the literal "personal" or an organization-workspace UUID the caller can access. When present and non-empty the issued key's access is restricted to exactly those workspaces (intersected with the caller's live membership at request time); omitting the field or passing an empty array issues an unrestricted key with the same reach as before. Organization keys (POST /api/preview/organizations/{id}/keys) are already hard-scoped to their organization and are unaffected. Existing clients are unaffected. Backed by issue #597.
- Additivepreview
Per-Type maximum capsule content length
POST /api/preview/types and PUT /api/preview/types/{id} accept a new optional request field `content_md_max` (integer, floor 0, ceiling 90000, default 20000). It sets a per-Type cap on capsule `content_md` length that overrides the global per-capsule limit; capsule create/update reject bodies over the effective cap. Lowering a Type's cap grandfathers existing over-cap capsules (they remain editable but cannot grow). A cap of 0 makes the Type title-only. The field also appears on Type response bodies, and GET /api/preview/capsules/{id} now carries the denormalized `type_content_md_max` so editors can render a live character counter. Existing clients are unaffected — omitting the field preserves the previous behaviour. Backed by issue #601.
- Additivepreview
update_type MCP tool for editing Type metadata
The MCP server now exposes an `update_type` tool — a thin client over the existing PUT /api/preview/types/{id} endpoint. AI assistants can correct a Type's name, guidance, summary, freshness thresholds (stale_after_days / expire_after_days), orphan-warning visibility, and default project without a human opening the web UI; previously only create_type was available. PATCH semantics — only supplied fields change. The slug stays immutable and the one-way encryption flip is intentionally not exposed. The tool forwards an optional updated_at optimistic-concurrency token (mapped to the endpoint's client_updated_at) so a concurrent edit returns a conflict instead of silently overwriting, and guidance/summary edits are captured in the Type's version history. No REST contract change — the underlying endpoint is unchanged; this publishes the new MCP tool surface. Backed by issue #536.
- Additivepreview
Per-Type opt-out for the orphan-capsules banner
POST /api/preview/types and PUT /api/preview/types/{id} accept a new optional request field `hide_orphan_warning` (boolean, default false). When true, the Type's detail page suppresses the orphan-capsules banner that flags capsules with zero inbound references. The field also appears on Type response bodies. Existing clients are unaffected — omitting the field preserves the previous behaviour (warning shown).
- Additivepreview
Capsule + type version history with quota gating
Append-only snapshot history shipped for non-encrypted capsules and types. New endpoints: GET /api/preview/(capsules|types)/{id}/versions (cursor-paginated, newest-first), GET /api/preview/(capsules|types)/{id}/versions/{versionNum}, POST /api/preview/(capsules|types)/{id}/versions/{versionNum}/restore (append-only — captures the pre-restore state as a new version), and DELETE /api/preview/(capsules|types)/{id}/versions/{versionNum} for manual entries. Encrypted capsules/types never produce a version (storage-layer enforcement: record trigger, encryption-toggle purge trigger, and BEFORE INSERT reject trigger on the version tables). Storage is over-counted and tracked per-user; at 85% of plan storage the version write is skipped — the underlying capsule/type UPDATE still succeeds. PUT /api/preview/capsules/{id} and PUT /api/preview/types/{id} responses now always carry `version_skipped` (boolean) and `version_skip_reason` ("quota" | "encryption" | null) — additive, existing fields unchanged. 30-day hard retention enforced by a 6-hourly prune cron; nightly reconciliation auto-corrects counter drift above 1%. Operational notes, accounting formula, and rollback SQL in docs/versioning/README.md.
- Additivepreview
GET /api/preview/types/{id} for read-side parity
Single-type read endpoint added at GET /api/preview/types/{id}, closing the CRUD gap with capsules and projects. Callers holding a type ID (returned from createType, embedded in a capsule payload, or surfaced via list_types) can now fetch the full record without listing all types and filtering client-side. The response shape mirrors items in GET /api/preview/types — including the joined workspace_name, capsule_count, and last_capsule_at fields. Backed by issue #417.
- Additivepreview
Canonical collection-membership endpoints
Adding and removing capsules from a collection now lives at POST /api/preview/collections/{id}/capsules and DELETE /api/preview/collections/{id}/capsules. The server resolves the parent type and workspace from the collection ID alone — closing a class of cross-workspace scope drift that surfaced as 400 "Invalid collection ID" against IDs that round-tripped through list_collections. The legacy type-scoped paths (POST | DELETE /api/preview/types/{id}/collections/{collectionId}/capsules) keep working for one release cycle via a 308 Permanent Redirect (preserves method + body) and are now marked deprecated in the OpenAPI document so codegen flags them. Their summary text was updated in this release; no behavior change beyond the redirect. Bug fix #423.
- Additivepreview
Read-side parity and defense-in-depth gates for preview API
30 read- and lifecycle-side operations are now part of the published OpenAPI surface, bringing the preview spec to read/write parity with the implemented routes. New operations include capsule list/get/delete/archive plus batch archive/delete/move; capsule backlinks, graph, and references reads; project list/get/delete and capsule add/remove plus project context; type list/delete and collection list/get/delete plus collection capsule add/remove; share list/resolve/revoke; search; user profile; workspaces; and attachment delete. The underlying handlers were already shipping — this change publishes them in the contract so Scalar, generated openapi-fetch / Dart clients, and Postman pick them up. Backed by defense-in-depth public-surface gates that fail closed when an operation is referenced without an explicit allowlist entry.
- Additivepreview
Typed 200-response schemas for capsule and share writes
POST /api/preview/capsules, PUT /api/preview/capsules/{id}, and POST /api/preview/share now publish typed JSON-Schema response bodies in the OpenAPI document. New response fields documented (capsule writes: id, ref_id, title, type_id, browser_url, share_token, share_url, share_verification, attachments; share create: share_token, share_url, browser_url, share_verification). Wire shape is unchanged — the server has always emitted these fields; only the contract surface (consumed by Scalar, generated openapi-fetch, and Postman) is new. Clients can now codegen typed response models instead of hand-rolling them. Backed by Zod modules under @symbol/shared/contracts/output/*.
- Additivepreview
Preview API launched
Symbol's public HTTP API is now available under /api/preview/* on the free preview tier. The full surface — capsules, types, projects, organizations, share, collections, invitations, attachments, mentions, references, library, search, workspaces, billing (non-webhook), GDPR, and support — ships with a published OpenAPI 3.1 document, RFC 9457 Problem Details errors, generated TypeScript and Dart clients, and an MCP server. Subsequent changes will appear here, classified breaking / additive / cosmetic. Preview gives no SLA and no paid tier; v1 will follow once the surface stabilises.