# Worked Recipes: CLI and Raw API Recipes combining the bundled `scripts/ghost` CLI with raw Admin API calls. Every recipe is executable end-to-end with only `GHOST_URL` and `GHOST_ADMIN_KEY` set (and `jq` for JSON plumbing). HTML/Lexical examples use placeholder tokens, never real credentials. ## Recipe 1: Draft a post now, publish after review Draft-first keeps half-written work off the public site while still letting you preview with admin themes. ```bash # 1. Create the draft ghost --json create-post --title "Release notes" \ --html "
What shipped this week…
" > /tmp/draft.json post_id=$(jq -r '.post.id' /tmp/draft.json) # 2. Later: re-read to fetch the CURRENT updated_at (collision guard) ghost get-post "$post_id" | grep updated_at # 3. Publish, passing the fresh timestamp ghost update-post "$post_id" \ --status published \ --updated-at "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" # NO — use the value from step 2 ``` The last line above is deliberately wrong: never synthesize `updated_at`. Copy the exact string from step 2's output; the server compares timestamps literally, and a mismatch means 409. Raw-API equivalent of step 3: ```bash curl -sS -X PUT "$BASE/posts/$POST_ID/" \ -H "Authorization: Ghost $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept-Version: v6.0" \ --data "{\"posts\":[{\"updated_at\":\"$UPDATED_AT\",\"status\":\"published\"}]}" ``` ## Recipe 2: Review queue — everything unpublished Drafts, scheduled, and published live on different filters; one call per status: ```bash ghost posts --status draft --limit 50 --json | jq -r '.posts[] | "\(.title)\t\(.slug)"' ghost posts --status scheduled --json | jq -r '.posts[] | "\(.title)\t\(.published_at // "-")"' ``` Pair it with an authoring-wide sanity check via jq types before feeding slugs onward: ```bash ghost posts --status draft --json | jq '{count: (.posts|length), all_slugs_strings: ([.posts[].slug | type] | all(. == "string"))}' ``` ## Recipe 3: Full export, page by page Ghost 6 caps pages at 100 rows; `limit=all` is gone. ```bash page=1 while :; do ghost posts --limit 100 --page "$page" --json > "/tmp/posts-$page.json" next=$(jq -r '.page.next // empty' "/tmp/posts-$page.json") jq -r '.posts[].id' "/tmp/posts-$page.json" [ -z "$next" ] && break page=$next sleep 0.2 done ``` Note the loop reads `meta.pagination.next` from the CLI's `.page` field rather than computing `(page * limit) < total`; totals shift under concurrent edits. ## Recipe 4: Publish at a future time (scheduling) ```bash ghost create-post --title "Launch day" \ --html "We're live.
" \ --status scheduled \ --published-at "2026-09-01T09:00:00.000Z" ``` Requirements: the timestamp must be in the future and ISO 8601; Ghost processes the queue on its own schedule (typically within five minutes of the mark). The post remains visible as `scheduled` in the Admin plane, invisible publicly until flip time. CLI enforces the pairing (`--status scheduled` without `--published-at` errors before any request is made). ## Recipe 5: Slug-based handoff between systems External systems key content by slug. Resolve slug → id → full record: ```bash curl -sS "$BASE/posts/slug/$SLUG/" \ -H "Authorization: Ghost $TOKEN" \ -H "Accept-Version: v6.0" | jq -r '.posts[0].id' ``` Then read or edit by that id. The CLI reads by id (`ghost get-post