WPA — WordPress Automation
CLI tool for WordPress automation — manage posts, pages, users, media, comments, and taxonomy terms via the REST API.
What is WPA?
WPA brings command-line WordPress management to any machine that can run Python — no server access, no PHP, no WordPress installation on the client side. Every operation is an authenticated REST API request scoped to a WordPress user's role, which makes WPA safe for remote administration, CI pipelines, and AI agents: the tool physically cannot exceed the permissions of the Application Password it holds. It complements (rather than replaces) wp-cli, covering the content- and user-management subset of WordPress that the REST API exposes, with command names wp-cli users will recognize.
Install
pip install wpa
Or install from source:
git clone https://github.com/cadentdev/wpa.git
cd wpa
pip install -e .
Create a site config
wpa site add
This prompts for your WordPress URL, username, application password (hidden), and optional admin path. Configs are stored at ~/.config/wpa/<site-name>/.env with 600 permissions.
WordPress Application Password
- Log into wp-admin → Users → Your Profile
- Scroll to "Application Passwords"
- Enter name: "WPA CLI", click "Add New Application Password"
- Copy the generated password (use it during
--new-sitesetup)
Usage
Manage posts
# List posts (with filtering)
wpa post list --site mysite
wpa post list --site mysite --status draft --author 1
wpa post list --site mysite --category 5 --tag 12
wpa post list --site mysite --search "announcement" --orderby date --order desc
# Get a single post
wpa post get 42 --site mysite
# Create a post
wpa post create --site mysite --title "My Post" --content "<p>Hello</p>" --status draft
# Create a post from a markdown file (YAML frontmatter supplies
# title/status/slug; CLI flags override frontmatter)
wpa post create --site mysite --file article.md --author 15 --categories 3,7
# Update a post
wpa post update 42 --site mysite --title "Updated Title" --status publish
# Delete a post (moves to trash; use --force to permanently delete)
wpa post delete 42 --site mysite
wpa post delete 42 --site mysite --force
Manage pages
# List pages
wpa page list --site mysite
wpa page list --site mysite --status publish --parent 10
# Get a single page
wpa page get 42 --site mysite
# Create a page from a markdown file (positional or --file; CLI flags
# like --parent/--author/--status override frontmatter)
wpa page create --site mysite pages/about.md
wpa page create --site mysite --file pages/about.md --parent 12
# Create a page from flags
wpa page create --site mysite --title "About" --content "<p>About us</p>"
# Publish shortcut (equivalent to wpa page create with markdown)
wpa publish pages/your-page.md --site mysite
# Publish attributed to a specific author (or set `author: 15` in
# frontmatter; the CLI flag wins when both are given)
wpa publish pages/your-page.md --site mysite --author 15
# Update a page
wpa page update 42 --site mysite --title "New Title" --parent 10
# Delete a page
wpa page delete 42 --site mysite
Manage users
# List all users
wpa user list --site mysite
# List users as JSON, specific fields only
wpa user list --site mysite --format json --fields id,username,email,roles
# Export users to TSV
wpa user list --site mysite --format tsv > users.tsv
# Filter by role or search term
wpa user list --site mysite --role editor
wpa user list --site mysite --search "jane"
# Get a single user
wpa user get 42 --site mysite
# Create a user and email them a one-time set-password link.
# A strong random password is generated (never displayed) when no
# password flag is given — the recommended flow.
wpa user create --site mysite --username jdoe --email jdoe@example.com \
--role author --send-email
# Create a user with an explicit password read from stdin
printf '%s\n' "$USER_PASSWORD" | wpa user create --site mysite \
--username jdoe --email jdoe@example.com --role author --password-stdin
# Note: without --send-email, NO notification email is sent — the
# WordPress REST API cannot send one. wpa prints a reminder either way.
# Update a user
wpa user update 42 --site mysite --email newemail@example.com --role editor
# Set a user's role (shortcut for update --role)
wpa user set-role 42 editor --site mysite
# Delete a user (reassign their posts to user 1)
wpa user delete 42 --site mysite --reassign 1
Manage media
# List media items
wpa media list --site mysite
wpa media list --site mysite --media-type image --per-page 50
# List media as JSON, specific fields only
wpa media list --site mysite --format json --fields id,title,source_url,media_type
# Get a single media item
wpa media get 123 --site mysite
# Import (upload) a local file as a WordPress media item
wpa media import /path/to/photo.jpg --site mysite
wpa media import /path/to/photo.jpg --site mysite --title "Cover photo" --alt-text "Team at launch"
# Delete a media item (moves to trash; use --force to permanently delete)
wpa media delete 123 --site mysite
wpa media delete 123 --site mysite --force
Output formats: table (default), json, csv, tsv. Use --fields to select columns (available: id, username, email, first_name, last_name, display_name, roles, registered, url).
Manage comments
# List comments (default shows approved only; filter by status, post, author)
wpa comment list --site mysite
wpa comment list --site mysite --status hold
wpa comment list --site mysite --post 42 --status approved
wpa comment list --site mysite --author-email "reviewer@example.com"
# Count comments per moderation status (one lightweight request each)
wpa comment count --site mysite
wpa comment count --site mysite --status hold # bare number
wpa comment count --site mysite --format json
# Get a single comment
wpa comment get 123 --site mysite
# Create a comment
wpa comment create --site mysite --post 42 --content "<p>Thanks for posting!</p>" \
--author-name "Reviewer" --author-email "reviewer@example.com"
# Update comment content or metadata
wpa comment update 123 --site mysite --content "<p>Edited.</p>"
# Moderation shortcuts (wp-cli parity)
wpa comment approve 123 --site mysite
wpa comment unapprove 123 --site mysite # move back to "hold"
wpa comment spam 123 --site mysite
wpa comment unspam 123 --site mysite # restore from spam to approved
wpa comment trash 123 --site mysite # soft delete
# Hard delete (moves to trash by default; --force skips trash)
wpa comment delete 123 --site mysite
wpa comment delete 123 --site mysite --force
Manage plugins
Requires an account with the activate_plugins capability (administrator on most installs).
# List installed plugins (all statuses by default)
wpa plugin list --site mysite
wpa plugin list --site mysite --status active
wpa plugin list --site mysite --search "cache" --format json
# Get a single plugin (folder/file identifier; .php suffix accepted)
wpa plugin get akismet/akismet --site mysite
# Activate / deactivate
wpa plugin activate akismet/akismet --site mysite
wpa plugin deactivate akismet/akismet --site mysite
Installing, updating, and deleting plugins are not supported yet — install/delete are planned (see issue #41's follow-ups); updating to a newer version is a REST API limitation, so use wp-admin or wp-cli for that.
Manage nav menus
Classic nav menus (block themes have none). Requires edit_theme_options capability.
# Menus
wpa menu list --site mysite
wpa menu get 3 --site mysite
wpa menu create --site mysite --name "Primary" --description "Main navigation"
wpa menu delete 3 --site mysite # always permanent; deletes its items too
# Menu items — custom links need --title and --url
wpa menu item list --site mysite --menu 3
wpa menu item add 3 --site mysite --title "Docs" --url "https://example.com/docs"
# Menu items — object links point at existing content
wpa menu item add 3 --site mysite --object page --object-id 12
wpa menu item add 3 --site mysite --object category --object-id 7
wpa menu item add 3 --site mysite --title "Blog" --object page --object-id 8 --position 2
# Reorder, re-parent, rename, remove
wpa menu item update 71 --site mysite --position 1
wpa menu item update 71 --site mysite --parent 70
wpa menu item delete 71 --site mysite
# Where can menus go? (read-only; assignment is theme-dependent and
# not exposed by the REST API)
wpa menu location list --site mysite
Manage site settings
Requires an account with the manage_options capability (administrator).
# List all registered settings
wpa option list --site mysite
wpa option list --site mysite --format json
# Get a single value (bare output for scripting; --format json for typed)
wpa option get title --site mysite
wpa option get posts_per_page --site mysite --format json
# Update a setting. Values are JSON-parsed when possible, so numbers and
# booleans round-trip typed; anything else is treated as a string.
wpa option update title "My Renamed Site" --site mysite
wpa option update posts_per_page 20 --site mysite
Unlike wp option, this cannot touch arbitrary wp_options rows — the REST API only exposes options registered with show_in_rest=true (core settings like title, description, timezone, posts_per_page, plus whatever plugins register). Unknown names fail with the list of available settings.
Manage widgets
Classic widgets (block themes manage widgets as blocks). Requires edit_theme_options capability.
# List widgets, optionally per sidebar
wpa widget list --site mysite
wpa widget list --site mysite --sidebar sidebar-1
# Get a single widget
wpa widget get recent-posts-3 --site mysite
# Move a widget to another sidebar and/or update its settings
wpa widget update recent-posts-3 --site mysite --sidebar sidebar-2
wpa widget update recent-posts-3 --site mysite --instance-json '{"title": "Latest", "number": 3}'
# Deactivate (move to the inactive sidebar; settings kept)
wpa widget deactivate recent-posts-3 --site mysite
# Delete — default parks it in the inactive sidebar; --force removes entirely
wpa widget delete recent-posts-3 --site mysite
wpa widget delete recent-posts-3 --site mysite --force
Creating widgets from the CLI is not supported — each widget type has its own instance schema, so there is no generic safe widget add. Configure new widgets in wp-admin, then manage them here.
List sidebars
# List registered sidebars (classic themes; block themes report none)
wpa sidebar list --site mysite
wpa sidebar list --site mysite --format json --fields id,name,status
Requires edit_theme_options capability. Read-only.
Manage taxonomy terms (categories, tags, custom)
# Categories via the alias (pre-sets --taxonomy=category)
wpa category list --site mysite
wpa category list --site mysite --search "news"
wpa category create --site mysite --name "Announcements" --description "Site announcements"
wpa category update 7 --site mysite --description "Major site announcements"
wpa category delete 7 --site mysite # always permanent; terms cannot be trashed
# Tags via the alias (pre-sets --taxonomy=post_tag)
wpa tag list --site mysite
wpa tag create --site mysite --name "wordpress" --description "Posts about WordPress"
wpa tag delete 12 --site mysite
# Generic term interface for built-in or custom taxonomies
wpa term list --site mysite --taxonomy category
wpa term list --site mysite --taxonomy post_tag
wpa term list --site mysite --taxonomy genre # custom taxonomy
wpa term get 7 --site mysite --taxonomy category
wpa term create --site mysite --taxonomy post_tag --name "api"
wpa term update 7 --site mysite --taxonomy category --name "Big Announcements"
wpa term delete 7 --site mysite --taxonomy category
Note on delete: The WordPress REST API does not support trashing taxonomy terms, so wpa term delete (and the category / tag aliases) always performs a permanent delete. There is no --force flag — force is implicit.
Output options
All list commands support these output modifiers:
# Output only IDs
wpa post list --site mysite --ids
# Output only the count
wpa post list --site mysite --count
# Output a single field per result
wpa post list --site mysite --field title
# Select specific columns
wpa user list --site mysite --fields id,username,email
# Debug mode (print HTTP request/response details)
wpa post list --site mysite --debug
Site management
wpa site add
wpa site list
wpa --version
Multi-site behavior
| Configs | --site flag |
Behavior |
|---|---|---|
| 0 | No | Prompts to create a new config |
| 1 | No | Uses the single config automatically |
| 2+ | No | Prompts to select from list |
| Any | Yes | Uses the named config (error if not found) |
Markdown file format
---
title: "Your Page Title"
slug: "your-page-slug"
status: draft
author: 15
---
Page content in markdown here...
title(required): Page titleslug(optional): URL slugstatus(optional):draft(default),publish,pending, orprivateauthor(optional): Author user ID; the--authorCLI flag overrides it
Site config format
Each site config is stored at ~/.config/wpa/<name>/.env:
WP_SITE_URL=https://example.com
WP_USER=your-username
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx
WP_ADMIN_PATH=wp-admin
WP_ADMIN_PATHis optional (defaults towp-admin). Override it if your site uses a custom admin URL.- The
XDG_CONFIG_HOMEenvironment variable is respected if set.
Environment variables
Two protective caps in the API client can be resized per environment:
| Variable | Default | Purpose |
|---|---|---|
WPA_MAX_RESPONSE_BYTES |
52428800 (50 MB) |
Maximum size of a single REST API response |
WPA_MAX_TOTAL_PAGES |
1000 |
Ceiling on pages fetched by paginated list commands |
Invalid values (non-integer, zero, negative) are ignored with a warning and the default applies — a misconfigured environment can resize the caps but never disable them.
Migration from repo-root .env
If you have an existing .env in the repo root and no XDG configs, the tool will offer to migrate it on first run.
Safety and Security
- Default status is always
draft— never publishes unless frontmatter explicitly says otherwise - HTTPS enforced for public addresses — rejects
http://for public URLs; allows HTTP for private/LAN addresses (RFC 1918, localhost,.lan/.local/.test/.internalTLDs) with a warning - Credentials in XDG config — stored outside the repo at
~/.config/wpa/with 600 permissions - Password input hidden — uses
getpassduring interactive setup - Status validation — rejects typos and invalid values in frontmatter
- Site name validation — only alphanumeric characters and hyphens allowed
- Connection error handling — timeouts and network failures produce clear messages, not tracebacks
Development
pip install -e '.[dev]'
pytest --cov=wpa --cov-report=term-missing
Links
- Product Requirements Document — Vision, design principles, command structure, and implementation roadmap
- Getting Started — REST API setup, Wordfence notes, staging guide
- Release Notes
- Contributing
- WP-CLI Command Inventory — Complete catalog of WP-CLI 2.12.0 commands used as the template for WPA planning
- WP-CLI REST API Mapping Matrix — Feasibility classification of every WP-CLI command against the REST API
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wpa-0.10.0.tar.gz.
File metadata
- Download URL: wpa-0.10.0.tar.gz
- Upload date:
- Size: 80.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9c1bac65e254f2d3f8a5597c526632b1403b98fcedab6233d8296122da7d7f5
|
|
| MD5 |
ffdeac381438b1f0b701a5f9decbd5d3
|
|
| BLAKE2b-256 |
3c4ce154bbca7137c65f0a90391f0dc78c0042668f20cac7cf7a07503877c159
|
Provenance
The following attestation bundles were made for wpa-0.10.0.tar.gz:
Publisher:
publish.yml on cadentdev/wpa
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wpa-0.10.0.tar.gz -
Subject digest:
a9c1bac65e254f2d3f8a5597c526632b1403b98fcedab6233d8296122da7d7f5 - Sigstore transparency entry: 2403715355
- Sigstore integration time:
-
Permalink:
cadentdev/wpa@b7ac89f564643491104f32609dbd9038b7a5b28c -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/cadentdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7ac89f564643491104f32609dbd9038b7a5b28c -
Trigger Event:
release
-
Statement type:
File details
Details for the file wpa-0.10.0-py3-none-any.whl.
File metadata
- Download URL: wpa-0.10.0-py3-none-any.whl
- Upload date:
- Size: 58.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f371344029240e463d6f3a83897a4e0a8f514aa37f7bc2b62a1d276949451d7e
|
|
| MD5 |
0a6f7c7d020ce847da93c1cbeae08954
|
|
| BLAKE2b-256 |
381d95480ef17582304ce364e402935594eae09a6ba67c183a524178b3f56b35
|
Provenance
The following attestation bundles were made for wpa-0.10.0-py3-none-any.whl:
Publisher:
publish.yml on cadentdev/wpa
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wpa-0.10.0-py3-none-any.whl -
Subject digest:
f371344029240e463d6f3a83897a4e0a8f514aa37f7bc2b62a1d276949451d7e - Sigstore transparency entry: 2403715572
- Sigstore integration time:
-
Permalink:
cadentdev/wpa@b7ac89f564643491104f32609dbd9038b7a5b28c -
Branch / Tag:
refs/tags/v0.10.0 - Owner: https://github.com/cadentdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b7ac89f564643491104f32609dbd9038b7a5b28c -
Trigger Event:
release
-
Statement type: