WordPress
Category: Publishing / CMS
Integration type: Tenant-configured (REST API or WP CLI; stored in integrations)
External API: WordPress REST API v2 or WP CLI via SSH
Purpose
WordPress is the primary blog publishing destination. After the Blog Writer agent generates content and HITL approval is granted, the Blog Publisher worker pushes the post to the tenant’s WordPress site. The integration supports:
- Creating draft posts (always — never auto-publish)
- Uploading featured images
- Setting categories and tags
- Attaching author metadata
- Checking post status after upload
The platform always creates posts as draft — publication is the DM agency’s responsibility. Auto-publish is intentionally not supported.
Config Structure
Tenants can connect WordPress in two ways:
Option A: WordPress REST API (recommended)
Available on any WordPress site (hosted or self-hosted). Requires an Application Password (WP 5.6+):
interface WordPressRestConfig {
siteUrl: string; // e.g. "https://acmeplumbing.com"
username: string; // WordPress username
appPassword: string; // Application Password (generated in WP → Users → Profile → Application Passwords)
}Option B: WP CLI over SSH (advanced)
For tenants where REST API is disabled or firewalled:
interface WordPressSshConfig {
host: string; // SSH host
port: number; // SSH port (default 22)
username: string; // SSH username
privateKey: string; // RSA private key (PEM format)
wpPath: string; // Path to WordPress installation, e.g. "/var/www/html"
}Both config types are stored in integrations.api_key (encrypted) and differentiated by metadata.connectionType: 'rest' | 'ssh'.
Integration Pattern
REST API connection (packages/tools/src/wordpress.ts)
class WordPressRestClient {
private auth: string;
constructor(private cfg: WordPressRestConfig) {
this.auth = Buffer.from(`${cfg.username}:${cfg.appPassword}`).toString('base64');
}
async createDraftPost(post: {
title: string;
content: string; // HTML
excerpt?: string;
categories?: number[];
tags?: number[];
authorId?: number;
featuredImageId?: number;
slug?: string;
}): Promise<{ postId: number; editUrl: string }> {
const response = await axios.post(
`${this.cfg.siteUrl}/wp-json/wp/v2/posts`,
{
title: post.title,
content: post.content,
excerpt: post.excerpt,
status: 'draft', // Always draft
categories: post.categories ?? [],
tags: post.tags ?? [],
author: post.authorId,
featured_media: post.featuredImageId,
slug: post.slug,
},
{
headers: {
Authorization: `Basic ${this.auth}`,
'Content-Type': 'application/json',
},
},
);
return {
postId: response.data.id,
editUrl: `${this.cfg.siteUrl}/wp-admin/post.php?post=${response.data.id}&action=edit`,
};
}
async uploadImage(imageBuffer: Buffer, filename: string, mimeType: string): Promise<{ mediaId: number; url: string }> {
const response = await axios.post(
`${this.cfg.siteUrl}/wp-json/wp/v2/media`,
imageBuffer,
{
headers: {
Authorization: `Basic ${this.auth}`,
'Content-Type': mimeType,
'Content-Disposition': `attachment; filename="${filename}"`,
},
},
);
return {
mediaId: response.data.id,
url: response.data.source_url,
};
}
async getCategories(): Promise<{ id: number; name: string; slug: string }[]> {
const response = await axios.get(
`${this.cfg.siteUrl}/wp-json/wp/v2/categories?per_page=100`,
{ headers: { Authorization: `Basic ${this.auth}` } },
);
return response.data.map((c: any) => ({ id: c.id, name: c.name, slug: c.slug }));
}
async verify(): Promise<void> {
// Test credentials by fetching current user info
const response = await axios.get(
`${this.cfg.siteUrl}/wp-json/wp/v2/users/me`,
{ headers: { Authorization: `Basic ${this.auth}` } },
);
if (!response.data.id) throw new Error('WordPress verification failed');
}
}Blog Publisher workflow
Blog Writer agent completes → BlogPost stored in MongoDB
│
▼ (HITL approval granted)
Blog Publisher worker picks up
│
├── Fetch BlogPost content from MongoDB
├── Fetch featured image from S3 (if any)
├── Upload image to WP Media Library
├── Resolve category IDs from tenant's WP site
├── Create draft post via REST API
└── Store postId + editUrl in blog_posts.external_id + blog_posts.publish_urlTest Cases
Unit tests (packages/tools/src/wordpress.test.ts)
| Test | Approach |
|---|---|
createDraftPost() always sets status: 'draft' | Mock axios.post; assert status === 'draft' |
createDraftPost() sends correct auth header | Assert Authorization: Basic ... |
createDraftPost() returns postId and editUrl | Mock response { id: 42 }; assert editUrl contains 42 |
uploadImage() sends image as binary body | Mock axios.post; assert Content-Type and Content-Disposition headers |
verify() calls /users/me endpoint | Assert URL and auth header |
verify() throws when id absent | Mock {}; assert error thrown |
| Throws on 401 (invalid app password) | Mock 401; assert error |
| Throws on 404 (WP REST API disabled) | Mock 404; assert descriptive error |
Integration tests
| Test | Approach |
|---|---|
| Create draft on WordPress.com test site | Use test site; assert post created in drafts |
| Upload image and use as featured media | Upload 1×1 PNG; create post with featuredImageId; assert media attached |
| Verify with valid credentials | Assert user info returned |
Local WP dev environment
Use Local (by Flywheel) or Docker WP for local integration testing:
# docker-compose.dev.yml
wordpress:
image: wordpress:latest
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wp
WORDPRESS_DB_PASSWORD: wp
WORDPRESS_DB_NAME: wp
depends_on:
- dbRelated
- Blog Writer Agent — generates blog content
- AWS S3 Provider — featured image storage before WP upload
- Tool Integration Layer — integration management