Skip to main content

CLI client for the Wagtail Write API, optimised for LLM orchestration

Project description

wagapi

A CLI client for wagtail-write-api, optimised for LLM orchestration.

wagapi is a thin, predictable HTTP client that translates CLI commands into wagtail-write-api HTTP calls and returns structured output. The intelligence lives in the LLM that orchestrates it.

# Discover the content model
wagapi schema

# Learn the fields for a page type
wagapi schema testapp.BlogPage

# Create a page with markdown body
wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" --body "A philosopher and novelist." --publish

Installation

# One-shot via uvx (no install needed)
uvx wagapi schema

# Or install permanently
pip install wagapi
# or
uv tool install wagapi

Python 3.10+ required.

Quick start

1. Set up a Wagtail site with wagtail-write-api

Follow the example app guide to get a local Wagtail instance running with wagtail-write-api:

cd wagtail-write-api/example
uv run python manage.py migrate
uv run python manage.py seed_demo
uv run python manage.py runserver

seed_demo prints API tokens for several test users:

--- API Tokens ---
  admin: 25e620d83a9c4a591f5986b1b74bbd4b7365c4be
  editor: 4fbeb9c8...
  moderator: 6c8b9634...
  reviewer: a2377491...

2. Configure wagapi

export WAGAPI_URL=http://localhost:8000/api/write/v1
export WAGAPI_TOKEN=25e620d83a9c4a591f5986b1b74bbd4b7365c4be

Or run wagapi init to save credentials to ~/.wagapi.toml.

3. Explore the content model

wagapi schema
testapp.SimplePage                  — "simple page"
  Fields: title, slug, alias_of, body, id
  Parents: wagtailcore.Page, testapp.SimplePage, testapp.EventPage
  Children: wagtailcore.Page, testapp.SimplePage, testapp.BlogIndexPage, testapp.EventPage

testapp.BlogPage                    — "blog page"
  Fields: title, slug, alias_of, published_date, feed_image, body, authors, id
  Parents: testapp.BlogIndexPage
  Children: (none)
...

Get the full field schema for a type:

wagapi schema testapp.BlogPage

4. Browse existing pages

wagapi pages list
wagapi pages get 3   # use an ID from the list above

5. Create a page

wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" \
  --body "## A Philosopher and Novelist

Iris Murdoch (1919–1999) was an Irish-British novelist and philosopher.

She argued that moral progress comes from **attention**."

The --body flag accepts markdown, which is auto-converted to StreamField blocks (headings, paragraphs with rendered HTML).

6. Publish, update, and manage

# Publish a draft (use the page ID returned by create)
wagapi pages publish <ID>

# Update a page
wagapi pages update <ID> --title "Iris Murdoch: The Sovereignty of Good"

# Create and publish in one step, using a URL path as parent
wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Simone Weil" \
  --body "Simone Weil was a French philosopher and mystic." \
  --field published_date:2026-04-07 \
  --publish

# Unpublish
wagapi pages unpublish <ID>

# Delete (prompts for confirmation)
wagapi pages delete <ID>

7. Inspect requests

# See HTTP request/response details
wagapi -v pages get <ID>

# Preview without executing
wagapi --dry-run pages create testapp.SimplePage --parent / --title "Test"

8. Pipe-friendly JSON output

When piped, output is JSON automatically:

wagapi pages list | jq '.items[].title'
wagapi schema testapp.BlogPage | cat

Force a format with --json or --human:

wagapi --human pages list
wagapi --json pages get 42

Configuration

Config priority

Settings are resolved in this order (highest priority first):

Priority Source Example
1 (highest) CLI flags --url, --token
2 Environment variables WAGAPI_URL, WAGAPI_TOKEN
3 Project dotfile ./.wagapi.toml
4 (lowest) User dotfile ~/.wagapi.toml

Dotfile format

# ~/.wagapi.toml
url = "https://cms.example.com/api/write/v1"
token = "abc123def456"

Commands

Global flags

Flag Env var Description
--url URL WAGAPI_URL API base URL
--token TOKEN WAGAPI_TOKEN Auth token
--json Force JSON output
--human Force human output
--verbose / -v Print HTTP request/response details to stderr
--dry-run Print the HTTP request that would be sent

wagapi init

Interactive setup that writes ~/.wagapi.toml:

$ wagapi init
Wagtail Write API URL: https://cms.example.com/api/write/v1
API Token: ****************************
Testing connection... ✓ Connected (3 page types found)
Config written to ~/.wagapi.toml

If --url and --token are both provided, skips interactive prompts.

wagapi schema

List available page types, or show the full field schema for a specific type:

wagapi schema                      # list page types (default)
wagapi schema --snippets           # list snippet types only
wagapi schema --all                # list both page and snippet types
wagapi schema testapp.BlogPage     # show fields, parents, children
wagapi schema --snippets testapp.Category  # show snippet schema

