Drafts and publishing
Publishing a card, reading only what was published, knowing what changed since — and the webhooks a site listens to.
Every card is a draft until someone publishes it. Publishing copies what the card says — title, icon, cover, properties, body — into its published version. Edits made afterwards stay in the draft: a site reading the published version keeps serving the old text until the card is published again.
Nothing changes for a client that does not ask. Without a version, every read
returns drafts, exactly as before.
Publishing and unpublishing
PUT /workspaces/{workspaceId}/items/{itemId}/publication
DELETE /workspaces/{workspaceId}/items/{itemId}/publicationBoth return the card (ItemDetail) with its publication fields up to date. In
the app, the card's header shows its state and a Publish button;
Unpublish is in the card's ⋯ menu.
- Publishing with nothing new does nothing. No write, no event, no webhook. A double click, a script run twice or an app that republishes everything to be safe costs nothing and wakes no one.
- Unpublishing removes the published version. The card becomes a draft again, its content untouched.
- An archived card cannot be published (409): it is in no list, so there is nothing a reader could see.
- Publishing is a write: it takes the
memberrole, like editing. For an installed app it is a scope of its own,items:publish—items:writeis not enough. An app that translates or corrects drafts should not be able to put them online.
Reading only what was published
Add "version": "published" next to the definition — not inside it. A saved
view serves both your editors (the draft) and your site (the published
version), so the version says who is reading, not what the view selects.
POST /workspaces/{workspaceId}/collections/{collectionId}/query{
"definition": {
"filter": { "op": "eq", "field": { "fieldId": "<slugFieldId>" }, "value": "hello-world" },
"sort": [{ "field": { "fieldId": "sys:order" }, "direction": "asc" }],
"includeSecondaryLists": false
},
"version": "published",
"limit": 1
}The same field works on grouped-query and group-counts. For a single card:
GET /workspaces/{workspaceId}/items/{itemId}?version=publishedIt returns a 404 if the card is not published or is archived — a site does not need to know which.
What a published read returns:
- only published cards — a card that was never published does not exist for it;
- what each card said when it was published: title, icon, cover, properties and body come from the published version. Filters and sorts apply to those values, so a slug you changed in the draft does not match until you republish;
- where the card is filed today: list, secondary lists, parent, manual order and status are read from the card as it is now. Moving a card or reordering a list does not require republishing;
updatedAtis the time the version was published.
A cursor taken on a draft read is refused on a published read, and vice versa — the two do not list the same cards.
Draft, published, changed
Every Item carries two optional fields:
publishedAt | changedSincePublication | State |
|---|---|---|
null | false | draft — never published, or unpublished |
| a date | false | published, up to date |
| a date | true | published, and the draft has changed since |
"Changed" means the content changed: title, icon, cover, properties or body. Changing the status, moving the card or reordering it does not count — none of that changes what the site shows.
To filter or group by state, use the system field sys:publication, whose
values are draft, published and changed. A card that is changed is
still online: "everything published" is
{ "op": "in", "field": { "fieldId": "sys:publication" }, "values": ["published", "changed"] }.
Two fields, not an enum
The state is published as a date and a boolean rather than a three-value
state, on purpose: strict decoders (the iOS client is one) fail the whole
response on an enum value they do not know. Both fields are optional — absent
means "not said here", not "draft".
Webhooks
Listen to item.published and item.unpublished, not item.updated: the
editor saves the draft every second or so while someone types, and none of that
changes what your site serves.
item.published carries publishedAt (before / after) and, on a republish,
what the new version changed compared with the previous one — a slug before and
after is the old URL to invalidate. The body itself does not travel; only the
fact that it changed (bodyChanged). A first publication carries publishedAt
alone.
What it is not
There is no scheduled publishing, no version history and no publish-many route, and a key that can read a workspace can read its drafts. See limits.