Authoring: everything a documentation space can do

Embed
Preview of the card
<a href="https://forumulate.com/guides/authoring-reference"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/462e2256-d0dd-4d85-b693-2ecda70f7eca/social-card.svg?v=3e7ae33eec8b" alt="Authoring: everything a documentation space can do" width="600" height="315"></a>

A reference for the features that exist and have had no guide. Each section says what the thing is, which calls do it, and the one constraint that is not obvious from the endpoint name.

Creating a space

See Creating a space for seed, shape and preset.

Importing

See Importing documentation. A repository sync records each page's source file in metadata.sourcePath, which an edit link's {path} uses.

Collections

POST GET          /orgs/{orgId}/spaces/{spaceId}/collections
GET PATCH DELETE  /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}
POST              /orgs/{orgId}/spaces/{spaceId}/collections/reorder
POST              /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}/pages

A collection is the group a page is filed under and the heading the sidebar draws. POST takes a name and derives the slug from it when you send none.

A collection's slug is a public address — …/c/{slug} is a reader page — so re-slugging one moves that address and writes the redirect for it. A page's own address does not change when you move it between collections, which is the point of filing pages this way. DELETE takes the collection's pages to the trash with it and says how many went: { "ok": true, "pages": 12 }.

How a collection sits in the rail: nav

POST and PATCH take a nav object. PATCH merges it into what is stored, so {"nav": {"collapsed": true}} keeps an icon you set earlier.

Key

What it does

collapsed

Starts the group closed when theme.docs.sidebar.groups is collapsible. A group holding the page being read is always open.

icon

An emoji, an icon name from GET /icons?for=page (book, code, …) or an image URL (any other bare word is a 422), drawn before the group's label.

linkHub

true: the group label opens the collection's landing page (…/c/{slug}) and the chevron folds the group. false: the label only folds it. Unset follows theme.docs.sidebar.groupLink (default: links).

hubCards

true: the landing page lists its pages as cards; false: as a list. Unset follows theme.docs.hub.layout (default: cards).

navScope

isolated: inside this collection the rail shows only it, with an "All docs" row back. shared (default) shows every group.

backStyle

With navScope: "isolated": title draws the way back as ONE centred row, "‹ {the collection's name}", linking to the parent scope; link (default) keeps the "All docs" row above the collection's title.

PATCH also takes null for any nav key, which removes it so the collection follows the theme again: {"nav": {"hubCards": null, "linkHub": null}}.

The collection's landing page: description, coverUrl, seo

Every collection has a landing page at …/c/{slug}: its name, a trail back to the space (and its section), its pages with their summaries, last-updated dates and reading times, the pages nested under them as sub-groups, and previous/next. POST and PATCH take three fields that shape it:

Field

What it does

description

One or two sentences (≤ 500). The subtitle under the title, the card text on the space's landing page, and the meta description. null clears it.

coverUrl

An image URL or an upload path. A banner above the title, and the page's social image. null clears it.

seo

{ "title"?, "description"? } — overrides for the search result and link previews only. Replaced whole; {} clears both.

The longer introduction under the title is the collection's hub intro (PUT …/hubs/collection/{slug}/intro, or the studio's Intro button). The studio's gear button beside each collection edits all of the above.

A landing page with one page, no description and no introduction says nothing a link to that page would not, so it is noindex and left out of the sitemap; every other landing page is in the space's sitemap.xml, carries a CollectionPage + ItemList + BreadcrumbList description for crawlers, and has its own social card (…/c/{slug}/social-card.png).

Filing a page under another: append, not prepend

POST …/pages/{pageId}/reorder with a new parentId (or collectionId) and no afterId now appends the page after its new siblings. It used to put it first, so a script that created pages and then filed each under a parent got them in reverse. afterId: null still means "first"; omitting afterId with the parent unchanged leaves the page where it is. Pages created at the same moment under one parent are serialised, so they keep the order they were created in.

Reordering is a move, not a replace

POST /orgs/{orgId}/spaces/{spaceId}/collections/reorder
{ "ids": ["<collectionId>", …] }      → { "ok": true }

A partial list is legal, and is the useful case. The ids you name are written to the front of the space's order, in the order you gave them. Every collection you did not name keeps its place behind them, in the order it already had — their positions relative to each other do not change. So {"ids": ["<one id>"]} promotes one collection to first and disturbs nothing else, and moving one thing is never a reason to restate the rest. Maximum 200 ids per call.

Two bodies are refused, both 422 validation.failed, with the ids at fault in error.details.ids: an id no collection in this space has, and the same id listed twice. Those used to be dropped from the list instead, so a reorder with a typo in it answered { "ok": true } and left the order exactly as it was, leaving a caller no way to tell a reorder that worked from one that was ignored (Crossly E-63). A reorder is not a filter: if the list does not describe this space, the right answer is to say so.

Versions

GET  POST       /orgs/{orgId}/spaces/{spaceId}/docs/versions
PATCH DELETE    /orgs/{orgId}/spaces/{spaceId}/docs/versions/{key}
POST            /orgs/{orgId}/spaces/{spaceId}/docs/versions/{key}/make-default
GET  PUT        /orgs/{orgId}/spaces/{spaceId}/docs/versioning

A docs space can publish several versions of itself, for example v1, v2 and next. Readers move between them with the version picker. default is the primary version, the one a bare address serves. In the console, the space's Settings fold has a Versions group that does everything below.

Each version lives at its own address:

Address

Serves

/p/acme/docs/install

install in the default version

/p/acme/docs/v2/install

install in v2

/p/acme/docs/v2

v2's home page (a redirect)

/p/acme/docs/v2/install/md

that page as Markdown

The same form works on a path mount (acme.com/docs/v2/install) and on a custom domain (docs.acme.com/v2/install). The switcher, the sidebar tree, previous and next, and the language switcher all link inside the version you are reading. An address that names a key the space does not have answers 404. The older ?version=<key> form still works, so links you already published keep working. Its canonical is the path form.

Creating a version copies every page. It is not a pointer. Each version is a real page tree, and pages share a family key so a reader can switch to the same page in another version instead of landing at the root. Plan how many versions you keep.

Why copies and not a live overlay. We considered a lighter model where a version stores only the pages that differ and falls back to the default for the rest. We did not build it, because a version is a promise that its text does not change. With an overlay, editing a page in the latest version would also change it in every older version that did not have its own copy, and readers of v1 would see v2 text with no notice. Copies cost storage. In exchange, each version keeps exactly the text it was published with.

A version key must match ^[a-z0-9][a-z0-9._-]{0,39}$: lowercase, up to 40 characters. Keys that are already address segments in a docs space are refused: c, me, tags, print, md, rss.xml, sitemap.xml, social-card.png and manifest.webmanifest. The default version's label comes from the space's defaultVersionLabel setting, or "Latest" when it is not set.

Creating a version never changes an existing page's id. The originals stay default and the copies get new ids. POST answers { "versions": […], "cloned": <pages copied> }.

Making another version the default. POST …/versions/{key}/make-default with { "retireAs": "v1" } moves that version's pages to the bare address. The outgoing default keeps its pages under retireAs, with its label (or retireLabel), badge and banner. Page ids do not change, so revisions, comments and links by id stay with the same text. The space's home page moves to the same page in the new default. Subscribers to version.default_changed are told.

PATCH …/docs/versions/{key} changes a version's label and reader extras. Keys merge, so a key you do not send keeps its value:

Key

Meaning

label

The name in the switcher. On default this is defaultVersionLabel.

badge

A short pill beside the label, up to 24 characters. "" clears it.

banner

unreleased or unmaintained puts a notice on every page of the version, with a link to the same page in default. none removes it.

hidden

true keeps the version out of the switcher. Its address still answers, so links you already published keep working. Not allowed on default.

archived

true retires the version. Its pages stay readable but leave the switcher and the sitemap, are marked noindex, and show the unmaintained notice unless the version has its own banner. Not allowed on default.

Canonicals and the sitemap. PUT …/docs/versioning with { "canonical": "self" | "latest" } decides how search engines see older versions:

  • self (the default): every version's page is its own page. Its canonical is its own /<version>/<slug> address, and the space sitemap lists it, except for hidden and archived versions.

  • latest: a page that also exists in the default version uses that page as its canonical, and other versions stay out of the sitemap. Use this when old versions are close copies that you want search engines to treat as the current page.

Listing pages by version. GET …/pages and GET …/pages/inventory carry versionKey and locale on every row and take ?version=<key>&locale=<code> to list one variant. Absent lists every variant, primary first among rows that share a slug. A key the space does not declare answers 422 naming the ones it does.

Locales

GET  POST   /orgs/{orgId}/spaces/{spaceId}/docs/locales
PATCH DELETE /orgs/{orgId}/spaces/{spaceId}/docs/locales/{code}
POST        /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/ai/translate
GET PUT DELETE /orgs/{orgId}/translation-overrides
GET         /public/orgs/{orgId}/i18n/{locale}

The same mechanism as versions, keyed by locale. …/ai/translate fills a locale from the primary using your own AI key (see AI features and bring-your-own-key); the number of locales you may add is a plan quota.

Every page gets a twin in every declared language. Adding a language clones the pages that exist then, and a page created afterwards — through the pages API, /items, an import or MCP — gets an unpublished twin for each declared language at creation, so a later page is as translatable as an earlier one. A twin can also be created explicitly with locale on the page-create door: POST …/collections/{id}/pages with { "slug": "webhooks", "title": "Webhooks", "locale": "es" }. It is a draft until published, and it then appears in ?locale=es listings.

PATCH …/docs/locales/{code} with { "label": "Español" } renames a language as the reader's switcher shows it. {code} may be default: the source language is labelled "Original" until you name it ({ "label": "English" }). Only the label changes — the code is the translation's address and stays.

Language addresses

Every language has its own path. The language code goes right after the space's address, and the original language has no code:

Address shape

Original

Spanish

Path of your own domain

numestri.com/docs/webhooks

numestri.com/docs/es/webhooks

A space on a site

forumulate.com/guides/<page>

forumulate.com/guides/es/<page>

Our address

/p/<org>/<space>/<page>

/p/<org>/<space>/es/<page>

Blog, forum, and so on

/blog/<post>

/blog/es/<post>

Legal documents

/legal/<org>/terms

/legal/<org>/es/terms

Versions

/docs/v2/<page>

/docs/es/v2/<page>

The code is never at the root of your domain, because the root belongs to your own site and only the space's path is routed to us. Your reverse proxy or Traefik rule for /docs already covers /docs/es/…; nothing changes there.

  • Only a declared language is a path segment. A code you have not added with POST …/docs/locales is read as a page name, so a page called es or de keeps its address.

  • When a language and a page share a name, the language wins. If you add Spanish to a space that already has a page called es, then /docs/es is the Spanish front door. The page is still at /docs/es?locale=default, and every link, canonical and sitemap entry we write for it uses that address. default is our name for the original, so no language can take it. Renaming the page (with a redirect from the old name) is the cleaner fix.

  • The interface follows the address. /docs/es/… shows the navigation, "On this page", "Updated", the pager and the not-found page in Spanish too, when Spanish is an interface language. ?lang=<code> in the same address still picks the interface language. A reader's saved language preference does not change a path-addressed page, because the page is cached by its address.

  • Links stay in the language. The navigation, sidebar, breadcrumbs, previous/next, cards, search results and front-door redirects of a Spanish page all point at Spanish addresses. The language switcher and hreflang name every language.

  • Old ?locale= links still work. …/webhooks?locale=es, and ?lang=es when Spanish is a declared content language, answer 301 with …/es/webhooks, and every other parameter is kept. Your redirect rules also apply inside a language: a rule for /old answers /es/old with /es/new.

A page with no translation in a declared language answers 404 there, unless the space sets theme.docs.localeFallback: "source". Then the original is served under a "not translated yet" notice (see Theme a documentation space).

What search engines see

Signal

What we publish

<html lang>

The language of the page's words: the translation's code, or the space's declared language for the original.

rel=canonical

Each translation's own address, on your custom domain when the space has one.

<link rel=alternate hreflang>

One per published translation, the original under its own language, and x-default for the original.

og:locale, og:locale:alternate

The page's language and its published translations, written es, pt_BR.

JSON-LD inLanguage

The same language as <html lang>.

sitemap.xml

Every published translation at its own address, each with <xhtml:link rel="alternate" hreflang> for its translations and x-default.

A fallback (localeFallback: source)

Canonical to the original, and not in hreflang or the sitemap. It is not marked noindex: it is a copy of the original, which is what a canonical says. Putting noindex next to a canonical gives search engines two instructions that contradict each other.

