The command line, and forumulate doctor

Embed
Preview of the card
<a href="https://forumulate.com/guides/cli"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/4403ea48-8f86-464b-b849-6b2ca0906071/social-card.svg?v=3e7ae33eec8b" alt="The command line, and forumulate doctor" width="600" height="315"></a>

Two packages are published on npm. @forumulate/sdk is a typed client for the API; @forumulate/cli is a command you run in a terminal, and it includes doctor — a check of a live site from the outside, which is the only vantage point that tells you what your readers actually get.

The Python and Go SDKs, the MCP server and the website integrations are in SDKs and integrations.

Install

npm install -g @forumulate/cli    # or: pnpm add -g @forumulate/cli
forumulate help

Nothing to install, one command:

npx --yes @forumulate/cli doctor https://docs.yourcompany.com

npx is the form to use in CI and the form to paste into a ticket. The package depends on @forumulate/sdk, which npm installs for you.

Point it at an API

Every command except doctor and conformance talks to the API and needs a token. forumulate login asks for one (it is not echoed), checks it, and stores it for this API in ~/.config/forumulate/credentials.json, readable only by you. In CI, set FORUMULATE_TOKEN instead; it always wins over a stored token.

Variable

What it is

FORUMULATE_TOKEN

A personal access token, sent as Authorization: Bearer …. Mint one at Account → Tokens.

FORUMULATE_URL

The site, default https://forumulate.com. The API is its /api; a value ending in /api is as-is.

FORUMULATE_API_URL

