If you already use OpenClaw as your editorial sidekick, the next logical step is wiring it to WordPress so new drafts go live with a single /publish. This guide walks through a full "zero-click" workflow—connecting OpenClaw to the WordPress REST API, managing media, categories and tags, and triggering the publish flow straight from a chat thread. No plugins to install on WP, no browser macro hacks—just the native API and a ~40-line skill.
Why bother automating WordPress publishing with OpenClaw?
Three reasons keep coming up in GitHub issues and our Discord:
- Context stays in chat. Writers do research, get feedback and iterate on copy in Telegram/Slack anyway. Pushing from the same thread cuts context switching.
- Meta automation. The agent can append canonical links, optimise images, run Grammarly or GPT-4 passes, then ship.
- Multi-site consistency. Once the skill exists, pointing it at another WordPress install is a one-line config tweak.
Prerequisites: WordPress REST API & OpenClaw basics
This tutorial was written against:
- WordPress 6.5.2 (self-hosted)
- OpenClaw 0.17.4 (Node 22.2 LTS)
- Composio SDK 1.9.0 for OAuth helpers (optional)
You need editor or admin rights on the WordPress instance and a working OpenClaw daemon already connected to your chat platform of choice. If you are new to OpenClaw hosting, spin up an agent on ClawCloud—Settings → API Keys → Add Custom Skill is where we will drop the code.
Generate WordPress credentials
Option 1: Application Passwords (simplest)
- Log in to WP dashboard → Users → Profile → Application Passwords
- Create a new password, call it
openclaw. Copy the 24-character string. - Note your site URL. We will hit
https://YOUR_SITE/wp-json/wp/v2/.
Application passwords are fine for personal blogs. For multi-author setups or SaaS WP hosting, use OAuth.
Option 2: OAuth 2.0 via Composio
Composio already ships an OAuth wrapper for WordPress.com and generic self-hosted WP. Run:
npm i @composio/sdk
Create a Composio app, set redirect URI to https://gateway.openclaw.ai/oauth/callback, then grab the client_id and client_secret. The rest looks identical once tokens hit the agent vault.
Scaffolding the WordPress skill
In your OpenClaw agent directory:
mkdir -p skills/wordpress
cd skills/wordpress
npm init -y
npm i node-fetch@3 base-64
Then edit index.js:
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
const base64 = require('base-64');
module.exports = {
description: 'Create, edit and publish WordPress posts',
parameters: {
type: 'object',
properties: {
title: {type: 'string'},
content: {type: 'string'},
status: {type: 'string', enum: ['draft','publish'], default: 'draft'},
categories: {type: 'array', items: {type: 'string'}},
tags: {type: 'array', items: {type: 'string'}},
mediaUrls: {type: 'array', items: {type: 'string'}},
postId: {type: 'integer'}
},
required: ['content']
},
handler: async ({title, content, status, categories=[], tags=[], mediaUrls=[], postId}, ctx) => {
const site = process.env.WP_SITE; // e.g. https://blog.example.com
const user = process.env.WP_USER;
const pass = process.env.WP_APP_PASSWORD;
const auth = 'Basic ' + base64.encode(`${user}:${pass}`);
async function ensureTerm(type, name) {
const search = await fetch(`${site}/wp-json/wp/v2/${type}?search=${encodeURIComponent(name)}`, {headers:{Authorization:auth}});
const existing = await search.json();
if (existing.length) return existing[0].id;
const create = await fetch(`${site}/wp-json/wp/v2/${type}`, {
method:'POST',
headers:{'Content-Type':'application/json', Authorization:auth},
body:JSON.stringify({name})
});
const created = await create.json();
return created.id;
}
const categoryIds = await Promise.all(categories.map(c=>ensureTerm('categories',c)));
const tagIds = await Promise.all(tags.map(t=>ensureTerm('tags',t)));
// handle media uploads
const mediaIds = [];
for(const url of mediaUrls){
const img = await fetch(url);
const buf = await img.buffer();
const filename = url.split('/').pop();
const upload = await fetch(`${site}/wp-json/wp/v2/media`, {
method: 'POST',
headers: { Authorization:auth, 'Content-Disposition':`attachment; filename="${filename}"` },
body: buf
});
const uploaded = await upload.json();
mediaIds.push(uploaded.id);
// naive: replace occurrences of URL with WP URL
content = content.replaceAll(url, uploaded.source_url);
}
const body = { title, content, status, categories:categoryIds, tags:tagIds, featured_media:mediaIds[0] };
const endpoint = postId ? `${site}/wp-json/wp/v2/posts/${postId}` : `${site}/wp-json/wp/v2/posts`;
const method = postId ? 'POST' : 'POST';
const res = await fetch(endpoint, { method, headers:{'Content-Type':'application/json', Authorization:auth}, body:JSON.stringify(body)});
const json = await res.json();
return {message:`${json.link} (${json.status})`};
}
};
Drop three environment variables into your agent config (or ClawCloud secrets):
WP_SITE=https://blog.example.com
WP_USER=alice
WP_APP_PASSWORD=abcd efgh ijkl mnop qrst uvwx
Restart the daemon:
openclaw daemon restart
Drafting, revising and publishing from chat
1. Create an initial draft
In your chat channel (Telegram example):
/skill wordpress "title=My Remote Draft" "categories=Automation,OpenClaw" "tags=wordpress,api" <
The agent should respond with something like:
https://blog.example.com/?p=1234 (draft)
2. Iterate
Edit the draft ID 1234:
/skill wordpress "postId=1234" "status=draft" <
3. Publish
When ready:
/skill wordpress "postId=1234" "status=publish"
The link now renders publicly, and WordPress handles RSS, XML-RPC pings, whatever else your plug-ins do.
Handling images, categories and tags at scale
WordPress rejects duplicated term names. The helper ensureTerm handles idempotency, but here are some real-world caveats:
- Multiple taxonomy levels. If you pass
Automation › OpenClaw, slashes confuse the REST API. Use slug parameters or pre-create hierarchy.
- Media file size limits. Shared hosting often caps at 2 MB. The skill uploads raw buffers; do a size check first or run an
ffmpeg compression micro-service.
- Alt text. The example sets
featured_media but no alt text. Add a ?alt_text= param on the media endpoint or build a second pass.
Full weekly newsletter workflow
- Monday—gather links. Drop URLs in the #newsletter channel. Allow the agent to save them to memory with the built-in summariser skill.
- Wednesday—draft. Ask OpenClaw:
/summarise last 5 links → markdown. Paste the output into /skill wordpress as a draft.
- Thursday—editorial review. Coworkers comment in the same thread. Iterate via
postId.
- Friday 09:00—publish. A scheduled task triggers
/skill wordpress postId=ID status=publish. ClawCloud’s cron syntax goes in tasks.yaml:
- id: weekly-newsletter
schedule: '0 9 * * 5'
command: skill wordpress postId=ID status=publish
The result: human input only on Wednesday; everything else is automated.
Common pitfalls and debugging tips
- 401 Unauthorized. Application passwords break if the user’s role lacks
edit_posts. Double-check capabilities.
- CORS preflight. When testing with
curl, WP might return Access-Control-Allow-Origin errors. OpenClaw runs server-side, so ignore them.
- JSON malformed. The agent input schema forces arrays for tags and categories, but chat UIs send strings. Quote your arrays:
"tags=foo,bar" becomes ["foo","bar"] after simple .split() logic—add it if teammates keep forgetting.
- Permalinks. If your site uses /%postname%/ and you’re on Nginx, the REST API still works but links in responses point to
?p=ID until the post is published. Not a bug, just WP caching.
- SSL terminates upstream. ClawCloud containers are IPv6-only behind Cloudflare. If your WordPress instance blocks IPv6, tunnel via
cloudflared or edit fail2ban rules.
Next steps
You now have a functioning "chat → WordPress" deploy pipeline. The obvious extensions:
- Hook the skill’s response into a Slack workflow so editors get a clickable "Preview" button.
- Run a second skill that snaps screenshots of the published page using the built-in browser control for visual QA.
- Add a
status=private flag for paywalled MemberPress content.
- Port the code to TypeScript and share it on github.com/OpenClaw/skills. The community will review and improve.
Questions? Jump into #wordpress-integration on Discord. We monitor the channel and merge PRs daily.