Keep ?locale= instead. Set theme.i18n.urlStyle: "query" on the space, or on its site so every space in the site uses it (a space's own setting wins). Then translations are at …/<page>?locale=<code>, path segments are not read as languages, and canonicals, hreflang and the sitemap use the query form. "path" (or unset) is the default.

A page in a right-to-left language (ar, he, fa, ur …) is laid out right to left, whatever language the reader's interface is in; when the space itself declares a right-to-left language, the navigation and rail mirror too.

translation-overrides is for the interface strings, not the content — "Was this helpful?" in Portuguese.

Listing needs docs.read; creating or deleting needs docs.write.

Snippets (reusable content)

POST GET      /orgs/{orgId}/spaces/{spaceId}/reusable-content
PATCH DELETE  /orgs/{orgId}/spaces/{spaceId}/reusable-content/{id}
GET POST      /orgs/{orgId}/library/reusables
DELETE        /orgs/{orgId}/library/reusables/{id}

A named block document stored once and referenced from any number of pages by a reusableRef block — :::reusable{name="install-warning"} in markdown. It is resolved at render, so editing the snippet updates every page that uses it.

library/reusables is the organization-wide version, available in every space.

The name is unique per space, and re-creating an existing name is a 409 name.taken naming the snippet that holds it. Use PATCH to change one, or pick another name. (This guide said the opposite — a silent 200 with the old content — which the API has never done.)

Variables

GET PUT   /orgs/{orgId}/spaces/{spaceId}/variables
DELETE    /orgs/{orgId}/spaces/{spaceId}/variables/{key}
GET PUT   /orgs/{orgId}/library/variables
DELETE    /orgs/{orgId}/library/variables/{key}

Key/value pairs substituted at render, referenced by a variableRef inline node. The product name, the current release, the support address.

Each row reports its scope — org for one defined once for every space, space for this space's own. A space variable shadows an org one of the same name.

Variables are typed: string, number, boolean, url. The value is checked on write, and the messages are exact — That is not a number., Use true or false., A URL must start with http:// or https://.

A url is validated by parsing it, not by a regular expression, because https:/\evil.test passes a regex and fails a parser, and the parser is what a browser will use.

Glossary

GET PUT   /orgs/{orgId}/spaces/{spaceId}/glossary

Terms the reader sees underlined, with the definition on hover.

PUT replaces the whole list. There is no merge and no per-term endpoint, because a glossary is edited as a list and partial merges of a list are how entries get duplicated. Read, modify, write back the whole thing.

The server rewrites what you send: it trims, drops any row with an empty term or definition, deduplicates case-insensitively with last one winning, and returns the result sorted alphabetically. A round-trip will not match your payload byte for byte. Maximum 200 entries; term up to 80 characters, definition up to 500.

Redirects

GET PUT   /orgs/{orgId}/spaces/{spaceId}/redirects
PUT       /orgs/{orgId}/spaces/{spaceId}/domains/{domainId}/redirect

Covered in Moving an existing site onto Forumulate. Whole-list replace, 500 maximum. from is a path; to may be a path or an absolute https:// URL, because a redirect target may legitimately leave the space.

Either side may end in *: from then matches everything below it, and a to ending in * carries the matched remainder (/old/* → /new/* sends /old/a/b to /new/a/b). The longest matching wildcard wins and an exact rule always beats a wildcard.

A rule may also set status (301/302/307/308, default 308), match.query (exact query values the request must carry), passQuery (carry or drop the incoming query) and type: "rewrite" (serve another path of the same space without changing the URL). See Status codes, query strings and rewrites.

Rules we write ourselves — when a page is renamed, or a collection is re-slugged — are kept when you PUT the table back, unless your list claims the same from.

Deleting and restoring

DELETE    /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}
DELETE    /orgs/{orgId}/spaces/{spaceId}/pages            (ids or collectionId)
GET       /orgs/{orgId}/spaces/{spaceId}/pages/trash
POST      /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/restore
DELETE    /orgs/{orgId}/spaces/{spaceId}/pages/trash      (purge)

A delete is a move to the trash, and restoring is a single call. Two things it does that are not obvious:

The children of a deleted page come up a level. A live page hanging off a deleted one is reachable by URL and in no navigation tree, so each child is re-parented to the deleted page's own parent — the top level if it had none. The same applies to the bulk door.

Restoring puts them back. The response says how many:

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/restore

→ { "ok": true, "slug": "install", "renamedFrom": null,
    "detached": false, "reattachedChildren": 3 }
  • reattachedChildren — children the delete had moved up, now back under this page. A child somebody moved deliberately in the meantime stays where they put it.

  • detached: true — the page's own parent is gone, so it came back at the top level.

  • renamedFrom — a page took its slug while it was in the trash, so it came back under a free one.

One level per delete: if you delete a page and then its parent, restoring either one returns the child to that one, not to both.

Snapshots

GET POST  /orgs/{orgId}/spaces/{spaceId}/snapshots
DELETE    /orgs/{orgId}/spaces/{spaceId}/snapshots/{id}
POST      /orgs/{orgId}/spaces/{spaceId}/snapshots/{id}/restore

Space-wide version history: one named label capturing every published page's live revision, restorable in a single action. This is how you undo a bad edit that spanned twenty pages without rolling each one back.

It captures the default version only, and only pages that are published. Drafts and non-default versions are not in a snapshot, so a restore will not bring them back. The listing returns the most recent 200.

A restore answers with four counts and takes one option:

Field

What it is

restored

Pages pointed back at their captured revision.

skipped

Captured pages whose page or revision no longer exists.

addedSince

Published pages that did not exist when the snapshot was taken.

unpublished

How many of those addedSince pages were taken out of the reader.

newPages (request)

keep (the default) or unpublish — what to do with the addedSince pages.

What a restore does not do

A restore points every captured page back at the revision it had. It is not a rewind of the whole space, and these are the four things it leaves alone:

Left alone

Why, and what to do instead

Pages created after the snapshot

They stay published by default. The response counts them as addedSince; send {"newPages":"unpublish"} to take them out of the reader instead.

Pages deleted after the snapshot

A deleted page is not brought back. Restore it from the trash first, then restore the snapshot. Each one is counted in skipped.

Drafts

An unpublished draft is untouched — a restore changes what readers are served, not what you are working on.

Other versions and locales

A snapshot is of the default version, so nothing in v1 or a translated version is captured, counted or changed.

POST /orgs/{orgId}/spaces/{spaceId}/snapshots/{id}/restore
{ "newPages": "unpublish" }

→ { "restored": 18, "skipped": 1, "addedSince": 3, "unpublished": 3 }

unpublish is reversible: the pages, their revisions and their place in the tree all stay, so publishing one again is a single call. keep is the default, because most callers send no body at all and a restore that started unpublishing pages on them would be the worst possible surprise from a word like "restore".

Templates

Three different things share the word. Pick carefully.

Page templates — a saved page body to start new pages from.

GET POST  /orgs/{orgId}/spaces/{spaceId}/page-templates
DELETE    /orgs/{orgId}/spaces/{spaceId}/page-templates/{id}
POST      /orgs/{orgId}/spaces/{spaceId}/page-templates/{id}/new-page

orgWide: true makes a template available in every space. The flag is the scoping, so you cannot narrow it afterwards — recreate it.

Space templates — scaffolding for a whole space.

GET          /orgs/{orgId}/space-templates
PATCH DELETE /orgs/{orgId}/space-templates/{id}
POST         /orgs/{orgId}/spaces/{spaceId}/apply-template
POST         /orgs/{orgId}/spaces/{spaceId}/save-as-template

GET /orgs/{orgId}/space-templates is the catalogue. Every entry says where it came from in source:

source

What it is

builtin

A starting point we ship. Some also create the collections to file pages in — those say so in their name.

mine

One this organization saved from a space with save-as-template.

bank

One another organization published.

Apply any of them with POST …/apply-template and {"templateId": "<id>"}. The answer reports every page the template names:

{
  "created": 3,
  "skipped": ["overview"],
  "failed": [{ "slug": "errors", "code": "quota.exceeded", "message": "…" }]
}

skipped is a page that was already in the space — applying is a merge, so it is left alone. failed is a page that was refused, with the error. A 200 with a non-empty failed is a partial apply: the pages that landed stay, and applying the same template again retries only the ones that did not.

There is an older, deprecated route — GET /orgs/{orgId}/templates — that lists only the documentation scaffolds. Every key it returns is an id in the catalogue above, so use the catalogue; nothing needs the old route, and it is not going away on a date.

Form templates — required fields on a forum category, so a bug report arrives with a version number.

GET POST     /orgs/{orgId}/spaces/{spaceId}/form-templates
POST         /orgs/{orgId}/spaces/{spaceId}/form-templates/attach
GET PATCH DELETE /orgs/{orgId}/spaces/{spaceId}/form-templates/{id}
GET          /public/orgs/{orgId}/spaces/{spaceId}/forum/categories/{categoryId}/form-template

They only take effect when community.enable_form_templates is on and the category has one attached. Then a topic with a blank required field is rejected, naming the fields that are missing.

Change requests

POST GET  /orgs/{orgId}/spaces/{spaceId}/change-requests
GET       …/change-requests/eligibility
GET PUT   …/change-requests/merge-rules
GET       …/change-requests/{crId}
POST      …/{crId}/approve  /close  /merge  /rebase  /review
POST      …/{crId}/request-review  /agent-review  /agent-run
POST GET  …/{crId}/comments
GET       …/{crId}/conflicts  /merge-blockers  /pages
GET POST  …/{crId}/reviewers
DELETE    …/{crId}/reviewers/{reviewerId}
POST      /orgs/{orgId}/spaces/{spaceId}/github/change-requests/{crId}/pull-request

A branch of page drafts reviewed and merged as a unit. Default merge rules: no approvals required, not all reviewers required, and a "changes requested" verdict blocks.

Four things about the gate:

  • The author's own approval does not count toward the required number. It is recorded and shown — it just never satisfies the count. The blocker message says "from someone other than the author" so the number does not look wrong to the one person most likely to be reading it.

  • One row per person, newest verdict wins. Approving twice is one approval.

  • Approvals go stale when a newer revision lands after them — but the stale blocker is inert unless a rule actually asks for approvals, so a space that requires none is never blocked by one.

  • Read GET …/{crId}/merge-blockers rather than inferring from status. Status and the gate were once two mechanisms that never met, which is how the approve button came to satisfy nothing.

Styleguide rules for change requests

An organization can keep a list of content rules that the docs agent checks every change request against, on top of its built-in review heuristics. Use them for banned phrases, terminology you want standardized, or a regular expression that catches a pattern such as a leaked internal hostname. Managing them needs org.manage_settings.

POST /orgs/{orgId}/content-rules
{
  "pattern": "simply",
  "message": "Avoid 'simply'; it dismisses the reader's difficulty.",
  "severity": "warn"
}

Field

Required

Notes

pattern

yes

Up to 300 characters. Matched case-insensitively.

message

yes

What the reviewer sees when it matches. Up to 300 characters.

isRegex

no

Treat pattern as a regular expression. An invalid one is a 422.

severity

no

warn (default) or info.

Without isRegex the pattern is a plain substring match. Each enabled rule reports at most one hit per page, with the matched text. GET /orgs/{orgId}/content-rules lists the rules, newest first, and DELETE /orgs/{orgId}/content-rules/{id} removes one.

Presets and shape

PUT   /orgs/{orgId}/spaces/{spaceId}/preset
POST  /orgs/{orgId}/spaces/{spaceId}/shape/presets/{name}
PUT PATCH /orgs/{orgId}/spaces/{spaceId}/shape
GET   /space-types/presets
GET   /space-types/{type}/presets

A layout preset picks the index layout, the piece layout and the front door together — PUT …/preset with at least one of preset, piecePreset, frontDoor.

A shape preset is applied with POST …/shape/presets/{name} and is merged, not replaced: a space that renamed a slot and then applied a preset meant to keep the rename, and applying the same preset twice is the same as applying it once. A preset for the wrong space type is a 400 naming the types it belongs to; an unknown name is a 404.

PUT …/shape replaces the whole override, including sections and params. A second PUT that sent only params wiped the sections — with a 200 and no mention of what went. Use PATCH unless you mean to replace everything.

The kinds of value a param can hold

params.add[].type and params.set.*.type take exactly these. This is the list — the declaration, the write enum, the console dropdown and the value check are all built from or tested against it.

type

A value looks like

Notes

text

"Remote"

Up to 2000 characters.

select

"Full time"

options is required: a choice with no choices is refused when you declare it, not when somebody tries to fill it.

multiselect

["Remote", "Hybrid"]

Any subset of options, which is likewise required.

url

"https://acme.com/apply"

http/https only, and absolute — it is a way out.

image

"https://acme.com/a.png"

A URL the console previews, or a path on this site such as /api/assets/<id> (what an upload returns).

email

"jobs@acme.com"

date

"2026-09-01"

YYYY-MM-DD.

boolean

true

Not "true".

number

9

Not "9".

money

{"min":70000,"max":90000,"currency":"GBP","period":"year"}

Every part optional; a fifth key is refused, because {"unit":"YEAR"} once published a band with no period on it.

relation

["<item id>"]

Always an array, even for one target — a single reference is a one-element array. Up to 50 ids.

null clears any param: a value that was set can always be unset. So does an empty (or all-whitespace) string — emptying a box in the console must not differ from never filling it in — and either way what is stored is null.

A relation also takes relationSpaceId — the space whose pieces the field points at; omit it for a relation inside the same space. It says where to look, not what is allowed: the console offers that space's pieces and the JSON Schema names it, and no write door refuses an id from another space in the org. Ids are not checked for existence either — an id that points at nothing, or at a piece a reader may not see, resolves to nothing on the public page rather than failing the write.

Resolve the ids a relation holds with GET /public/orgs/{orgId}/relations?ids=a,b,c, which answers titles and reader URLs for the public pieces among them.

Feedback

POST  /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/feedback
POST  /public/orgs/{orgId}/spaces/{spaceId}/ask/{queryId}/feedback
POST  /orgs/{orgId}/spaces/{spaceId}/docs/ask/{queryId}/feedback
GET   /orgs/{orgId}/spaces/{spaceId}/docs/analytics
GET   /orgs/{orgId}/spaces/{spaceId}/docs/feedback
GET   /orgs/{orgId}/spaces/{spaceId}/docs/feedback.csv

"Was this helpful?" — { "rating": "up" | "down", "comment": "…" }, comment up to 1000 characters. No authentication, because the people whose opinion you want are readers. A widget asking on a four-step scale (theme.docs.feedback.scale: 4) also sends score (1–4); the server then derives rating from it (3–4 is up), so the up/down counts keep one meaning.

How the widget looks and where it sits is theme.docs.feedback (see Theme a documentation space). metadata.hideFeedback: true removes it from one page. Page views are counted either way.

It answers 204 with an empty body. Do not try to parse the response.

Read it back through …/docs/analytics, which aggregates to per-page up/down counts and a daily series of views, feedback and searches.

The written comments are GET …/docs/feedback: newest first, { comments: [{ id, pageId, rating, score, comment, createdAt }], page }, filtered by pageId, rating (up/down) and sinceDays (1–365, default 90), paged with limit and cursor. Who left a comment is never in it. …/docs/feedback.csv is the same list as a spreadsheet (createdAt,page,rating,score,comment); a comment starting with =, +, - or @ is prefixed with ' so it opens as text. Both need docs.read; the console shows them under Analytics.

PDF

GET  /public/orgs/{orgId}/spaces/{spaceId}/pdf
GET  /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/pdf
GET  /public/orgs/{orgId}/spaces/{spaceId}/export
GET  /orgs/{orgId}/export
GET  /users/me/export

The whole space as one document, or one page. Both take ?version= and ?locale=. The per-page route exists because a 119-page space is not what the sidebar's "Download PDF" means to the person clicking it.

A page with no published revision is a 404, so a draft's existence never leaks through the PDF route.

llms.txt, llms-full.txt, skill.md and per-page markdown

GET  /public/orgs/{orgId}/spaces/{spaceId}/llms.txt
GET  /public/orgs/{orgId}/spaces/{spaceId}/llms-full.txt
GET  /public/orgs/{orgId}/spaces/{spaceId}/skill.md
GET  /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}.md
GET  /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/markdown

llms.txt is an index with a line per page. llms-full.txt is every published page concatenated. skill.md packages the space as an agent skill. The per-page .md is also served at https://<your-address>/<slug>.md.

On a documentation page the actions bar offers these to readers: copy, view as Markdown, open in ChatGPT, Claude or Perplexity, connect over MCP, and one-click "Add to Cursor" / "Add to VS Code" installs of the space's MCP server. Which ones, in what order and in what style is theme.docs.pageActions — see the docs theme guide.

On a single-space custom domain, llms.txt, llms-full.txt and skill.md are served at the address root.

All of these honour the per-space AI-ingestion opt-out. Set it with PUT /orgs/{orgId}/spaces/{spaceId}/discovery, and these endpoints answer 404 — "This space does not offer its content to AI tools." It is a 404 and not a 403 on purpose; a 403 confirms the space exists.

The PDF routes deliberately do not check it. A space that declined to be harvested wholesale has not declined to be printed.

The landing page: home-page versus home-doc

GET PUT  /orgs/{orgId}/spaces/{spaceId}/home-page   { "homePageId": "…" | null }
GET PUT  /orgs/{orgId}/spaces/{spaceId}/home-doc    { "doc": { … } | null }

Two different things, and the space's preset decides which one is used, not which one you set.

home-page

home-doc

What it is

An existing page, nominated as the landing page

A block document rendered above a fixed listing

For

Documentation-shaped spaces whose front door is a page

Blog, forum, roadmap, changelog, status, contact — whose listing has a fixed shape, so their landing cannot be an ordinary page

The rule:

  • preset === "landing" → homeDoc renders. homePageId is never read.

  • any other preset → homePageId wins; if unset, the first page in reading order. homeDoc is never read.

They never compete. Setting both is not an error and produces no warning — one is simply dead, depending on the preset. If you set one and nothing changed, check the preset first.

Three more facts:

  • homePageId self-heals. It is only used if the page is still published — hidden or not: hideFromNav keeps it out of the tree and keeps it the landing page. A deleted or unpublished home page falls back to the first page rather than sending every reader to a dead id.

  • theme.docs.homeAt: "root" renders the home page AT the space root instead of redirecting to its slug. The page's own address keeps answering, with its canonical pointing at the root. See Theme a documentation space.

  • A page from another space is a 404 — "That page is not in this space."

  • PUT …/home-doc with an empty document clears the setting rather than storing an empty one.

Editorial blocks: pull quotes, breakout media and margin notes

Four things a long read needs that a manual does not: a quote set large, a picture wider than the text, a figure on a tinted panel, and a note in the margin. Every attribute below is optional. A block that sets none of them renders exactly as it did before they existed. GET /docs/block-schema publishes each one with its allowed values, and a value outside them is a 422.

Pull quotes and cites

blockquote takes attrs:

Key

Values

What it does

variant

default | pull

pull sets the quote at display size (semibold, no bar, no italic).

cite

text, up to 300 characters

Printed under the quote as "— cite".

align

start | center

center centres the quote (and drops the bar from a plain one).

{
  "type": "blockquote",
  "attrs": { "variant": "pull", "cite": "Ada Lovelace", "align": "center" },
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "The engine weaves patterns." }] }
  ]
}

In markdown, the cite is the quote's last line, starting with an em dash (—), a horizontal bar (―) or two hyphens (--). Only that last line becomes the cite. The quoted lines above it stay the quote:

> The best interface
> is no interface.
> — Golden Krishna

{: .pull .center}

The {: …} line after the quote is the block-hook line (see Block hooks). On a quote, .pull and .center are read as the quote's variant and align, not as classes. Any other class on the same line is still a hook.

A theme can make every plain quote a pull quote with components.quote: {style: "pull", size, align}. A quote whose own variant is default keeps the bar even then.

Breakout pictures and rows

image.attrs.width takes a number (a percentage of the column), a CSS length, or one of two breakout words:

  • wide runs past the text measure to the theme's media width (layout.widths.media, published as --reader-media). With no media width set, it runs to the page shell. It is never narrower than the column.

  • full runs edge to edge across the viewport, with square corners.

A breakout picture is always a <figure> (with data-width), even without a caption. Its caption stays on the text measure. columns.attrs.breakout (wide | full) does the same for a row of columns. For example, a three-up row of art can run wider than the words around it.

On a docs page a breakout runs to the article's full width, never over the navigation. To reach further, give the page metadata.layout: "full".

A column with role: "aside" (::column{role=aside sticky}) is lifted out of its row on a docs page and shown in the right-hand rail, sticky beside the article — a request example beside the prose. One per page; below 1280px it follows the article. Its width and top offset are theme.docs.aside.

Figure panels

Two more image attrs put a picture on a tinted panel, like a product screenshot on a light card:

Key

Values

background

none, surface, muted, accent, primary, or a #hex

pad

none, sm, md, lg

In markdown, write the image attrs as an attribute list directly after the image, with no space before it:

![Address resolution](/api/assets/abc 'Resolution, end to end'){width=wide bg=#f6f9fc pad=md}

The keys are width (wide/full), bg and pad. If any token in the list is not one of these, the whole line is read as ordinary text. Rows use the directive attribute: :::columns{breakout="full"}.

Sidenotes and where footnotes go

The sidenote mark puts a note in the right margin, beside the line it belongs to. Its text is attrs.note, as for a footnote:

{
  "type": "text",
  "text": "margin",
  "marks": [{ "type": "sidenote", "attrs": { "note": "Set at 1280px and wider." } }]
}

At 1280px and wider the note hangs in the margin. Below that it is a small note under its line. Both the word and the note carry the same number. In markdown, write the note straight after the word it belongs to, the way you write a footnote:

A word in the margin>[the note] and one at the foot^[the other note].

A ] inside the note is written \\].

Footnotes stay hover notes unless the space's theme says otherwise. footnotes.placement takes popover (the default, today's behaviour), margin (every footnote becomes a numbered margin note) or end (numbered in the text and collected into one list after the body, each with a link back).

A row of columns: divider, gap and one caption

A columns block takes three more look keys and a caption. All are optional; a row with none of them is the row it always was (a 1rem gap, no rule, no figure).

Key

Values

divider

true (a hairline in the theme's line colour), or a colour: a token (bg, surface, panel, ink, muted, brand, accent) or a #hex

gap

The space between columns, and between rows once they wrap: none, xs, sm, md, lg, xl (scaled by the theme's section rhythm) or whole px 0–400

rowGap

The space between rows once the columns wrap, when it should differ from gap. Same values

caption

One caption under the whole row, up to 500 characters. The row becomes a <figure class="columns-figure"> with the caption as its <figcaption>

spaceBefore, spaceAfter

The row's own room above / below it, instead of the section's one gap: a length (96, 4rem, clamp(…)) or one per screen size, {"lg": 96, "sm": 40}. Unset is the row's 0.75rem

spaceAround

Both at once; spaceBefore/spaceAfter override it on their side

valign

How the columns line up against each other: start, center (a picture beside a paragraph, centred on it), end, baseline. Unset, each column is as tall as the row. :::columns{valign="center"}

alternate

true: alternating rows. Among the sibling rows that set it, every second one runs right to left (copy then picture, picture then copy). Stacked on a phone, every row reads in its own order. :::columns{alternate="true"}

A gridItem takes href: the whole cell becomes one link (a stretched link named by the cell's first heading; links inside the cell still work). ::gridItem{colSpan="4" href="/guides/start"}. A button with style: "pill" is a link pill for a row of them, and match (a path prefix) marks the pill of the page being read as current (aria-current, on a blog's or a community's front page) — :::button{label="Community" href="/community" style="pill" match="/community"}:::.

The divider is drawn between side-by-side columns only, so it goes away when the row stacks (on a phone, or on a tablet with stackOn: "md"). Under a breakout row the caption stays on the text measure, and a captioned wide or full row breaks out exactly as an uncaptioned one does. Anything else is refused with a 422 naming the key, for example divider: "red" or gap: 401.

{
  "type": "columns",
  "attrs": { "divider": true, "gap": "lg", "caption": "Left: the old card. Right: the new one." },
  "content": [
    { "type": "column", "content": [] },
    { "type": "column", "content": [] }
  ]
}

In markdown they ride on the directive line. divider on its own is the hairline:

:::columns{divider gap="lg" rowGap="24" caption="Left: the old card. Right: the new one."}
::column
…
::
:::

Text colour

The textColor mark colours a run of words. attrs.color is required. It takes the same colours as the block color key: a theme token (muted, brand, accent, ink, bg, surface, panel) or a #hex. A token follows the theme, including dark mode. A hex is the same in both schemes. Anything else (red, url(…)) is refused with a 422.

{
  "type": "text",
  "text": "quieter words",
  "marks": [{ "type": "textColor", "attrs": { "color": "muted" } }]
}

In markdown, put the words in brackets with the colour straight after them:

Plain, then [quieter words]{color=muted}, then [a red one]{color=#e5132f}.

The colour wraps any other marks, so a coloured link is written [[the docs](/docs)]{color=brand}. In the editor, select the words and use the A button in the formatting bubble. Leave the box empty to remove the colour.

Sections, video and block hooks

Three additions to the block model, all optional, all invisible on a page that does not use them. GET /docs/block-schema describes every one of them — the section keys under the section node, the video node, and the three hooks once under hooks (they apply to every node, so they are not repeated on each).

A section's look

A section block (a named slot) has always taken key and width. It also takes these, each nullable — null or absent is "as before":

Key

Value

background.color

a colour: a token (bg, surface, panel, ink, muted, brand, accent) or #hex

background.gradient

{ type: "linear" | "radial", angle?: 0–360, stops: [{ color, at?: 0–100 }] }, 2–4 stops

background.image

{ src, size?: cover | contain | auto, position?, overlay?: { color, opacity 0–1 }, attachment? }

background.image.position

center (default), top, bottom, left, right, top left, top right, bottom left, bottom right

background.image.attachment

scroll (default), fixed, or parallax (a scroll-driven effect; none under reduced motion)

background.video

{ src, poster? } — muted, looping, inline; see below

padding

{ top?, bottom?, x? }, each a pixel count 0–400 or a token none xs sm md lg xl

minHeight

"60vh" (1–100) or "480px" (1–2000), any length ("clamp(20rem, 60svh, 40rem)"), or one per screen size ({"lg": 640, "sm": "80svh"})

contentAlign

start, center, end — the text and the content inside

verticalAlign

start, center, end — where the content sits when minHeight leaves room

radius

pixels 0–64, or none sm md lg full

border

{ width: 0–8, color?, style?: solid | dashed | dotted, sides?: all | y | x | top | bottom } — y is the hairline above and below each band of a gridded page

shadow

none sm md lg

fullBleed

true: the background runs edge to edge of the screen; the content keeps its column

scheme

auto (default), light, dark — paint this section in that scheme whatever the page is in

pattern

none (default), dots, grid — a dotted or squared backdrop drawn over the fill, under the content

patternColor

the pattern's ink: a colour token or #hex. Default: the ink at 22%

patternFade

none (default), radial (strongest in the middle, gone at the edges), bottom (fades out downward)

{
  "type": "section",
  "attrs": {
    "key": "hero",
    "scheme": "dark",
    "fullBleed": true,
    "minHeight": "60vh",
    "verticalAlign": "center",
    "contentAlign": "center",
    "background": {
      "image": { "src": "/api/assets/…", "overlay": { "color": "#000000", "opacity": 0.45 } }
    },
    "padding": { "top": "xl", "bottom": "xl" }
  },
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 1 },
      "content": [{ "type": "text", "text": "Ship it" }]
    }
  ]
}

A scheme uses the same colours the whole page would in that mode, including your theme's paletteDark and brand — see A section in its own scheme. A section with a background or a scheme gets comfortable padding by default; set padding to override it. Background images go through the same address rule as an image block, so a library picture works on a path-mounted custom domain too.

Background video. It plays muted, looped and inline, and is never in the tab order. A reader whose system asks for reduced motion is not sent the video at all — its <source> only matches prefers-reduced-motion: no-preference — and sees the poster, which also paints the background while the video loads. Give one.

In the editor, a section's Style button opens all of the above.

The video block

{
  "type": "video",
  "attrs": {
    "src": "/api/assets/…",
    "poster": "/api/assets/…",
    "caption": "The two-minute tour",
    "aspectRatio": "16:9",
    "controls": true,
    "autoplay": false,
    "muted": false,
    "loop": false
  }
}
  • src is a library path (upload an MP4 from the block, or /api/assets/…) or an https:// URL. Anything else — http:, data:, a bare //host — is a 422. A video on another host also needs that host in the theme's mediaOrigins: videos are not proxied, and the reader's policy refuses an origin it was not told about.

  • aspectRatio is 16:9 (default), 4:3, 1:1, 9:16 or 21:9. A tall video stops at the height of the screen and letterboxes.

  • autoplay always plays muted (no browser autoplays sound), and is paused — with controls shown — for a reader who asked for reduced motion. controls: false is only honoured on an autoplaying video; otherwise there would be no way to start it.

In markdown it is one line, and round-trips:

:::video{src="/api/assets/abc" poster="/api/assets/def" ratio="4:3" caption="Tour" autoplay loop}:::

A video as a lesson. Six more attributes, all optional:

  • chapters — [{ "at": 90, "title": "Setup" }] (seconds, up to 100), listed under the player; a click seeks. In the editor, one 0:00 Title per line.

  • transcript — WebVTT or plain text (200,000 characters), shown under the player in a searchable panel and indexed with the page. WebVTT is also the captions track, so there is no second file to host. Plain-text lines that open with a timestamp (0:42 Now we…) seek too.

  • transcriptOpen, speed (the 0.75×–2× row), resume (start where this viewer stopped) — unset means the course's theme.courses.video on a lesson, and off everywhere else.

  • completeAt — on a course lesson, complete it once this percent has actually been played (0 = never; unset = the course's video.completeAt).

The player is the browser's own <video>: no third-party script or request. In markdown: chapters="0:00 Intro|1:30 Setup", transcript="<base64>", the flags speed/nospeed, resume/noresume, transcriptopen, and completeAt="90".

The quiz block (knowledge check)

{ "type": "quiz", "attrs": {
  "question": "Which hook runs after every render?",
  "choices": [{ "text": "useEffect", "correct": true }, { "text": "useMemo" }],
  "multiple": false,
  "explanation": "Effects run after the browser paints."
} }

Ungraded: checked in the reader's browser, records nothing; it draws nothing until it has a question, two choices and a right one. Graded: { "quizId": "<course quiz id>" } — one of the course's lesson quizzes or module assessments, mounted where the block sits on a lesson and graded on the server (a graded check is not drawn outside a course). In markdown it is :::quiz{data="<percent-encoded JSON>"}:::. "Reveal the solution" is an expandable with "style": "solution" (untitled, it says "Reveal the solution").

Block hooks: anchor, className, hook

Every block — a paragraph, a callout, a section, a video — may carry any of three attributes inside its attrs, so customCss can reach that one block:

{
  "type": "hint",
  "attrs": { "style": "info", "anchor": "limits", "className": "loud", "hook": "limits" }
}

renders as <div class="hint hint-info u-loud" id="limits" data-fx-block="limits">. The names, the u- prefix and how ids are kept unique are in Hooks you put on a block yourself. Values that do not match the rules are a 422, never silently changed.

A heading with no anchor gets one from its words: lowercased, accents folded (é → e), punctuation dropped, spaces as hyphens, at most 80 characters, and -2, -3 … for repeats. "Caché con ETags" is #cache-con-etags; letters with no accent to fold are kept (#политика). Headings published before accents were folded were addressed without the accented letter (#cach-con-etags); that old id still lands on the heading.

In the editor, any block at any depth takes them — the paragraph in the second column, a grid item, a card, a tab's body, a carousel slide, a modal's contents. Right-click inside the block: the menu lists that block and every block around it, innermost first ("Paragraph", "Column", "Columns"), outlining each on the canvas as you point at it, and picking one opens its Anchor / Classes / Hook fields. The same menu opens from the # in the formatting bubble for whatever is selected, and from Ctrl/⌘ + Alt + A. Shift + right-click is still the browser's own menu. A top-level block also has the # beside its drag handle, and a section has the same three fields at the bottom of its Style popover.

In markdown, any block with a directive carries them as attributes — :::hint{style="info" anchor="limits" class="loud" hook="limits"}. Every other block uses an attribute list in braces, the kramdown/Pandoc spelling: #anchor, then .class per class (up to four), then hook=name.

## Pricing {#pricing .wide hook=plans}

Our plans start at zero. {#intro .lede}

- Sign up
- Invite your team

{: #steps .tight}
  • A heading or paragraph takes the list at the very end of its (last) line, after a space.

  • A list, checklist, quote, code block, table, image, divider, formula or mermaid diagram takes it on a line of its own that starts with {:, after one blank line below the block. Leave the blank line in: without it a list's last item (or a table's last row) would read the line as its own text.

They round-trip: an export writes exactly this, and importing it gives the same hooks back. Braces in ordinary prose are left alone — the list is only read at the end of a line and only when every token in it is valid, so the set {a, b}, {#} or {#Not A Slug} stay text. If a sentence really does end in something like {#note}, write \{#note}; an export escapes it for you. Hooks on a list item or inside a table cell have no spelling in markdown, so the studio's in-place markdown edit sends a page with those to the full editor rather than saving it back without them. A section's look is not in markdown either (a section is not, in markdown — only its words are), and writing a slot's words through PUT …/sections/{key} keeps its look.

Custom HTML blocks: where their CSS can reach

The CSS of a custom HTML block is rewritten so that every rule lands inside that block. h2 { … } styles the block's own headings; :root, html and body mean the block itself. That holds inside @media, @supports, @container, @layer and @starting-style, however deeply nested, and for nested rules (&). A selector that starts with + or ~ would reach the block's siblings and is dropped. @keyframes, @font-face and @property keep their bodies but get a block-specific name — your @keyframes spin becomes the block's own, and the block's animation: spin … follows it — so a block cannot redefine an animation, a font family or a registered property of the page around it. @page is kept as written. @import, @charset, @namespace, the @layer a, b; statement and at-rules we do not know (such as @scope) are removed, and nothing in the CSS can close the <style> element it is printed into.

Embeds

An embed block frames another site inside a page. In markdown it is one line:

::embed{url="https://calendly.com/acme/intro"}

Paste the page's ordinary address — the one in your browser's address bar — and the reader turns it into that provider's own embed. These are recognised:

Provider

Paste a link like

YouTube

youtube.com/watch?v=…, youtu.be/…, /shorts/…

Vimeo

vimeo.com/123456

Loom

loom.com/share/…

Figma

figma.com/file/…, /design/…, /proto/…, FigJam /board/…

CodePen

codepen.io/{user}/pen/{id}

CodeSandbox

codesandbox.io/s/{id}

StackBlitz

stackblitz.com/edit/{id}, /github/{owner}/{repo}

Replit

replit.com/@{user}/{repl}

Google Docs

a document, spreadsheet or presentation link

Google Maps

google.com/maps/place/…, ?q=…, or an embed URL

Calendly

calendly.com/{user}/{event}

Typeform

form.typeform.com/to/{id} (or your subdomain)

Tally

tally.so/r/wMx1kQ

Spotify

a track, album, playlist, episode, show or artist

SoundCloud

any track or set page

Airtable

a shared view or form (airtable.com/…/shr…)

Miro

miro.com/app/board/{id}/

Excalidraw

a shared scene (excalidraw.com/#json=…) or a live room (#room=…)

X / Twitter

x.com/{user}/status/{id}

Instagram

instagram.com/p/C1a2B3c4D5e or a /reel/ link

GitHub Gist

gist.github.com/{user}/{id}

Anything else is shown as a link, unless you have told us you trust the host: add its exact origin to the theme's frameOrigins (see Theming, advanced) and an embed of any URL on it is framed as written — a Stripe pricing table, a HubSpot form, a status widget, your own app.

Every frame is sandboxed with the least it needs: a video player may run its script and keep its own cookies; a booking page or a form may also submit; a gist gets no script at all. None of them may navigate your page or open a dialog over it. Frames load lazily, and send at most your page's origin as the referrer — an owner-declared host is sent none.

A gist is framed from GitHub's plain HTML render of it (….pibb) rather than the usual <script> embed, because that script writes into your page and the reader's security policy will not run third-party script there.

Embeds work the same on every address the space is published at — your own domain, a *.forumulate.com subdomain and the forumulate.com address — and in docs pages, blog posts, changelog releases, legal documents, events, a contact page's copy and a space's intro. Forum posts and replies render a known provider but never frame a frameOrigins host: you trusted that host for your pages, not as something any member can drop into a thread.

Forms

A form block puts a newsletter box, a waitlist or a lead form on a page. Type /form in the editor and pick or create a form in the block's inspector. The form itself is stored once (/orgs/{orgId}/forms), checked on the server for every submission, and can be placed on any number of pages. Responses are in Console → Forms, can be exported as CSV, and can be deleted per person for a data-protection request. Forms work without JavaScript and on every address, including custom domains mounted under a path. See Forms for the field types, consent, spam handling, notifications and the API.

Unsetting a metadata key. PATCH …/pages/{pageId}/metadata takes null on EVERY key, and null removes that key from the page — the page then follows the space again, exactly as one that never set it. {"layout": null} answers 200 and hands the article width back to theme.docs.articleWidth; {"hideTitle": null}, {"template": null} and the rest work the same way. The keys you do not send are left alone. (fields keeps its own one-level-deeper rule: {"fields": {"audience": null}} clears one field; {"fields": null} clears them all.)

Metadata keys (PATCH …/pages/{pageId}/metadata) for what hangs under a page:

Key

Meaning

commentsEnabled

false turns comments off on this page when the space takes comments; null or absent follows the space.

hideFeedback

true removes "Was this page helpful?" from this page. Views are still counted.

hidePager

true removes previous / next from this page.

pager

{ "prev": "<pageId>" or false, "next": "<pageId>" or false, "label": "…" } — this page's own previous / next instead of the tree's; false removes that side; label is what other pages' pagers call this one. null clears.

editUrl

This page's "Edit this page" address, beating theme.docs.editLink; false hides the link here; null clears.

sourcePath

The repository file the page came from. A repository sync records it; it is the {path} an edit or issue link fills in.

keywords

Up to 20, published as <meta name="keywords">; trimmed and de-duplicated on write.

A page's own look and share card

In the docs rail

Five metadata keys decide how a page appears in the docs tree (PATCH …/pages/{pageId}/metadata):

Key

What it does

icon

An emoji, an icon name from GET /icons?for=page (book, code, lock, …) or an image URL (any other bare word is a 422) — in the tree and before the title.

navTitle

What the tree calls the page, when that is not its title. Previous/next and breadcrumbs keep the title.

badge

{ "text": "New", "variant": "new" } — a pill after the label and after the title. Variants: default, note, tip, caution, danger, success, new.

navKind

separator (a rule) or heading (a label, not a link). The page stays routable — set noindex on it.

navFont

mono sets this page's label in the code face.

A glyph name used to print as the word ("book") and a longer one was refused; both now draw the glyph.

Every page — and every blog post, changelog release and legal document — can override the space for itself alone, from Page look & sharing in its details rail or over the API:

  • Custom CSS for this page only, after the space theme. It is sanitised and scoped to the reader exactly like the space's customCss (@import removed, nothing can close the <style>), so h2 { … } styles this page's headings and nothing else.

  • A partial theme — colours, palettes, fonts, colour scheme, components, widths, motion — layered over the space's. A misspelled key is refused by name.

  • A share image separate from the cover, which wins over the cover and the generated social card everywhere the page is shared; plus an og title, an og description and the X/Twitter card type.

  • JSON-LD of your own, which replaces the automatic TechArticle / BlogPosting / Article markup on that page. It must be valid JSON.

On a page these are metadata keys (PATCH …/pages/{pageId}/metadata with customCss, theme, shareImageUrl, ogTitle, ogDescription, twitterCard, jsonLd); on a post, a release or an inline legal document they go under customization. null clears one. See Theme your site → Per-page customization for every rule.

You see them while you set them. The page editor's canvas wears the page's own CSS and theme on top of the space's, and changes as you type in the details rail — before anything is saved. It is the reader's own stylesheet, aimed at the canvas alone, so h2 { … } restyles the page's headings and never the editor around them. For a post, a release or a legal document, the studio's live view (and any other preview showing that piece) swaps in your unsaved CSS and theme the same way, and puts the saved look back if you close the card without saving. A ?draft=1 or preview-link view of a page that has never been published shows its own look too.

The canvas also moves the way the reader does where the pointer is concerned: link, button, card and image hovers, hover-triggered animations and the focus ring follow the space's (or the page's) motion. Scroll reveals, page transitions, entrance animations and anything pinned to the window (a scroll-progress bar, a cursor glow, a sticky header) stay off while you edit — a paragraph that fades in as you scroll to it is not one you can type into.

Interactive blocks

Carousels, galleries with a full-screen viewer, pricing tables, testimonials, logo walls, stats, marquees, a button that opens a dialog, and a before/after slider. Type / in the editor and pick one (Media: Carousel, Gallery, Before / after, Marquee; Layout: Pricing, Testimonials, Logo wall, Stats, Button with dialog), or write the JSON below through the API.

Three promises hold for all of them:

  • The page says everything with no JavaScript. Every slide, every price, every quote and the final value of every stat is in the server's HTML — a crawler, a reader with scripts off and a screen reader all get the content. Behaviour (arrows, autoplay, the viewer, count-up, the slider) is added by one small script the reader fetches only on a page that has one of these blocks; a page without them loads nothing extra.

  • Reduced motion is honoured. A carousel does not autoplay (it offers a play button instead), stats do not count, a marquee stands still and wraps.

  • They wear your theme. Colours, corner radius, shadows, button styles and focus rings come from your palette and components.* tokens. Every block has a stable hook for customCss ([data-fx-carousel], [data-fx-pricing], … — the full list is in the fx-1 hook set). Pictures load through the same image rules as an image block, so they work on a custom domain mounted under a path.

Every block also takes the three block hooks.

Slides of any blocks — pictures, text, cards. Swipe or scroll it with no script (CSS scroll snap); the dots are links to each slide. With script it gains previous/next buttons, arrow keys/Home/End on the focused row, and autoplay that pauses while the pointer is over it or focus is inside it, with its own pause button.

{
  "type": "carousel",
  "attrs": {
    "label": "Customer stories",
    "autoplay": true,
    "interval": 6000,
    "loop": true,
    "arrows": true,
    "dots": true,
    "perView": 3,
    "perViewMd": 2,
    "perViewSm": 1,
    "gap": "md"
  },
  "content": [{ "type": "carouselSlide", "attrs": { "label": "Acme" }, "content": ["…blocks…"] }]
}
  • perView (1–6) is how many slides show at once on a wide screen, perViewMd (1–4) at 900px and below, perViewSm (1–3) at 640px and below.

  • interval is milliseconds per slide, 2000–30000 (default 5000).

  • gap between slides is none, sm, md or lg.

  • label is the region's name for screen readers ("Carousel" if unset); a slide's label is announced after "2 of 5".

  • It follows the ARIA carousel pattern: a named region with aria-roledescription="carousel", each slide a group "n of N". The slide row is a polite live region while you move it yourself, and silent while it autoplays.

  • A carouselSlide exists only inside a carousel, and a carousel holds only slides — anything else is a 422 that says so.

A gallery is a grid of image blocks (only images). By default each picture opens in a full-screen viewer: previous/next across the gallery, arrow keys, swipe on a phone, zoom (button, double click/tap, pinch, +/-/0), drag to pan while zoomed, caption and "2 of 7", Esc, a swipe down or a click outside to close, focus trapped while open and returned to the picture after. With no script a click opens the picture itself.

{
  "type": "gallery",
  "attrs": { "columns": 3, "gap": "md", "lightbox": true, "aspect": "square" },
  "content": [{ "type": "image", "attrs": { "src": "/api/assets/…", "alt": "…", "caption": "…" } }]
}

columns is 2–6 (two on a phone), gap is none, sm, md or lg, aspect crops every tile to square, 4:3, 3:2 or 16:9 (auto keeps each picture's shape), lightbox: false makes it a plain grid. Any single image block opens the same viewer with "lightbox": true — in the editor, select the picture and tick Open full-screen.

pricing

Plan cards (or a comparison table) with a toggle between billing periods — up to four, monthly/yearly being the usual two. The toggle is a group of native radio buttons that works with no script; a browser too old for CSS :has() shows every period's price instead of one.

{
  "type": "pricing",
  "attrs": {
    "periods": [
      { "label": "Monthly", "suffix": "/mo" },
      { "label": "Yearly", "suffix": "/yr" }
    ],
    "defaultPeriod": 0,
    "currency": "$",
    "layout": "cards",
    "plans": [
      {
        "name": "Pro",
        "description": "For growing teams",
        "prices": [29, 290],
        "priceNote": "per seat",
        "features": [
          { "text": "Unlimited projects" },
          { "text": "SSO", "state": "no" },
          { "text": "Seats", "state": "text", "value": "Up to 50" }
        ],
        "cta": { "label": "Start trial", "href": "/signup" },
        "highlighted": true,
        "badge": "Most popular"
      }
    ]
  }
}
  • A price is a number (printed with currency, thousands separated, and the period's suffix), a word ("Free", "Custom") printed as written, or null ("—"). prices[i] belongs to periods[i].

  • A feature's state is yes (a check, the default), no (a cross) or text (its value in place of the mark). Each mark is also spoken: "Included", "Not included".

  • highlighted raises and outlines the plan and gives it a badge ("Popular" unless badge says otherwise); its button is the primary style.

  • layout: "table" is the comparison view: a row per feature (in the order they first appear), a column per plan, a check, a cross or the plan's words in each cell, and a header row that stays on screen as you scroll. On a phone the table scrolls sideways in its own box.

  • 1–6 plans, up to 40 features each.

testimonials and logoWall

{
  "type": "testimonials",
  "attrs": {
    "layout": "grid",
    "columns": 3,
    "items": [
      {
        "quote": "It changed how our team ships.",
        "author": "Ana Ruiz",
        "role": "CTO, Acme",
        "avatar": "/api/assets/…",
        "logo": "/api/assets/…",
        "rating": 5,
        "href": "https://…"
      }
    ]
  }
}

layout is grid (default), carousel (the carousel above, columns at a time, with optional autoplay/interval) or single (large, centred). A rating is 1–5 whole stars, spoken as "Rated 4 out of 5". columns is 1–4, and a block holds 1–24 testimonials.

{
  "type": "logoWall",
  "attrs": {
    "title": "Trusted by",
    "logos": [{ "src": "/api/assets/…", "alt": "Acme", "href": "https://acme.com" }],
    "columns": 6,
    "grayscale": true,
    "marquee": false,
    "speed": "normal",
    "size": "md"
  }
}

A logo's alt is the company name — its only words. grayscale greys the logos until hovered or focused; marquee scrolls them sideways forever instead of a grid (see marquee below for how that behaves). columns is 2–8, a wall holds 1–40 logos, size is sm, md or lg, and speed (for marquee) is slow, normal or fast.

stats

{
  "type": "stats",
  "attrs": {
    "layout": "row",
    "columns": 4,
    "animate": true,
    "items": [
      { "value": 12500, "prefix": "$", "suffix": "+", "label": "Saved", "description": "…" }
    ]
  }
}

The final figure ($12,500+) is in the page's HTML. With animate (the default) the digits count up from zero once the block scrolls into view; the number a screen reader hears is the final one, said once. decimals (0–3) fixes the decimal places. layout is row (divided), grid or cards. columns is 2–6, and a block holds 1–12 figures.

marquee

Any blocks — a line of text, logos, buttons — scrolling sideways forever, in CSS alone. speed is slow, normal or fast; direction left or right; pauseOnHover (default on) also pauses while a link inside has focus; fade (default on) fades both edges; gap is none, sm, md or lg. The content appears once to screen readers and the keyboard (the repeat that makes the loop seamless is hidden from both). Under reduced motion it stops and wraps onto lines.

{
  "type": "modal",
  "attrs": { "label": "Watch the demo", "title": "Demo", "style": "primary", "size": "md" },
  "content": ["…blocks…"]
}

A button (style primary, secondary or link) that opens a <dialog> holding any blocks — a video, a form, a table. size is sm, md, lg or full. It uses the browser's own modal dialog: focus moves in and is trapped, Esc closes it, and focus returns to the button. In browsers with invoker commands it opens with no script at all; elsewhere the reader's script opens it. The dialog's words are in the page's HTML either way. It is a block of its own rather than an option on button, because a button holds no blocks.

hero and person

The two landing-page primitives. A hero is the band a page opens with — the words, the buttons and a picture, laid out properly in one block instead of six; put it in a section for what is behind it (a colour, a gradient, a picture, a video). A person is one member of a team grid: put several in a grid.

{
  "type": "hero",
  "attrs": {
    "eyebrow": "New",
    "title": "Ship docs your users actually read",
    "level": 1,
    "sub": "One sentence on who it is for.",
    "actions": [
      { "label": "Get started", "href": "/signup" },
      { "label": "Talk to us", "href": "/contact", "style": "secondary" }
    ],
    "note": "No credit card required",
    "media": { "kind": "image", "src": "/api/assets/…", "alt": "The editor" },
    "layout": "split",
    "mediaSide": "end",
    "size": "lg"
  }
}

layout is center (default: everything centred, the picture under the buttons), split (words beside the picture from 900px up; on a phone the words come first whichever side the picture takes — mediaSide start or end) or below (words above a wide picture). size is the headline's scale: md, lg (default) or xl. level is 1 (default — the hero's headline is the page's; hide the page title with hideTitle so there is one H1) or 2. Up to 3 actions; unset style makes the first primary and the rest secondary. media is a picture (kind: "image") or a muted, looping video (kind: "video", src a library path or https URL, optional poster) that is never sent to a reader who asked for reduced motion. A picture carries its stored size, so the hero reserves its box before it loads.

{
  "type": "person",
  "attrs": {
    "name": "Ada Lovelace",
    "role": "Head of Engineering",
    "photo": "/api/assets/…",
    "bio": "Writes the first program.",
    "links": [{ "label": "LinkedIn", "href": "https://…" }],
    "layout": "card",
    "photoShape": "circle"
  }
}

layout is card (default: photo above, centred) or row (photo beside the words); photoShape is circle (default), rounded or square. With no photo the person's initials stand in, on a disc tinted with the brand colour. photoAlt defaults to the name. The name is not a heading, so a grid of twelve does not put twelve names in the page's outline. Up to 6 links.

In markdown both travel as one encoded directive, :::hero{data="…"}::: and :::person{data="…"}::: — export one to get it.

officeMap

Places — offices, venues, stores — each drawn as a card with a small street map under a pin: a careers page's "Where we work", a contact page's offices.

{
  "type": "officeMap",
  "attrs": {
    "columns": 3,
    "zoom": 15,
    "aspect": "wide",
    "places": [
      {
        "name": "Berlin",
        "address": "Torstraße 1, 10119 Berlin",
        "lat": 52.5297,
        "lng": 13.4013,
        "note": "Hybrid, three days a week",
        "href": "/jobs/…?location=Berlin"
      }
    ]
  }
}

A place is its coordinates (lat −85.05…85.05, lng −180…180): nothing is geocoded. In the editor, paste what any map's "share" gives — 52.52, 13.405, an openstreetmap.org link or a Google Maps @lat,lng link — into Paste coordinates. zoom is 2 (a continent) to 18 (a building), default 15, and a place may set its own. aspect is wide (2:1, default), landscape (4:3) or square; showMap: false keeps the cards without maps. Up to 24 places; columns 1–4 (default 3), two to a row on a tablet, one on a phone.

The maps are OpenStreetMap's, but a reader's browser never asks OpenStreetMap for anything: each tile is fetched once by our server through the reader's image proxy and served from our own CDN, and the page reserves each map's box before a tile arrives. Where there is no proxy (a feed, an email, the editor's preview) the card shows the box and pin without tiles. The required "© OpenStreetMap contributors" credit and an "Open map" link are on every map.

In markdown it travels as one encoded directive, :::offices{data="…"}:::.

The section library

Whole bands — "Hero – split", "Feature grid – 3 up", "Alternating rows", "Bento – 5 cells", "Pricing – 3 tiers", "FAQ", "Team grid", "Call to action", "Newsletter" and more — insert from the slash menu (the Sections group), the block palette, and the studio's + beside any block. Each is ordinary blocks in a section with role: "layout", so every part of it is edited like the rest of the page. An agent gets the same list, as block JSON ready to splice into a page's content, from GET /section-library (filter with ?family=hero|features|proof|pricing|company|conversion) or one entry from GET /section-library/{id}. The section library gallery shows every one, with its id, a picture and its JSON.

imageCompare — before and after

{
  "type": "imageCompare",
  "attrs": {
    "before": "/api/assets/…",
    "after": "/api/assets/…",
    "beforeAlt": "The dashboard in 2025",
    "afterAlt": "The dashboard today",
    "beforeLabel": "Before",
    "afterLabel": "After",
    "start": 50,
    "caption": "…"
  }
}

Two pictures, one over the other, and a divider. With no script it shows both at the start split (percent from the left). With script the divider follows a drag anywhere on the picture, and a native slider takes the keyboard: Tab to it, arrows, Home, End — spoken as "40% before".

Interactive blocks in markdown

Every one round-trips. The containers are fences around ordinary markdown, so their words survive any renderer:

:::carousel{label="Tour" autoplay interval="6000" loop perView="3"}
::slide{label="Ship"}
![Deploy](/api/assets/a1 "One click")
::
:::

:::gallery{columns="3" aspect="square"}
![One](/api/assets/a1)

![Two](/api/assets/a2)
:::

:::lightbox
![Opens full-screen](/api/assets/a3)
:::

:::marquee{speed="slow" direction="right" nofade}
Words that scroll
:::

:::modal{label="Watch the demo" title="Demo" size="lg"}
Anything here.
:::

:::compare{before="/api/assets/b" after="/api/assets/a" beforeLabel="Old" start="40"}:::

noarrows, nodots, nolightbox, nopause and nofade turn off what is on by default. Pricing, testimonials, logos and stats are tables of facts with no markdown spelling, so — like a form — each is one line with its attributes encoded: :::pricing{data="…"}:::, :::testimonials{data="…"}:::, :::logos{data="…"}:::, :::stats{data="…"}:::. A payload edited into something invalid is dropped on import rather than failing the page.

Code blocks and code groups

A codeBlock is { type: "codeBlock", attrs, content: [{ type: "text", text }] }. language colours it. Every other attr is optional, and a block with none of them renders exactly as a plain block always has:

Attr

Value

Does

title

string

A filename caption bar (in a code group: the tab)

lineNumbers

true, or a number

A numbered gutter, from 1 or from that number

highlight

"2,5-7" or [2,5,6,7]

Highlighted lines

ins / del

same

Inserted (green) / deleted (red) lines

diffMarkers

true

A + / − beside them — drawn, never copied

focus

same

Every other line dims until the reader hovers or tabs to the block

mark

["apiKey"] (≤ 20, each ≤ 80 chars)

Those words marked wherever they occur

maxLines

3–500

Clamped with a "Show all N lines" button

collapse

same as highlight

Those lines folded behind "N collapsed lines"

wrap

true / false

Wrap long lines, or never, whatever the theme says

frame

terminal · editor · none

A terminal's dot bar, the title as an editor tab, or opt out of the theme's terminal bars

src

https URL

The code is fetched from there when the draft is saved (see below)

srcLines

"10-40", "10-", "-40"

Keep only those lines

preview

true or {pattern, background, backgroundDark, css, align, minHeight}

For language: "html": the markup drawn above its source

twoslash

true

For TypeScript and JavaScript: type hovers, // ^? answers and compiler errors (see below)

Line ranges are block-relative: line 1 is the first line of the block, whatever lineNumbers starts the gutter at.

twoslash. For ts, tsx, js, jsx, mts and cts blocks. When the page is published, the server checks the block with the TypeScript compiler and stores the results on the published block. Readers' pages never run a compiler or contact a third party. The reader shows:

  • the type of every name when it is hovered or focused. The block is one tab stop, and the arrow keys move between names.

  • // ^? answers. A line containing only // ^? is hidden, and the type at the caret's column on the line above is shown under that line. It is never copied by the copy button.

  • compiler errors, with a wavy underline and their message on hover.

  • // ---cut---. That line and every line above it are checked but not shown, so setup code can stay out of the sample.

The block is checked as one file with the standard library (ES2022 and the DOM), in strict mode, without npm packages. A "cannot find module" error from an import is therefore not shown, and names from a package have no type. The results are computed again on every publish. Each page can have up to 12 twoslash blocks of up to 20,000 characters each. In Markdown, add the flag to the fence: ```ts twoslash. In the editor, it is the Type hovers switch in a TypeScript block's options.

language: "ansi" is terminal output: its colour escapes become colours (on the server, so they show with JavaScript off) and every other escape sequence is dropped.

src. Fetched at save, not at publish — publishing ships what was reviewed — with a 5-second timeout, 200 KB, ten blocks per save, through the same guard as every outbound URL. A github.com/…/blob/… URL is read from raw.githubusercontent.com, and a #L10-L40 permalink is the slice when srcLines is unset. The server stamps srcFetchedAt; send srcFetchedAt: null (the editor's Refresh from source) to fetch again. A failed fetch keeps the code the block had and says so on X-Field-Warnings.

preview runs no script: the markup goes through the same sanitiser as a custom HTML block, and css is scoped to the preview. pattern is stripes, dots or grid. For code a reader can run, use the playground block (below).

A code group shows the same sample in several languages, as tabs in one code header:

{
  "type": "codeGroup",
  "attrs": { "picker": "dropdown", "syncKey": "pm", "title": "Install" },
  "content": [
    {
      "type": "codeBlock",
      "attrs": { "language": "bash", "title": "npm" },
      "content": [{ "type": "text", "text": "npm i postwick" }]
    },
    {
      "type": "codeBlock",
      "attrs": { "language": "bash", "title": "yarn" },
      "content": [{ "type": "text", "text": "yarn add postwick" }]
    }
  ]
}

It holds 1–12 code blocks and nothing else. A child's tab label is its title, else its language's name, else "Tab N". picker is tabs (default) or dropdown; syncKey (letters, digits, dashes) keeps every group with the same key on the same tab, and a group also follows ordinary tabs with the same titles and remembers the reader's pick. With JavaScript off the first sample shows. In the editor, Code group in the slash menu inserts one; a tabs block whose every tab holds a single code block offers Convert to code group in the code block's bubble. A stored tabs block is never converted for you.

Motion blocks: floating layers, sticky stories, code demos

Three blocks for pages that are art-directed rather than only written: a picture or a giant word that floats over its section and drifts as you scroll, a picture that stays put while the steps beside it scroll past, and a code demo (which does not run yet — see below). Type / and pick Floating layer (Media), Sticky story (Layout) or Code demo (Advanced), or write the JSON below.

For scroll-linked movement of blocks you already have — a hero picture that sinks, a caption that fades as it leaves, a progress bar — name the block with its hook attribute and use the theme's motion.scroll.

Reduced motion turns every drift off, whatever the theme says, and a page with no JavaScript and no scroll timelines is the natural page: every layer where you put it, every step and picture of a story in order.

layer — a picture or a word floating over its section

{
  "type": "layer",
  "attrs": {
    "kind": "text",
    "text": "Typography",
    "x": 6,
    "y": 10,
    "size": 14,
    "font": "heading",
    "weight": 800,
    "color": "brand",
    "z": 0,
    "rate": 0.4,
    "rotate": -4,
    "opacity": 0.9,
    "blend": "multiply",
    "hideOn": ["phone"]
  }
}

Attribute

Values

Default

kind

text (needs text, up to 120 characters) or image (needs src)

required

x, y

Where its top-left corner sits in its section, as percentages (y from -50: a negative y rises above the section's top, which then opens its clip upward)

0, 0

width

A picture's width as a percentage of its section (a word sizes itself)

its own

size

A word's size as a percentage of the screen width, 1–40 (never under 1rem)

8

font, weight

heading, body or mono; 100–900

heading, 800

color

A colour token (bg, surface, panel, ink, muted, brand, accent) or a hex

the text's

z

0 behind the section's words (over its painted background); 1–5 over them

0

rate

Parallax as the page scrolls, −1 to 1: 0.4 drifts at 40% of the scroll speed (far away), -0.3 rushes ahead (near). 0 is still

0

rotate

Degrees, ±180

0

opacity

0–1

1

blend

How it mixes with what is behind it: normal, multiply, screen, overlay, darken, lighten, difference, exclusion, soft-light, hard-light

normal

hideOn

Any of phone, tablet

shown

Inside a full-bleed section, x and width are measured across the viewport, not the column, and the layer is clipped at the screen's edges rather than the column's.

Put layers inside a section: the section becomes their frame (positioned, its own stacking context, and clipped, so a layer pushed past an edge never widens the page). A layer is decoration — hidden from screen readers, never clickable, not part of search — so never put words in one that a reader needs. In the editor it is shown in place, not floating, so you can edit it.

story — a picture that sticks while the steps scroll past

{
  "type": "story",
  "attrs": { "label": "The firing", "side": "end", "mediaWidth": "wide", "top": 96 },
  "content": [
    {
      "type": "storyStep",
      "attrs": { "media": "/api/assets/…", "alt": "The kiln at dawn", "caption": "Day one" },
      "content": [
        { "type": "paragraph", "content": [{ "type": "text", "text": "Loading the kiln." }] }
      ]
    },
    {
      "type": "storyStep",
      "content": [
        { "type": "paragraph", "content": [{ "type": "text", "text": "Twelve hours later." }] }
      ]
    }
  ]
}

Attribute (story)

Values

Default

label

The region's name for screen readers

"Story"

side

Which side the picture is on: start (left), end

start

mediaWidth

narrow, half, wide

half

top

px from the top of the screen the picture sticks at, 0–240

96

A story holds 1–12 storySteps, and a step holds ordinary blocks. A step's media (with alt and caption) is the picture shown while that step is the one being read; a step without one keeps the picture before it. On a phone the picture sticks across the top and the steps scroll up over it.

With no script, the story is every step and every picture, in order, the picture column sticky in CSS alone. The reader's script (fetched only on a page with one) stacks the pictures into one frame and changes it as each step reaches the middle of the screen; under reduced motion the change is instant rather than a crossfade.

playground — a runnable code demo

{
  "type": "playground",
  "attrs": {
    "title": "Easing, side by side",
    "html": "<div class=\"box\"></div>",
    "css": ".box { transition: transform .3s ease-out }",
    "js": "console.log('ready')",
    "layout": "split",
    "editable": true,
    "height": 320,
    "fallbackSrc": "/api/assets/…",
    "fallbackAlt": "Two boxes, one easing out",
    "fallbackCaption": "Ease-out on the left"
  }
}

A playground shows its HTML, CSS and JavaScript in editors next to a live preview. Readers can edit the code (unless editable is false), run it again, reset it to your version, and read what it logs to the console.

Attr

Meaning

html, css, js

The sources, up to 20,000 characters each. Only the sources you set are shown.

tabs

The order of the editors, for example ["js", "html"]. Default: HTML, CSS, JavaScript.

layout

split (code beside the preview, stacked on a phone), stacked (code above), or preview (the code folded away).

editable

false makes the editors read-only and removes Reset. Default true.

height

The preview's height in pixels, 120 to 1200 (default 320). The space is reserved before the demo loads, so the page does not move.

fallbackSrc, fallbackAlt, fallbackCaption

A picture shown in the preview until the demo runs, and instead of the playground when demos cannot run.

Where the code runs. Demo code never runs on your site's address or on ours. It runs in an isolated frame on a separate sandbox domain that has no cookies, no session and no network access. The demo cannot call APIs, load third-party scripts or send data anywhere. Images from https: URLs in your code still load. The demo runs when it scrolls into view.

When demos cannot run. If the platform has no sandbox domain configured, the editor says so on the block. Readers then see only the fallback picture, or nothing if there is none, and the code is not put into the page. Your code is kept, and the playground starts working as soon as a sandbox is available.

Motion blocks in markdown

:::layer{kind="text" text="Typography" x="6" y="10" size="14" z="0" rate="0.4" hideOn="phone"}:::

:::story{side="end" mediaWidth="wide" top="96"}
::scene{media="/api/assets/a1" alt="The kiln at dawn" caption="Day one"}
Loading the kiln.
::
::scene
Twelve hours later.
::
:::

:::playground{title="Easing" layout="split" fallback="/api/assets/p1" fallbackAlt="Two boxes"}
…one fenced code block per source, tagged html, css or js…
:::

A story's steps are ::scene (::step is the stepper's). A playground's sources are ordinary fenced code blocks tagged html, css and js; anything else inside the fence is kept after the block. noedit sets editable: false. A layer whose values are out of range is dropped on import rather than failing the page.

Front-page blocks

Three blocks bring a space's live content into a document. They are what a space front page is built from, and they work in its sidebar too. None has children — a feed's content is data, fetched when the page is read, never stored in the document. Each takes the three block hooks.

spaceFeed

{
  "type": "spaceFeed",
  "attrs": {
    "space": "self",
    "filter": { "tags": ["release"], "featured": true },
    "sort": "latest",
    "limit": 6,
    "layout": "grid",
    "columns": { "sm": 1, "md": 2, "lg": 3 },
    "heading": "Featured",
    "moreLink": { "label": "All posts" }
  }
}

Every attribute is optional and nullable.

Attribute

Value

space

self (default — the page's own space) or another space's id in the same organization

filter

{ category?, tags?, tagMatch?, pinned?, featured?, exclude?, author?, lane?, version?, when?, section? } — see below

sort

latest (default), top, hot, oldest — each type answers the ones its public list honours

period

day, week, month, year, all (default) — the window top and hot rank over

limit

1–50, default 6. Ignored when paginate is on

layout

feed (default), grid, magazine, rows, compact, tiles, cards-mixed, split, bento, carousel

columns

{ sm?, md?, lg?, offsets? }, each 1–6 — phone, tablet, desktop; offsets up to 6 pixel counts (0–400), a staggered grid

fields

up to 16 card field names; unset = the space shape's summary fields

paginate

true: page through the list instead of showing a window; "more": one "Show more" button that appends in place

pageSize

1–50, default 10 (or limit, if set) when paginating

emptyText

≤ 300 characters, shown when nothing matches

heading

≤ 200 characters, an <h2> above the feed — or { text?, subtitle?, style? }, style plain or rule-top

moreLink

{ label?, href?, position?, style? } — a "see all" link; no href links to the target space. top/below (or {lg, md, sm} of them), link/button

card

the composed card — { slots?, image?, kicker?, tagStyle?, dateStyle?, metaLayout?, byline?, readMore?, titleSize?, titleWeight?, excerptLines?, surface? }, every object strict. See Designing the card

layoutSm

a layout for phones, when it differs

pattern

a bento's items per row, up to 12 numbers of 1–6, the last repeating

lead

{ style?, titleSize? }, style stack, split, overlap, overlay, left-column

rest

cards (default) or text-only

perView

{ sm?, md?, lg? }, 1–6 — a carousel's items in view

offset

0–50 items to skip first

dedupe

true: leave out items an earlier feed on the page drew

tabs

{ tags?, limit?, allLabel?, style? } — tag tabs over the feed that filter it in place

controls

true: the space's own reading list — follows ?sort=, ?category=, ?page= and carries the list's sort tabs and filters

The filter keys: category (a community category slug, ≤ 120), tags (up to 10, each ≤ 60) with tagMatch any (default) or all, pinned and featured (booleans — false keeps only the rest), exclude (up to 50 of pinned, featured or an item id), author (a display name), lane (a roadmap status), version (a changelog version), when (upcoming or past, for events) and section (a docs collection, FAQ section or careers department). A key the target's type does not honour is ignored; the per-type table is in Space front pages.

Card fields per type: blog title, standfirst, cover, author, date, readTime, tags, series, replies; community title, author, replies, views, date, category, tags, excerpt; changelog title, summary, date, version, tags; roadmap title, status, votes, excerpt; events title, date, location, excerpt; careers title, location, employment, department; FAQ and courses title, section; docs and custom title, section, excerpt.

A feed whose target space this reader may not see — private, gated, deleted or in another organization — renders nothing at all, heading included.

spaceCategories

{
  "type": "spaceCategories",
  "attrs": { "layout": "tiles", "fields": ["color", "count", "topTopics"], "topTopics": 3 }
}

Attribute

Value

space

self (default) or a community space's id in the same organization; any other type renders nothing

layout

grid (default), tiles, list

columns

{ sm?, md?, lg? }, each 1–6

fields

any of color, image, logo, count, description, topTopics; default all but topTopics

topTopics

0–5, how many top topics a tile lists when the topTopics field is on (default 3)

limit

1–50 categories

parent

a category slug: only its subcategories

heading

≤ 200 characters

emptyText

≤ 300 characters. With no categories and no emptyText, the block renders nothing

moreLink

{ label?, href? }

spaceBand

{ "type": "spaceBand", "attrs": { "band": "search" } }

band is required: one of the space type's own front-door bands (intro, search, subscribe, lanes, board, roles, tags, …), rendered as the reader always drew it. The names each type offers, with the options each takes, are served by GET /space-types/{type}/presets (frontPage.bands, frontPage.bandOptions) and listed in Space front pages. A band the type does not have renders nothing.

Band

Options beside band

tags

tags, limit (1–30), style (pills, tabs, text), showAll, allLabel, showSearch, rss (none, icon, link)

subscribe

layout (stacked, inline-row), heading, body, buttonLabel, placeholder, inputStyle (box, underline), frame (card, none)

pagination

style (links, button)

An option on a band that does not take it is a 422 naming its path.

Front-page blocks in markdown

:::feed{space="self" layout="magazine" sort="top" period="week" limit="6"}:::
:::feed{category="announcements" pinned tags="release,beta" heading="News"}:::
:::categories{layout="tiles" fields="color,count,topTopics" topTopics="3"}:::
:::band{name="search"}:::

Each is one self-closing directive. cols, colsMd and colsSm are the desktop, tablet and phone columns; fields and tags are comma lists; more and moreHref are the moreLink (moreAt, moreStyle); empty is emptyText; and the bare flags paginate, controls, dedupe, pinned and featured mean true (paginate="more" is the Show-more button). card, lead and tabs are JSON in one attribute; a band's options ride on its directive (:::band{name="tags" style="tabs" rss="icon"}:::). A directive that no longer validates is dropped on import, never stored.

API reference blocks

The openapi block's own attributes

An operation block ({ "type": "openapi", "attrs": { "operation": … } }) takes two optional attributes that override the space's theme.docs.api for that one block:

Attr

Values

What it does

layout

stacked, split

split puts samples, responses and "Try it" in a sticky pane beside the operation at ≥1280px (on a page with exactly one top-level operation block, which then has no on-this-page rail).

params

table, list

Parameters and fields as a table, or as rows with nested children behind a disclosure.

metadata.api: "POST /v1/refunds" builds the block for you from a spec registered in the space — see Importing.

walkthrough — code beside the steps that explain it

One piece of code in a sticky pane, and a list of steps. Each step names the lines it is about; scrolling to a step (or focusing or clicking it) lights those lines. Below 1024px it is one column, and each step shows its line range.

{
  "type": "walkthrough",
  "attrs": {
    "language": "ts",
    "title": "server.ts",
    "code": "const client = createClient();\nawait client.ping();"
  },
  "content": [
    {
      "type": "walkthroughStep",
      "attrs": { "lines": "1", "title": "Create the client" },
      "content": [
        { "type": "paragraph", "content": [{ "type": "text", "text": "One client per process." }] }
      ]
    }
  ]
}

lines uses a code block's highlight syntax: 3, 3-7, 3-7,9. A walkthrough needs at least one step, and a step lives only inside a walkthrough. In the editor it is Code walkthrough in the slash menu.

Docs blocks (wave D)

Every attribute below is optional; unset draws the block exactly as before. The live schema (GET /docs/block-schema) lists each one, and now also a number's min/max and a description where the schema has one.

Block

New attributes

hint

kind (note/tip/caution/important/check — the author's word; {style:"tip"} stores style:"success", kind:"tip"), title, icon

tabs

syncKey (a slug: groups with the same key switch together, across pages; ?tabs=<key>:<Title> selects one), overflow (wrap/scroll), orientation (horizontal/vertical)

cards

columns (1–6, or {lg, md, sm}), variant (default/horizontal/panel/compact/row)

card

iconUrl, iconStyle (plain/tile/circle), color, cta, kicker, badge

stepper

headingLevel (2–4: each step title is a real heading the page outline indexes), titleSize (12–32 px), collapsible

step

icon, badges (≤ 4), aside (the step's last block sits beside it on wide screens)

column

width is a percent of the row, 5–95; role: "aside" and sticky (the docs page lifts the column into its rail); fit: "content" — as wide as what it holds (a button beside a heading), width ignored, never stretched to the full row on a phone; align: "end" — pushed to the row's end edge

gridItem

border is true or a colour; gradient / gradientDark (JSON only)

table

collapseAfter (3–200 body rows, then "Show more"), stickyHeader. A table's leading header rows are now a <thead>

image

srcDark

prompt

variant (card/compact), wrap, targets (subset of chatgpt,claude,codex,cursor), showText, actions (copy, open)

button

width: "full". Unset is now the label's width on a docs page too (it stretched in docs and not on a front page)

updates

format: "list" — date, a linked title, a one-line summary; the page offers …/rss.xml

heading

variant: "code"

bulletList / orderedList

marker: "arrow-end", rule (true or a colour)

New blocks:

Block

Shape

field

attrs: {name, type?, required?, default?, deprecated?, location?: path/query/body/header/response, badges?}, content: the description plus nested fields

example

attrs: {kind: request/response, title?}, content: code blocks; a docs page shows the top-level ones in its rail at ≥ 1280 px

fileTree

content: one nested bulletList

swatch

attrs: {colors: [{name, value: "#hex", note?}] (1–24), layout?: grid/row}

buttons

attrs: {align?}, content: buttons

expandableGroup

attrs: {exclusive?, style?: row/card/bordered}, content: expandables

New marks: mathInline (the text is the LaTeX), badge (attrs.variant), icon (attrs.name, a glyph; the text is its label). The editor now keeps kbd marks.

A section nested in a column, a grid cell or another section no longer needs a key: it is given band-<n> (and role: "layout") and the write says so on X-Field-Warnings. A top-level section still needs its key.

Round 3 block attributes

Every key below is optional and unset draws the block exactly as before — except the ones marked fix, which change what an existing page shows. The live schema (GET /docs/block-schema) lists each one; the markdown spelling of each is in Markdown in Forumulate › Round 3.

Block

Attribute

Value

tabs

asCodeGroup

fix. Unset (or "auto"): a tab group whose EVERY tab holds exactly one code block draws as a code group — the tab titles become the code header's tabs, and syncKey still links it to the page's other groups. false keeps the plain tabs. A vertical strip never converts.

tabs

variant

underline (default) or card: the group in a bordered card, its strip as the card's header

prompt

variant

card (default), compact, or pill: one line — logos first, the prompt (cut with an ellipsis), the copy glyph last. A pill's action is copy unless you set actions

prompt

align

start (default) or center (centred in its column)

prompt

targets

as before, provider ids (chatgpt, claude, codex, cursor) narrow the "Open in …" links; an entry {"icon": …, "label": …} is a LOGO drawn at the prompt's start. Up to 6 entries in all. icon is a glyph name, an emoji or an image address (https://…, /api/assets/…); label (≤ 40) names it for a screen reader

prompt

textStyle

mono (default) or prose: the text's markdown is drawn — bold, code, links, lists

prompt

collapse

1–40: the body is folded to that many lines, faded, behind "Show more"

heading

icon, iconStyle

an icon before the heading's words (glyph name, emoji or image address); inline (default) or tile, a 32 px tinted square. The heading's text — what the page outline lists — is unchanged

heading

lineHeight

0.9–2, unitless: a 56 px hero set at 1

gridItem

textAlign

fix (it was refused). start, center, end: the cell's text, and its icon and buttons, line up with it

gridItem

icon, iconStyle

the cell's icon, above its content; tile is the tinted square

section

pattern, patternColor, patternFade

a dotted or squared backdrop — see A section's look

Fix: grid cells now stretch to their row's height, so a row of tiles is one height, and a cell's align (center, bottom) places its content inside the full-height cell instead of shrinking the cell.

Fix: a code fence marked text (or plaintext, txt, plain, none) no longer shows a "TEXT" language chip.

{
  "type": "gridItem",
  "attrs": {
    "colSpan": 1,
    "textAlign": "center",
    "icon": "rocket",
    "iconStyle": "tile",
    "border": true
  },
  "content": [
    {
      "type": "heading",
      "attrs": { "level": 3 },
      "content": [{ "type": "text", "text": "Quickstart" }]
    }
  ]
}
{
  "type": "prompt",
  "attrs": {
    "text": "Add authentication to my Next.js app",
    "variant": "pill",
    "align": "center",
    "targets": [{ "icon": "https://example.com/cursor.svg", "label": "Cursor" }, "claude"]
  }
}

A docs space can also set the look its prompts take when they name none — theme.docs.blocks.prompt.variant; see Theme a documentation space › Blocks. A prompt that names its own variant keeps it.

On a page with metadata.hideTitle, the first heading is the page's hero: the reader marks it data-hero-heading, and the on-this-page rail leaves it out.

In the editor, a grid cell's Style popover has the text alignment, the icon and its style; every other key above is kept when you edit the page.

Round 4 block attributes

Each attribute mirrors a theme key of the same name and wins over it for this block alone (block > page metadata.themePatch > space theme > look > today — see Theme components › Theme, page or block), so two blocks on one page can look different. Every one is optional and unset draws the block exactly as before. The live schema (GET /docs/block-schema) lists each, with its values and bounds; the markdown spelling is in Markdown in Forumulate › Round 4.

Block

Attribute

Value

cards

border

card (the outline), tile (only the icon tile is ruled — a picture icon too), none

cards

iconAlign

start or center; beside the icon (horizontal, row), center centres the title on it

cards

iconSize, tileSize

px, 12–64 and 32–96

cards

titleWeight, titleSize, descriptionSize

400/500/600/700; px 12–24; px 12–24

cards

background, backgroundDark

#hex fills, light and dark

cards

ctaPlacement

below or end (a horizontal card's CTA at the row's end)

hint

tone

one #hex for the bar, the badge and the wash

hint

titleGap

px 0–24 under the title

hint

edge

bar, hook or none

codeBlock

look

bordered, flat or inset

codeBlock

showLanguage

true (everywhere, TEXT on plain text), false, "auto" (not in a group, not on plain text). A titled block never shows a chip

codeBlock

titleFont, titleIcon

mono/ui; a file glyph before the title

codeGroup

look, showLanguage, titleFont, titleIcon

as on a code block, for the whole group

codeGroup

tabStyle

underline or pill

codeGroup

lineNumbers

true or a start line: every sample that does not set its own is numbered

table

wrap

auto (today), wrap, nowrap

table

more

{style?, align?, fade?, background?, backgroundDark?, ink?, inkDark?, font?: ui/mono, case?: none/upper} — the "Show more" button

table

codeInk, codeInkDark

up to six #hex, inline code ink per column, first column first

tabs

active

{color?, colorDark?, underline?} — the active tab

tabs

tabStyle

pill draws the strip as pills (a code-only group passes it to its code group)

prompt

layout

inline (today) or stacked (the actions under the text)

prompt

primary

copy or open: that action filled in the brand

prompt

icon

a glyph name drawn before the label

prompt

header

tabs: a prompt alone in a tab panel puts its actions in the tab strip, and drops its label row

prompt

copyStyle

button (the words) or icon (the glyph)

prompt

phoneTargets

1–6: on a phone a compact prompt keeps Copy and this many "Open in …" links; the rest are under More

heading

iconColor

#hex: the heading icon's tint (its tile's fill and border)

gridItem

accent

#hex: the cell icon's tint

columns

gap

one space value as before, or {lg, md, sm} — a gap per screen size, each step filled from the one above

section

patternColor

a colour as before, or {from, to, angle?}: the dots (or grid) cut from a gradient

On a wide or full docs page a row of columns now spans the article, as a layout grid does.

Fix: a one-line code block with no title puts its copy glyph inside the line, at the end, at every width — no band above it.

Fix: four cards with descriptions and no columns go four across only on a track of 1000px or more (two by two below it); four title-only cards keep the 720px rule.

Fix: a wide table on a phone fades its end edge while it can still scroll, and an inline code chip cut by a line break keeps its corners.

{
  "type": "codeGroup",
  "attrs": { "tabStyle": "pill", "look": "inset", "lineNumbers": true },
  "content": [
    {
      "type": "codeBlock",
      "attrs": { "language": "bash", "title": "npm" },
      "content": [{ "type": "text", "text": "npm i @acme/sdk" }]
    },
    {
      "type": "codeBlock",
      "attrs": { "language": "bash", "title": "pnpm" },
      "content": [{ "type": "text", "text": "pnpm add @acme/sdk" }]
    }
  ]
}
{
  "type": "section",
  "attrs": {
    "key": "hero",
    "pattern": "dots",
    "patternColor": { "from": "#f97316", "to": "#8b5cf6", "angle": 120 },
    "patternFade": "radial"
  },
  "content": [
    { "type": "paragraph", "content": [{ "type": "text", "text": "Payments, everywhere." }] }
  ]
}

In the editor, a card grid has a Look button over it with every cards attribute above; a grid cell's Style popover has the icon's tint. Every other attribute is kept when you edit the page.

Round 5 block attributes

The same contract as round 4: each attribute mirrors a theme key of the same name and wins over it for this block alone; every one is optional and unset draws the block as before. Theme keys and the DOCS layer's defaults: Theme components › Round 5. Markdown: Markdown in Forumulate › Round 5.

Block

Attribute

Value

cards

minWidth

px 160–480: the narrowest a card gets — the grid shows min(columns, floor(width ÷ minWidth)) columns

cards

gap, rowGap

a space token or px, or {lg, md, sm} (each step filled from the one above): the room between cards, and between rows

cards

divider

boolean: a panel card's rule under its icon band (false drops it)

cards

iconBand

px 0–120: a panel card's icon band height

tabs

style

underline (today) or pill — a bordered segmented strip, the active segment filled from active.color (else the brand)

prompt

labelCase

upper (today) or none

prompt

background, backgroundDark

#hex: the prompt's fill, light and dark

prompt

collapseStyle

footer (today) or pill: a floating "⌄ Show more" over the fade

prompt

openInDesktop

0–6: the "Open in …" links kept in the row from 641px; the rest are under More

codeBlock

preview.attach

separate (today) or code: the preview and its source as one bordered card

The theme's docs.blocks.prompt.icon now draws on every prompt that names no icon of its own.

{
  "type": "cards",
  "attrs": {
    "columns": 3,
    "minWidth": 260,
    "gap": { "lg": 16, "sm": 12 },
    "variant": "panel",
    "divider": false,
    "iconBand": 88
  },
  "content": [
    {
      "type": "card",
      "attrs": { "title": "Deploy", "icon": "rocket", "description": "Ship from the CLI." }
    }
  ]
}
{
  "type": "prompt",
  "attrs": {
    "text": "Add auth to my app",
    "variant": "compact",
    "openInDesktop": 2,
    "collapseStyle": "pill",
    "labelCase": "none"
  }
}

In the editor, a card grid's Look button has min width, gap, row gap, panel divider and icon band; the other attributes are kept when you edit.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Ai

Operation

Request

What it does

Shortcuts

Ai_review

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/ai/review

Create the review in a space (pages AI)

client.ai.review()

Ai_rewrite

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/ai/rewrite

Create the rewrite in a space (pages)

client.ai.rewrite()

Ai_translate

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/ai/translate

Create the translate in a space (pages AI)

client.ai.translate()

Ai_ask

POST /orgs/{orgId}/spaces/{spaceId}/docs/ask

Create the ask in a space (docs)

client.ai.ask()

Ai_askStream

POST /orgs/{orgId}/spaces/{spaceId}/docs/ask/stream

Create the stream in a space (docs)

client.ai.askStream()

Ai_askFeedback

POST /orgs/{orgId}/spaces/{spaceId}/docs/ask/{queryId}/feedback

Create the feedback in a space (docs ask)

client.ai.askFeedback()

Ai_agent

POST /orgs/{orgId}/spaces/{spaceId}/docs/agent

Create the agent in a space

client.ai.agent()

Block Schema

Operation

Request

What it does

Shortcuts

BlockSchema_get

GET /docs/block-schema

Get the block schema

client.meta.blockSchema.get()

Change Request

Operation

Request

What it does

Shortcuts

ChangeRequest_agentReview

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/agent-review

Create the agent review in a space

forumulate change-requests agent-review<br>client.changeRequests.agentReview()

ChangeRequest_agentRun

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/agent-run

Create the agent run in a space

forumulate change-requests agent-run<br>client.changeRequests.agentRun()

ChangeRequest_list

GET /orgs/{orgId}/spaces/{spaceId}/change-requests

List change requests in a space

forumulate change-requests list<br>client.changeRequests.list()

ChangeRequest_create

POST /orgs/{orgId}/spaces/{spaceId}/change-requests

Create a change request in a space

forumulate change-requests open<br>client.changeRequests.create()

ChangeRequest_mergeRules

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/merge-rules

List merge rules in a space

forumulate change-requests rules get<br>client.changeRequests.mergeRules()

ChangeRequest_setMergeRules

PUT /orgs/{orgId}/spaces/{spaceId}/change-requests/merge-rules

Replace merge rules in a space

forumulate change-requests rules set<br>client.changeRequests.setMergeRules()

ChangeRequest_mergeBlockers

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/merge-blockers

List merge blockers in a space

forumulate change-requests blockers<br>client.changeRequests.mergeBlockers()

ChangeRequest_conflicts

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/conflicts

List conflicts in a space

forumulate change-requests conflicts<br>client.changeRequests.conflicts()

ChangeRequest_eligibility

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/eligibility

Get the eligibility in a space

forumulate change-requests eligibility<br>client.changeRequests.eligibility()

ChangeRequest_get

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}

Get the change request (spaces)

forumulate change-requests get<br>client.changeRequests.get()

ChangeRequest_pages

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/pages

List pages in a space (orgs change requests)

forumulate change-requests pages<br>client.changeRequests.pages()

ChangeRequest_requestReview

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/request-review

Create the request review in a space

forumulate change-requests request-review<br>client.changeRequests.requestReview()

ChangeRequest_approve

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/approve

Approve the change request

forumulate change-requests approve<br>client.changeRequests.approve()

ChangeRequest_merge

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/merge

Merge the change request

forumulate change-requests merge<br>client.changeRequests.merge()

ChangeRequest_rebase

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/rebase

Create the rebase in a space

forumulate change-requests rebase<br>client.changeRequests.rebase()

ChangeRequest_listReviewers

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/reviewers

List reviewers in a space

forumulate change-requests reviewers list<br>client.changeRequests.listReviewers()

ChangeRequest_addReviewers

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/reviewers

Create a reviewer in a space

forumulate change-requests reviewers add<br>client.changeRequests.addReviewers()

ChangeRequest_removeReviewer

DELETE /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/reviewers/{reviewerId}

Delete the reviewer

forumulate change-requests reviewers remove<br>client.changeRequests.removeReviewer()

ChangeRequest_review

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/review

Create the review in a space (change requests)

forumulate change-requests review<br>client.changeRequests.review()

ChangeRequest_close

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/close

Close the change request

forumulate change-requests close<br>client.changeRequests.close()

ChangeRequest_comments

GET /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/comments

List comments in a space (change requests)

forumulate change-requests comments<br>client.changeRequests.comments()

ChangeRequest_comment

POST /orgs/{orgId}/spaces/{spaceId}/change-requests/{crId}/comments

Create a comment in a space (change requests)

forumulate change-requests comment<br>client.changeRequests.comment()

Collection

Operation

Request

What it does

Shortcuts

Collection_list

GET /orgs/{orgId}/spaces/{spaceId}/collections

List collections in a space

forumulate collections list<br>client.collections.list()

Collection_create

POST /orgs/{orgId}/spaces/{spaceId}/collections

Create a collection in a space

forumulate collections create<br>client.collections.create()<br>client.spaces.createCollection()

Collection_get

GET /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}

Get the collection

forumulate collections get<br>client.collections.get()<br>MCP set_name_translation

Collection_update

PATCH /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}

Update the collection

forumulate collections update<br>client.collections.update()<br>MCP set_name_translation

Collection_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}

Delete the collection

forumulate collections delete<br>client.collections.remove()

Collection_reorder

POST /orgs/{orgId}/spaces/{spaceId}/collections/reorder

Reorder the collection

forumulate collections reorder<br>client.collections.reorder()

Collection_tree

GET /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}/pages

List pages in a space (orgs collections)

forumulate collections pages<br>client.collections.tree()

Collection_createPage

POST /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}/pages

Create a page in a space

forumulate collections pages create<br>client.collections.createPage()<br>client.spaces.createPage()

Collection_order

POST /orgs/{orgId}/spaces/{spaceId}/collections/{collectionId}/order

Create the order in a space

forumulate collections pages order<br>client.collections.order()

Content Rule

Operation

Request

What it does

Shortcuts

ContentRule_list

GET /orgs/{orgId}/content-rules

List content rules in an organization

client.docs.contentRules.list()

ContentRule_create

POST /orgs/{orgId}/content-rules

Create a content rule in an organization

client.docs.contentRules.create()

ContentRule_remove

DELETE /orgs/{orgId}/content-rules/{id}

Delete the content rule

client.docs.contentRules.remove()

Discovery

Operation

Request

What it does

Shortcuts

Discovery_analyticsSummary

GET /orgs/{orgId}/spaces/{spaceId}/docs/analytics

List analytics in a space (orgs docs)

client.docs.discovery.analyticsSummary()

Discovery_feedbackComments

GET /orgs/{orgId}/spaces/{spaceId}/docs/feedback

Get the feedback in a space

client.docs.discovery.feedbackComments()

Discovery_feedbackCsv

GET /orgs/{orgId}/spaces/{spaceId}/docs/feedback.csv

File of feedback

client.docs.discovery.feedbackCsv()

Discovery_llmsFull

GET /orgs/{orgId}/spaces/{spaceId}/docs/llms-full.txt

File of llms full (docs)

client.docs.discovery.llmsFull()

Discovery_tags

GET /orgs/{orgId}/spaces/{spaceId}/docs/tags

List tags in a space (docs)

client.docs.discovery.tags()

Discovery_tagPages

GET /orgs/{orgId}/spaces/{spaceId}/docs/tags/{tag}

Get the tag (spaces docs)

client.docs.discovery.tagPages()

Discovery_skillMd

GET /orgs/{orgId}/spaces/{spaceId}/docs/skill.md

File of skill (docs)

client.docs.discovery.skillMd()

Discovery_llmsTxt

GET /orgs/{orgId}/spaces/{spaceId}/docs/llms.txt

File of llms (orgs spaces docs)

client.docs.discovery.llmsTxt()

Discovery_pageMarkdown

GET /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/markdown

Get the markdown in a space

client.docs.discovery.pageMarkdown()

Discovery_draftMarkdown

GET /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/draft/markdown

Get the markdown in a space (draft)

client.docs.discovery.draftMarkdown()

Discovery_checkLinks

POST /orgs/{orgId}/spaces/{spaceId}/docs/check-links

Create a check link in a space

client.docs.discovery.checkLinks()

Discovery_emptySections

GET /orgs/{orgId}/spaces/{spaceId}/docs/empty-sections

List empty sections in a space

client.docs.discovery.emptySections()

Discovery_orphans

GET /orgs/{orgId}/spaces/{spaceId}/docs/orphans

List orphans in a space

client.docs.discovery.orphans()

Discovery_backlinks

GET /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/backlinks

List backlinks in a space

client.docs.discovery.backlinks()

Discovery_stale

GET /orgs/{orgId}/spaces/{spaceId}/docs/stale

Get the stale in a space

client.docs.discovery.stale()

Form Template

Operation

Request

What it does

Shortcuts

FormTemplate_list

GET /orgs/{orgId}/spaces/{spaceId}/form-templates

List form templates in a space

client.forms.template.list()

FormTemplate_create

POST /orgs/{orgId}/spaces/{spaceId}/form-templates

Create a form template in a space

client.forms.template.create()

FormTemplate_get

GET /orgs/{orgId}/spaces/{spaceId}/form-templates/{id}

Get the form template

client.forms.template.get()

FormTemplate_update

PATCH /orgs/{orgId}/spaces/{spaceId}/form-templates/{id}

Update the form template

client.forms.template.update()

FormTemplate_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/form-templates/{id}

Delete the form template

client.forms.template.remove()

FormTemplate_attach

POST /orgs/{orgId}/spaces/{spaceId}/form-templates/attach

Attach the form template

client.forms.template.attach()

Library

Operation

Request

What it does

Shortcuts

Library_listReusable

GET /orgs/{orgId}/spaces/{spaceId}/reusable-content

Get the reusable content in a space

client.docs.variables.listReusable()

Library_createReusable

POST /orgs/{orgId}/spaces/{spaceId}/reusable-content

Create the reusable content in a space

client.docs.variables.createReusable()

Library_updateReusable

PATCH /orgs/{orgId}/spaces/{spaceId}/reusable-content/{id}

Update the reusable content

client.docs.variables.updateReusable()

Library_deleteReusable

DELETE /orgs/{orgId}/spaces/{spaceId}/reusable-content/{id}

Delete the reusable content

client.docs.variables.deleteReusable()

Library_listVariables

GET /orgs/{orgId}/spaces/{spaceId}/variables

List variables in a space

client.docs.variables.listVariables()

Library_setVariable

PUT /orgs/{orgId}/spaces/{spaceId}/variables

Replace variables in a space

client.docs.variables.setVariable()

Library_deleteVariable

DELETE /orgs/{orgId}/spaces/{spaceId}/variables/{key}

Delete the variable (spaces)

client.docs.variables.deleteVariable()

Org Library

Operation

Request

What it does

Shortcuts

OrgLibrary_list

GET /orgs/{orgId}/library/reusables

List reusables in an organization

client.orgs.library.list()

OrgLibrary_create

POST /orgs/{orgId}/library/reusables

Create a reusable in an organization

client.orgs.library.create()

OrgLibrary_remove

DELETE /orgs/{orgId}/library/reusables/{id}

Delete the reusable

client.orgs.library.remove()

OrgLibrary_listVariables

GET /orgs/{orgId}/library/variables

List variables in an organization

client.orgs.library.listVariables()

OrgLibrary_setVariable

PUT /orgs/{orgId}/library/variables

Replace variables in an organization

client.orgs.library.setVariable()

OrgLibrary_deleteVariable

DELETE /orgs/{orgId}/library/variables/{key}

Delete the variable (library)

client.orgs.library.deleteVariable()

Page

Operation

Request

What it does

Shortcuts

Page_getPage

GET /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}

Get the page (orgs spaces)

forumulate docs get<br>client.pages.getPage()

Page_update

PATCH /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}

Update the page

forumulate docs update<br>client.pages.update()

Page_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}

Delete the page

forumulate docs delete<br>client.pages.remove()

Page_getMetadata

GET /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/metadata

Get the metadata in a space

forumulate docs meta<br>client.pages.getMetadata()<br>MCP set_page_fields

Page_setMetadata

PATCH /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/metadata

Update the metadata in a space

forumulate docs meta set<br>client.pages.setMetadata()<br>MCP set_page_fields

Page_reorder

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/reorder

Reorder the page

forumulate docs move<br>client.pages.reorder()

Page_list

GET /orgs/{orgId}/spaces/{spaceId}/pages

List pages in a space (orgs)

forumulate docs list<br>client.pages.list()

Page_inventory

GET /orgs/{orgId}/spaces/{spaceId}/pages/inventory

Get the inventory in a space

forumulate docs inventory<br>client.pages.inventory()

Page_bulkDelete

POST /orgs/{orgId}/spaces/{spaceId}/pages/bulk-delete

Create the bulk delete in a space

forumulate docs bulk delete<br>client.pages.bulkDelete()

Page_bulkMove

POST /orgs/{orgId}/spaces/{spaceId}/pages/bulk-move

Create the bulk move in a space

forumulate docs bulk move<br>client.pages.bulkMove()

Page_duplicate

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/duplicate

Duplicate the page

forumulate docs duplicate<br>client.pages.duplicate()

Page_trash

GET /orgs/{orgId}/spaces/{spaceId}/pages/trash

List trash pages in a space

forumulate docs trash list<br>client.pages.trash()

Page_emptyTrash

POST /orgs/{orgId}/spaces/{spaceId}/pages/empty-trash

Create the empty trash in a space

forumulate docs trash empty<br>client.pages.emptyTrash()

Page_restore

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/restore

Restore the page

forumulate docs restore<br>client.pages.restore()

Page_unpublish

POST /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/unpublish

Unpublish the page

forumulate docs unpublish<br>client.pages.unpublish()

Page_purge

DELETE /orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/purge

Purge the page

forumulate docs purge<br>client.pages.purge()

Page Template

Operation

Request

What it does

Shortcuts

PageTemplate_list

GET /orgs/{orgId}/spaces/{spaceId}/page-templates

List page templates in a space

client.pages.template.list()

PageTemplate_save

POST /orgs/{orgId}/spaces/{spaceId}/page-templates

Create a page template in a space

client.pages.template.save()

PageTemplate_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/page-templates/{id}

Delete the page template

client.pages.template.remove()

PageTemplate_newPage

POST /orgs/{orgId}/spaces/{spaceId}/page-templates/{id}/new-page

Create the new page in a space

client.pages.template.newPage()

Public export

Operation

Request

What it does

Shortcuts

Public_exportAll

GET /public/orgs/{orgId}/spaces/{spaceId}/export

Export the space

client.public.exportAll()

Public llms full.txt

Operation

Request

What it does

Shortcuts

Public_llmsFull

GET /public/orgs/{orgId}/spaces/{spaceId}/llms-full.txt

File of llms full (public reader)

client.public.llmsFull()

Public llms.txt

Operation

Request

What it does

Shortcuts

Public_llmsTxt

GET /public/orgs/{orgId}/spaces/{spaceId}/llms.txt

File of llms (public reader orgs spaces)

client.public.llmsTxt()

Public pages

Operation

Request

What it does

Shortcuts

Public_pagePdf

GET /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/pdf

Get the PDF in a space (pages)

forumulate reader pdf<br>client.public.pagePdf()

Public_pageMarkdown

GET /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}.md

Get the page (public reader orgs spaces markdown)

forumulate reader markdown<br>client.public.pageMarkdown()<br>MCP get_page

Public_pageVersion

GET /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/version

Get the version in a space

forumulate reader version<br>client.public.pageVersion()

Public_page

GET /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}

Get the page (public reader orgs spaces)

forumulate reader page<br>client.public.page()<br>client.public.getPage()

Public_view

POST /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/view

Create the view in a space (pages)

client.public.view()

Public pdf

Operation

Request

What it does

Shortcuts

Public_pdf

GET /public/orgs/{orgId}/spaces/{spaceId}/pdf

Get the PDF in a space

client.public.pdf()

Public skill.md

Operation

Request

What it does

Shortcuts

Public_skillMd

GET /public/orgs/{orgId}/spaces/{spaceId}/skill.md

File of skill (public reader)

client.public.skillMd()

Public Interaction

Operation

Request

What it does

Shortcuts

PublicInteraction_ask

POST /public/orgs/{orgId}/spaces/{spaceId}/ask

Create the ask in a space (public reader)

client.public.interaction.ask()

PublicInteraction_askStream

POST /public/orgs/{orgId}/spaces/{spaceId}/ask/stream

Create the stream in a space (public reader)

client.public.interaction.askStream()

PublicInteraction_askFeedback

POST /public/orgs/{orgId}/spaces/{spaceId}/ask/{queryId}/feedback

Create the feedback in a space (public reader ask)

client.public.interaction.askFeedback()

PublicInteraction_listComments

GET /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/comments

List comments in a space (public reader pages)

client.public.interaction.listComments()<br>client.public.listComments()

PublicInteraction_postComment

POST /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/comments

Create a comment in a space (public reader pages)

client.public.interaction.postComment()<br>client.public.postComment()

PublicInteraction_reactToComment

POST /public/orgs/{orgId}/spaces/{spaceId}/comments/{commentId}/react

Create the react in a space (public reader comments)

client.public.interaction.reactToComment()<br>client.public.reactToComment()

PublicInteraction_extRender

POST /public/orgs/{orgId}/spaces/{spaceId}/ext/render

Render the ext

client.public.interaction.extRender()

PublicInteraction_extAction

POST /public/orgs/{orgId}/spaces/{spaceId}/ext/action

Create the action in a space

client.public.interaction.extAction()

PublicInteraction_feedback

POST /public/orgs/{orgId}/spaces/{spaceId}/pages/{pageId}/feedback

Create the feedback in a space (public reader pages)

client.public.interaction.feedback()

PublicInteraction_faqSearchEvent

POST /public/orgs/{orgId}/spaces/{spaceId}/faq/search-events

Create a search event in a space

client.public.interaction.faqSearchEvent()

Public Relation

Operation

Request

What it does

Shortcuts

PublicRelation_resolve

GET /public/orgs/{orgId}/relations

List relations in an organization

client.public.relation.resolve()

Section Library

Operation

Request

What it does

Shortcuts

SectionLibrary_list

GET /section-library

Get the section library

client.meta.sectionLibrary.list()

SectionLibrary_preview

GET /section-library/{id}/preview.svg

Get a schematic picture of one section

client.meta.sectionLibrary.preview()

SectionLibrary_one

GET /section-library/{id}

Get one entry of the section library

client.meta.sectionLibrary.one()

Shape

Operation

Request

What it does

Shortcuts

Shape_get

GET /orgs/{orgId}/spaces/{spaceId}/shape

Get the shape in a space

forumulate shape get<br>client.shapes.get()<br>MCP get_space_shape<br>MCP get_page_shape

Shape_put

PUT /orgs/{orgId}/spaces/{spaceId}/shape

Replace the space's shape override

forumulate shape set<br>client.shapes.put()<br>client.shapes.set()

Shape_patch

PATCH /orgs/{orgId}/spaces/{spaceId}/shape

Merge a patch into the space's shape override

forumulate shape patch<br>client.shapes.patch()

Shape_setDefaultKind

PUT /orgs/{orgId}/spaces/{spaceId}/shape/default-kind

Replace the default kind in a space

forumulate shape kinds default<br>client.shapes.setDefaultKind()

Shape_setKind

PUT /orgs/{orgId}/spaces/{spaceId}/shape/kinds/{name}

Replace the kind

forumulate shape kinds set<br>client.shapes.setKind()

Shape_removeKind

DELETE /orgs/{orgId}/spaces/{spaceId}/shape/kinds/{name}

Delete the kind

forumulate shape kinds remove<br>client.shapes.removeKind()

Shape_applyPreset

POST /orgs/{orgId}/spaces/{spaceId}/shape/presets/{name}

Create the preset

forumulate shape preset<br>client.shapes.applyPreset()

Space Settings discovery

Operation

Request

What it does

Shortcuts

SpaceSettings_getDiscovery

GET /orgs/{orgId}/spaces/{spaceId}/discovery

Get the discovery in a space

client.spaces.settings.getDiscovery()

SpaceSettings_setDiscovery

PUT /orgs/{orgId}/spaces/{spaceId}/discovery

Replace the discovery in a space

client.spaces.settings.setDiscovery()

Space Settings glossary

Operation

Request

What it does

Shortcuts

SpaceSettings_getGlossary

GET /orgs/{orgId}/spaces/{spaceId}/glossary

Get the glossary in a space

client.spaces.settings.getGlossary()

SpaceSettings_setGlossary

PUT /orgs/{orgId}/spaces/{spaceId}/glossary

Replace the glossary in a space

client.spaces.settings.setGlossary()

Space Settings home doc

Operation

Request

What it does

Shortcuts

SpaceSettings_getHomeDoc

GET /orgs/{orgId}/spaces/{spaceId}/home-doc

Get the home doc in a space

client.spaces.settings.getHomeDoc()

SpaceSettings_setHomeDoc

PUT /orgs/{orgId}/spaces/{spaceId}/home-doc

Replace the home doc in a space

client.spaces.settings.setHomeDoc()

Space Settings home page

Operation

Request

What it does

Shortcuts

SpaceSettings_getHomePage

GET /orgs/{orgId}/spaces/{spaceId}/home-page

Get the home page in a space

client.spaces.settings.getHomePage()

SpaceSettings_setHomePage

PUT /orgs/{orgId}/spaces/{spaceId}/home-page

Replace the home page in a space

client.spaces.settings.setHomePage()

Space Settings redirects

Operation

Request

What it does

Shortcuts

SpaceSettings_getRedirects

GET /orgs/{orgId}/spaces/{spaceId}/redirects

List redirects in a space

client.spaces.settings.getRedirects()

SpaceSettings_setRedirects

PUT /orgs/{orgId}/spaces/{spaceId}/redirects

Replace redirects in a space

client.spaces.settings.setRedirects()

Space Snapshot

Operation

Request

What it does

Shortcuts

SpaceSnapshot_list

GET /orgs/{orgId}/spaces/{spaceId}/snapshots

List snapshots in a space

forumulate snapshots list<br>client.snapshots.list()

SpaceSnapshot_create

POST /orgs/{orgId}/spaces/{spaceId}/snapshots

Create a snapshot in a space

forumulate snapshots create<br>client.snapshots.create()

SpaceSnapshot_restore

POST /orgs/{orgId}/spaces/{spaceId}/snapshots/{id}/restore

Restore the snapshot

forumulate snapshots restore<br>client.snapshots.restore()

SpaceSnapshot_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/snapshots/{id}

Delete the snapshot

forumulate snapshots delete<br>client.snapshots.remove()

Space Template

Operation

Request

What it does

Shortcuts

SpaceTemplate_list

GET /orgs/{orgId}/space-templates

List space templates in an organization

client.spaces.template.list()

SpaceTemplate_publish

PATCH /orgs/{orgId}/space-templates/{id}

Update the space template

client.spaces.template.publish()

SpaceTemplate_remove

DELETE /orgs/{orgId}/space-templates/{id}

Delete the space template

client.spaces.template.remove()

Space Template Actions

Operation

Request

What it does

Shortcuts

SpaceTemplateActions_save

POST /orgs/{orgId}/spaces/{spaceId}/save-as-template

Create the save as template in a space

client.spaces.templateActions.save()

SpaceTemplateActions_apply

POST /orgs/{orgId}/spaces/{spaceId}/apply-template

Apply template in a space

client.spaces.templateActions.apply()

Space Types

Operation

Request

What it does

Shortcuts

SpaceTypes_list

GET /space-types

List space types

forumulate space-types list<br>client.spaceTypes.list()

SpaceTypes_allPresets

GET /space-types/presets

List presets

forumulate space-types presets<br>client.spaceTypes.allPresets()

SpaceTypes_allSlots

GET /space-types/slots

List slots

forumulate space-types slots<br>client.spaceTypes.allSlots()

SpaceTypes_one

GET /space-types/{type}

Get the space type

forumulate space-types get<br>client.spaceTypes.one()

SpaceTypes_presets

GET /space-types/{type}/presets

List presets (for a space type)

forumulate space-types type-presets<br>client.spaceTypes.presets()

SpaceTypes_slots

GET /space-types/{type}/slots

List slots (for a space type)

forumulate space-types type-slots<br>client.spaceTypes.slots()

Translation

Operation

Request

What it does

Shortcuts

Translation_list

GET /orgs/{orgId}/spaces/{spaceId}/docs/locales

List locales in a space

forumulate docs locales list<br>client.docs.translations.list()<br>MCP list_translation_locales<br>MCP set_translation_locales

Translation_create

POST /orgs/{orgId}/spaces/{spaceId}/docs/locales

Create a locale in a space

forumulate docs locales add<br>client.docs.translations.create()<br>MCP set_translation_locales

Translation_update

PATCH /orgs/{orgId}/spaces/{spaceId}/docs/locales/{code}

Update the locale (spaces docs)

forumulate docs locales relabel<br>client.docs.translations.update()<br>MCP set_translation_locales

Translation_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/docs/locales/{code}

Delete the locale (spaces docs)

forumulate docs locales remove<br>client.docs.translations.remove()<br>MCP set_translation_locales

Translation Override Public

Operation

Request

What it does

Shortcuts

TranslationOverridePublic_catalog

GET /public/orgs/{orgId}/i18n/catalog

The interface strings you can override

client.i18n.publicOverrides.catalog()

TranslationOverridePublic_map

GET /public/orgs/{orgId}/i18n/{locale}

Get the translation (public reader i18n)

client.i18n.publicOverrides.map()

Version

Operation

Request

What it does

Shortcuts

Version_list

GET /orgs/{orgId}/spaces/{spaceId}/docs/versions

List versions in a space

forumulate versions list<br>client.docs.versions.list()

Version_create

POST /orgs/{orgId}/spaces/{spaceId}/docs/versions

Create a version in a space

forumulate versions create<br>client.docs.versions.create()

Version_update

PATCH /orgs/{orgId}/spaces/{spaceId}/docs/versions/{key}

Update the version

forumulate versions update<br>client.docs.versions.update()

Version_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/docs/versions/{key}

Delete the version

forumulate versions delete<br>client.docs.versions.remove()

Version_makeDefault

POST /orgs/{orgId}/spaces/{spaceId}/docs/versions/{key}/make-default

Create the make default in a space

forumulate versions default<br>client.docs.versions.makeDefault()

Versioning

Operation

Request

What it does

Shortcuts

Versioning_get

GET /orgs/{orgId}/spaces/{spaceId}/docs/versioning

Get the versioning in a space

forumulate versions settings<br>client.docs.versioning.get()

Versioning_set

PUT /orgs/{orgId}/spaces/{spaceId}/docs/versioning

Replace the versioning in a space

forumulate versions configure<br>client.docs.versioning.set()

The Forumulate teamUpdated