The exact API base, for a local API with no /api prefix (http://localhost:4000).

forumulate login
forumulate whoami
forumulate config set org acme     # the default --org for every command

Two flags apply to every command: --json prints the raw JSON instead of the formatted summary, and --version prints the CLI version. Every command exits non-zero on failure.

Command reference

Resource commands

Every group takes the same verbs — list, get, create, update, delete, and publish where a thing has a draft — and the same scope flags: --org, --space and --library, each an id or a slug. forumulate <group> --help prints a group's verbs and the API operation each one calls.

Command

What it does

orgs list|get|create|update|delete, orgs deepl status|set|clear, orgs strings list|set|remove

Your organizations, their DeepL key, and their own wording for interface strings.

members list|update|remove|sign-out|reassign

People in an org.

invites list|create|bulk|revoke|pending|accept|redeem|join, invites links list|create|revoke

Invitations you send (one, many, or a shareable link) and the ones waiting for you.

groups …, roles …, service-accounts / bots … tokens list|create|revoke

Groups and join requests, custom roles, and non-human members with their tokens.

tokens / pats list|scopes|create|rotate|rename|revoke

Your personal access tokens, by id or by name.

webhooks list|events|create|update|delete|test|rotate-secret|deliveries|replay

Endpoints, their deliveries, and replays.

extensions …, audit list|actions|export

Installed extensions and the marketplace; the audit log (export as CSV).

spaces list|overview|get|create|update|delete|restore|purge|duplicate|move|rename|access|preview, spaces slugs list|forget

Spaces in an org: slugs, moves, copies and the trash.

snapshots list|create|restore|delete, shape get|set|patch|preset|kinds …

Space snapshots, and a space's shape override.

items / pages list|get|content|create|update|write|section|fields|chunks|verify|publish|delete

Any piece in a space. --file page.md fills the body; front matter sets title and slug.

docs list|get|save|publish|unpublish|history|diff|rollback|move|duplicate|…, docs schedule get|set|cancel, docs checkpoints …, docs trash …, docs import zip|docx|url|files, docs locales …, docs search

A docs space's pages: drafts, revisions, scheduled publishing, rollback, the trash, imports and languages.

collections …, versions list|create|update|default|delete|settings|configure

Collections and their page order; docs versions.

change-requests / crs list|open|review|approve|merge|close|rebase|comment|…, crs reviewers …, crs rules get|set

Proposed docs changes, review and merge rules.

openapi list|register|resync|lint|import|generate|…, openapi sdk languages|get|download

OpenAPI specs behind reference pages, and the SDKs generated from them.

sdks packages|register|publish|publishes, sdks registries list|set|test|remove

Publishing those SDKs to package registries.

blog, forum, changelog list|get|create|update|publish|delete|…

Articles, topics and releases; scheduling, pinning, settings, flags, polls, replies. create --file post.md --publish.

blog / forum / changelog / status translations list|get|put|remove|auto

One piece's translations at its own door. translations (below) is the whole-space workflow.

roadmap board|get|submit|update|status|vote|comment|merge|proxy-votes …

A roadmap space.

status board, status incidents …, status components …, status channels …, status groups get|set

A status space: incidents, components, where they are announced.

events …, course outline|enrol|complete, careers …, forms …

Events and RSVPs, course progress, job applications, forms and submissions (CSV export).

libraries list|get|create|update|delete, libraries members|locales|model|keys …

Headless CMS libraries: grants, languages, the model, delivery keys.

entries list|types|get|create|update|fields|patch|publish|bulk|trash|restore|lock …|locale get|set

A library's entries.

media …, releases list|create|add|schedule|publish|rollback|…, environments / envs list|fork|compare|promote

Library media, content releases, and sandbox environments.

domains list|add|setup|verify|activate|www|allow-ips|redirect|remove|…, assets list|all|upload|download|delete

Custom domains on a space (by id or hostname), and its uploaded files.

theme get|set|patch|export|import, theme draft get|set|patch|apply|discard|publish

A space's theme: kept in git with export/import, staged as a draft, then published.

sites …, sites domains …, sites theme export|import, sites draft …

Sites: several spaces under one domain and theme.

github status|connect|import|export|pull-request

Sync a docs space with a repository.

reader page|markdown|pdf|version, reader blog index|article|rss|json-feed|…

What a reader is served, as the public API answers it (--out for a PDF or feed).

space-types list|get|presets|slots

What each kind of space is.

search <query…>, search semantic|recent|tags|tag|gaps|reindex

Unified search across docs, forum and blog, and the searches that found nothing.

publish <folder>

Publish a folder of Markdown — see Publish from git.

sync <folder>

Content as code: the folder publish as a plan unless --apply, with --diff, --prune, blog categories and FAQ questions — see Content as code.

snapshot <orgId> <spaceId> <label>

Take a space version snapshot.

content, import

Headless CMS models, and imports from other CMSs — see Importing.

conformance <collectionUrl>

Run the Content Sync Protocol suite against any platform's collection endpoint.

doctor <readerUrl>

Check a live site from the outside (below).

config set|get|unset|list

Defaults: org, space, library, api-version.

completion bash|zsh|fish

Shell completion: eval "$(forumulate completion zsh)".

The flags of a resource command come from the API: every query parameter of its operation (--limit, --state) and every top-level body field (--name, --visibility) is a flag. delete, remove and revoke ask before they send; --yes skips the question and is required where there is no terminal. --dry-run prints the request instead of sending it, and --paginate walks a list to its end. A command whose answer is a file (an SDK archive, a PDF, a CSV export) writes it to --out <file>; one that uploads (assets upload, media upload, docs import zip) takes the local path as its argument.

The older positional forms — spaces <orgId>, items <orgId> <spaceId>, search <orgId> <query…>, domains [verify] <orgId> <spaceId> — still work and print what they always did.

forumulate api: every operation

The resource commands cover the daily work. forumulate api reaches every operation in the API reference, from a copy of the OpenAPI document bundled with the CLI:

forumulate api list --tag Webhook --search replay   # discover: operationId, method, path, summary
forumulate api describe Space_create                # parameters as flags, the body as fields
forumulate api Space_list acme --limit 20           # path params positionally, or --orgId acme
forumulate api GET /orgs/{orgId}/members --paginate --json
forumulate api POST /orgs/acme/spaces --data @space.json
forumulate api Space_update acme <spaceId> --field settings.overview="Start here"

Input

How to give it

Path parameters

Positional, in path order, or --orgId / --org-id. orgId defaults to --org, FORUMULATE_ORG, the config.

Query, header params

--<name> value. A repeated flag is joined with commas, which is how array parameters are spelled.

A JSON body

--data '{…}', --data @file.json, --data - (stdin); then --field a.b=value (typed; a:=<json> for raw JSON).

A top-level body field

--<field> value, typed by the operation's schema.

A multipart upload

--form name=value, --form file=@path.

A name we also use

-p name=value — an operation's own parameter always wins over a CLI flag of the same name.

Output is a table for a list (with a footer naming the next --cursor), and key value lines for a single resource; --json prints the body exactly as received, --paginate follows page.nextCursor and prints one merged list.

Every request carries Forumulate-Version: 2026-12-01 (pin another with --api-version or forumulate config set api-version), and every write that declares one carries an Idempotency-Key — the same key on a retry, so a retried create is not a second create. A 429 is retried after its Retry-After; a 502, 503 or 504 is retried only for a request that is safe to repeat. A route newer than your CLI still works as forumulate api <METHOD> <path>, sent as written.

doctor

npx --yes @forumulate/cli doctor https://docs.yourcompany.com

It takes one URL and nothing else, speaks plain HTTP, and reports the same way whether you point it at a Forumulate space, at a staging mount, or at a site running on something else entirely. A check that only works on its author's deployment is a checklist with extra steps.

Every one of these checks exists because a customer discovered the failure after going live.

Check

Passes when

Fails when, and why that matters

health

GET <apiBase>/api/health answers with a JSON body.

A plain-text 500 reads as a permanent bug, so a careful client stops retrying a few-minute outage.

public-read

An anonymous GET of the URL answers 200, or redirects somewhere that is not a sign-in page.

A public page that redirects to sign-in is a login wall; crawlers index nothing.

canonical

The document has a rel=canonical whose host is the host you asked.

Search engines credit the canonical host, so a platform URL here hands your authority to the platform. Skipped when there is no HTML to read.

html-untouched

The markup contains no data-cfemail and no /cdn-cgi/scripts/…email-decode.

Cloudflare Email Obfuscation is rewriting addresses on the page. The decoder it injects carries no CSP nonce, so the browser refuses it and every address renders as [email protected] with a dead link.

cache-headers

The document carries a Cache-Control that is not no-store.

Without one, every reader pays full origin latency and a CDN in front of it does nothing.

robots

GET <origin>/robots.txt answers 200 and does not Disallow the path you asked about.

A Disallow here means the page is never indexed, however good it is.

sitemap

robots.txt carries a Sitemap: line.

A crawler finds pages faster when it is handed the list.

crawler-access

A request sent with a Googlebot User-Agent gets the same status as the anonymous one.

A WAF rule or bot challenge that answers crawlers differently removes the site from search without telling anybody.

feeds

<path>/rss.xml answers 200.

Skipped, not failed, when there is no feed — a documentation space is not expected to have one.

A healthy run against our own guides:

Checking https://forumulate.com/guides
  PASS  health  Health endpoint answers JSON
        200 with a JSON body.
  PASS  public-read  A logged-out reader can read the page
        307 → /guides/getting-started
  PASS  canonical  Canonical link points at this host
        https://forumulate.com/guides
  PASS  html-untouched  No proxy is rewriting the markup
        No rewritten email addresses.
  PASS  cache-headers  The document carries a usable Cache-Control
        public, s-maxage=60, stale-while-revalidate=60, max-age=0, must-revalidate
  PASS  robots  robots.txt allows this path
        Allows /guides
  PASS  sitemap  robots.txt names a sitemap
        Declared.
  PASS  crawler-access  A crawler gets the same answer as a reader
        reader 307, crawler 307
  SKIP  feeds  A feed is served under this path
        404 for /guides/rss.xml
8 passed, 0 failed, 1 skipped

Exit codes

Code

When

0

No check failed. Skips do not fail the run.

1

At least one check failed, or you gave no URL, or the command is unknown, or the run threw.

So doctor belongs in CI exactly as it is:

npx --yes @forumulate/cli doctor https://docs.yourcompany.com || exit 1

One thing to watch

doctor sends its health check to FORUMULATE_URL when that variable is set, and to the site's own origin when it is not. If you have FORUMULATE_URL pointing at a local API from an earlier command in the same shell, the health check reports on that API rather than on the site you named. Unset it, or run doctor in its own shell.

--json

npx --yes @forumulate/cli doctor https://docs.yourcompany.com --json

The object has four top-level keys beside url: checks (each with id, title, state, detail and matters), passed, failed and warnings. warnings is the count the formatted output prints as "skipped".

conformance

npx --yes @forumulate/cli conformance https://docs.yourcompany.com/_api/…/collections/…

Runs the Content Sync Protocol suite against a collection endpoint. It is deliberately platform-neutral: point it at us, or at whatever you are migrating from, and compare.

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.

Member

Operation

Request

What it does

Shortcuts

Member_list

GET /orgs/{orgId}/members

List members in an organization

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

Member_invite

POST /orgs/{orgId}/members

Create a member in an organization

forumulate invites create<br>client.members.invite()

Member_invites

GET /orgs/{orgId}/members/invites

List invites in an organization

forumulate invites list<br>client.members.invites()

Member_revokeInvite

DELETE /orgs/{orgId}/members/invites/{inviteId}

Delete the invite (members)

forumulate invites revoke<br>client.members.revokeInvite()

Member_bulkInvite

POST /orgs/{orgId}/members/bulk

Create the bulk in an organization (members)

forumulate invites bulk<br>client.members.bulkInvite()

Member_listInviteLinks

GET /orgs/{orgId}/members/invite-links

List invite links in an organization

forumulate invites links list<br>client.members.listInviteLinks()

Member_createInviteLink

POST /orgs/{orgId}/members/invite-links

Create an invite link in an organization

forumulate invites links create<br>client.members.createInviteLink()

Member_revokeInviteLink

DELETE /orgs/{orgId}/members/invite-links/{linkId}

Delete the invite link

forumulate invites links revoke<br>client.members.revokeInviteLink()

Member_update

PATCH /orgs/{orgId}/members/{userId}

Update the member

forumulate members update<br>client.members.update()

Member_remove

DELETE /orgs/{orgId}/members/{userId}

Delete the member

forumulate members remove<br>client.members.remove()

Member_revokeSessions

POST /orgs/{orgId}/members/{userId}/sessions/revoke

Revoke the session

forumulate members sign-out<br>client.members.revokeSessions()

Member_reassignContent

POST /orgs/{orgId}/members/{userId}/reassign-content

Create the reassign content in an organization

forumulate members reassign<br>client.members.reassignContent()

The Forumulate teamUpdated