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)

  1. Log in to WP dashboard → Users → Profile → Application Passwords
  2. Create a new password, call it openclaw. Copy the 24-character string.
  3. 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

  1. Monday—gather links. Drop URLs in the #newsletter channel. Allow the agent to save them to memory with the built-in summariser skill.
  2. Wednesday—draft. Ask OpenClaw: /summarise last 5 links → markdown. Paste the output into /skill wordpress as a draft.
  3. Thursday—editorial review. Coworkers comment in the same thread. Iterate via postId.
  4. 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.