JSON output returns the raw schema from the API verbatim, including create_schema, patch_schema, and read_schema.

wagapi pages list

wagapi pages list [OPTIONS]
Option Description
--type TYPE Filter by page type, e.g. testapp.BlogPage
--parent ID_OR_PATH Direct children of page ID or URL path (e.g. 5 or /blog/)
--descendant-of ID_OR_PATH All descendants of page ID or URL path
--status STATUS draft, live, or live+draft
--slug SLUG Exact slug match
--path PATH Exact URL path match, e.g. /blog/my-post/
--search QUERY Full-text search
--order FIELD Sort field, e.g. title, -first_published_at
--limit N Items per page (default: 20)
--offset N Pagination offset

wagapi pages get

wagapi pages get 42
wagapi pages get 42 --version live

wagapi pages create

wagapi pages create <type> --parent ID_OR_PATH --title TITLE [OPTIONS]
Option Description
--parent ID_OR_PATH Required. Parent page ID or URL path (e.g. /blog/)
--title TITLE Required. Page title
--slug SLUG URL slug (auto-generated from title if omitted)
--field KEY:VALUE Set a field value (repeatable). Values starting with [ or { are auto-parsed as JSON
--body TEXT Body content as markdown. Use - for stdin
--publish Publish immediately (default: create as draft)
--raw Treat field values as raw JSON (no auto-wrapping)

Markdown body (auto-detected: StreamField blocks or RichTextField HTML):

--body checks the page type schema to determine the field type. For StreamField fields, markdown is converted to blocks. For RichTextField fields, markdown is sent as-is for server-side conversion to HTML.

wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" \
  --body "## Early Life

Iris Murdoch was born in Dublin in 1919.

## Philosophy

She argued that moral progress comes from **attention**."

With extra fields:

wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" --field published_date:2026-04-06

JSON field values (auto-detected):

# Arrays and objects are auto-parsed — no --raw needed
wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" \
  --field 'authors:[{"name": "Jo", "role": "Writer"}]' \
  --field published_date:2026-04-06

Raw mode for full StreamField control:

wagapi pages create testapp.BlogPage --parent /blog/ \
  --title "Iris Murdoch" --raw \
  --field 'body:[{"type":"paragraph","value":"<p>Hello</p>","id":"abc123"}]'

Reading body from stdin:

cat post.md | wagapi pages create testapp.BlogPage \
  --parent /blog/ --title "Iris Murdoch" --body -

wagapi pages update

wagapi pages update 42 --title "New Title" --publish

Same field options as create (minus --parent and type). Only specified fields are sent (PATCH semantics).

Block-level StreamField editing:

Instead of replacing the entire body, you can append or insert individual blocks. The CLI fetches the current body, splices in the new block(s), and sends the result.

# Append a block to the end of the body
wagapi pages update 42 --append-block '{"type":"image","value":7}'

# Insert at a specific position (0-indexed)
wagapi pages update 42 --insert-block 1 '{"type":"paragraph","value":"<p>New paragraph</p>"}'

# Multiple operations in one call
wagapi pages update 42 \
  --insert-block 0 '{"type":"heading","value":{"text":"Preface","size":"h1"}}' \
  --append-block '{"type":"paragraph","value":"<p>Epilogue</p>"}'
Option Description
--append-block JSON Append a block to the end of body (repeatable)
--insert-block INDEX JSON Insert a block at INDEX in body (repeatable)

A UUID id is auto-generated for each block unless one is provided. Cannot be combined with --body.

wagapi pages delete

wagapi pages delete 42
wagapi pages delete 42 --yes   # skip confirmation

No confirmation prompt when piped (non-TTY).

wagapi pages publish / unpublish

wagapi pages publish 42
wagapi pages unpublish 42

wagapi images list / get

wagapi images list [--search QUERY] [--limit N] [--offset N]
wagapi images get 7

wagapi snippets

wagapi snippets list <type> [--search QUERY] [--limit N] [--offset N]
wagapi snippets get <type> <id>
wagapi snippets create <type> --field name:VALUE [--field slug:VALUE] ...
wagapi snippets update <type> <id> --field name:VALUE ...
wagapi snippets delete <type> <id> [--yes]

The <type> argument is always required (e.g. testapp.Category) because each snippet model lives in its own database table.

Markdown-to-StreamField conversion

When --raw is not set and a field is a StreamField, the CLI auto-converts markdown into blocks:

Markdown StreamField block
# Heading {"type": "heading", "value": {"text": "...", "size": "h1"}}
## Heading heading with "size": "h2"
### Heading heading with "size": "h3"
Paragraph text {"type": "paragraph", "value": "<p>...</p>"}
- item (bullet list) {"type": "paragraph", "value": "<ul><li>...</li></ul>"}
1. item (ordered list) {"type": "paragraph", "value": "<ol><li>...</li></ol>"}
![alt](wagapi:image/42) {"type": "image", "value": 42}

Each block gets a generated UUID v4 id.

The auto-wrapper only produces heading, paragraph, and image blocks. For other block types (e.g. quote, embed, code), use --raw mode.

Error handling

Errors go to stderr. Exit codes:

Code Meaning
0 Success
1 General / unexpected error
2 Usage / argument error
3 Connection / network error
4 Authentication error (401)
5 Permission denied (403)
6 Not found (404)
7 Validation error (400/422)

LLM integration

Include this in your LLM's system prompt to enable wagapi tool use:

You have access to `wagapi`, a CLI for managing Wagtail CMS content.

Before creating or updating pages:
1. Run `wagapi schema` to discover available page types
2. Run `wagapi schema <type>` to see the exact fields and StreamField block types

Key commands:
  wagapi schema [type]                   — discover content model and block schemas
  wagapi pages list [--type T] [--slug S] [--path P]  — list/find pages
  wagapi pages get <id>                  — read page detail (latest draft)
  wagapi pages create <type> --parent ID_OR_PATH --title T [--field K:V]... [--body MD] [--publish]
  wagapi pages update <id> [--field K:V]... [--append-block JSON]... [--insert-block IDX JSON]... [--publish]
  wagapi pages delete <id> --yes
  wagapi pages publish <id>
  wagapi snippets list <type>             — list snippets of a type
  wagapi snippets get <type> <id>        — get snippet detail
  wagapi snippets create <type> [--field K:V]...  — create a snippet
  wagapi images list

The --parent flag accepts a page ID or a URL path (e.g. --parent /blog/).
Body text accepts markdown by default. Use --raw for full StreamField JSON control.
To add a block (e.g. an image) to an existing page without replacing the whole body:
  wagapi pages update <id> --append-block '{"type":"image","value":<image_id>}'
  wagapi pages update <id> --insert-block <position> '{"type":"image","value":<image_id>}'
Pages are created as drafts unless --publish is passed.
Output is JSON when piped.

Example LLM tool call sequence

User: "Create a blog post about Iris Murdoch and publish it"

# Step 1: Discover the content model
wagapi schema | cat

# Step 2: Get the BlogPage field schema
wagapi schema testapp.BlogPage | cat

# Step 3: Create and publish
wagapi pages create testapp.BlogPage \
  --parent /blog/ \
  --title "Iris Murdoch: The Sovereignty of Good" \
  --body "## A Philosopher and Novelist

Iris Murdoch (1919–1999) was an Irish-British novelist and philosopher..." \
  --field published_date:2026-04-06 \
  --publish | cat

Development

# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest tests/ -v

Command tree

wagapi
├── init                          Configure connection
├── schema                        List all page types
│   └── <type>                    Show field schema for a type
├── pages
│   ├── list                      List pages (with filters)
│   ├── get <id>                  Get page detail
│   ├── create <type>             Create a page
│   ├── update <id>               Update a page (supports --append-block, --insert-block)
│   ├── delete <id>               Delete a page
│   ├── publish <id>              Publish latest revision
│   └── unpublish <id>            Revert to draft
├── snippets
│   ├── list <type>               List snippets of a type
│   ├── get <type> <id>           Get snippet detail
│   ├── create <type>             Create a snippet
│   ├── update <type> <id>        Update a snippet
│   └── delete <type> <id>        Delete a snippet
└── images
    ├── list                      List images
    └── get <id>                  Get image detail

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wagapi-0.6.0.tar.gz (27.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wagapi-0.6.0-py3-none-any.whl (24.8 kB view details)

Uploaded Python 3

File details

Details for the file wagapi-0.6.0.tar.gz.

File metadata

  • Download URL: wagapi-0.6.0.tar.gz
  • Upload date:
  • Size: 27.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wagapi-0.6.0.tar.gz
Algorithm Hash digest
SHA256 9b7aa405034d6de7d13be5e064c8f6ef2c04a55fb5104228940f05ceb594a7fe
MD5 80d06c006c91268c66ae5f6336ee58ca
BLAKE2b-256 82008dbed734666fef6caf6c127768bb243e7dcce781d6f5f316e9d591f6e843

See more details on using hashes here.

Provenance

The following attestation bundles were made for wagapi-0.6.0.tar.gz:

Publisher: publish.yml on tomdyson/wagapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wagapi-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: wagapi-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 24.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for wagapi-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88de5f80d3bb9cda4dc9adffdc534bd17c10b1014583aa9936d2eb24383f1489
MD5 bba4c518389dec5155fbe90b9224c21e
BLAKE2b-256 189f3249030ed45ac83e85e75628cd2810f7888a479162c56c8979bd2640b1dc

See more details on using hashes here.

Provenance

The following attestation bundles were made for wagapi-0.6.0-py3-none-any.whl:

Publisher: publish.yml on tomdyson/wagapi

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page