You can use either:
- n8n WordPress node (simpler for basic post creation)
- HTTP Request node to
/wp-json/wp/v2/posts (more control)
A typical post creation payload includes:
title
content
status (draft or publish)
categories
excerpt (optional)
Example request body:
{
"title": "Designing Human-in-the-Loop AI Workflows",
"content": "Your HTML content here...",
"status": "publish",
"excerpt": "A practical introduction to HITL workflow design.",
"categories": [30]
}
Important output to capture
After post creation, store the returned post ID:
You will need this in later steps to:
- assign the featured image
- update Yoast SEO fields
Step 2: Download the Featured Image as Binary in n8n
WordPress Media API expects file content, not just an image URL.
If your image is coming from a URL (Google Drive URL, generated image URL, etc.), add an n8n HTTP Request node to download the image first.
n8n settings (Download node)
- Method: GET
- URL: your image URL
- Response Format: File
- Download: enabled (binary output)
This should produce binary data (for example under binary.data).
If you do not pass binary correctly, the next media upload step will fail or upload an invalid file.
Use an HTTP Request node in n8n (not the WordPress node) for this step.
Endpoint
POST /wp-json/wp/v2/media
Auth
Use your WordPress credentials (Application Passwords with Basic Auth is the simplest and most reliable).
Required details
- Send binary data
- Set correct
Content-Type
- Set
Content-Disposition header with a file name
Content-Disposition: attachment; filename="featured-image.jpg"
Content-Type: image/jpeg
If your file is PNG, use image/png, etc.
Common Upload Error and Fix
One common error is:
rest_upload_sideload_error
“Sorry, you are not allowed to upload this file type.”
This usually means one of the following:
- MIME type/header mismatch
- You are uploading a JPEG but sending
image/png
- Or no
Content-Type is set
- File extension missing/wrong
- WordPress checks file extension and mime type
- Host security/plugin restrictions
- Some hosts block certain uploads or formats (e.g., WebP, SVG)
- The binary content is not actually an image
- You passed text/JSON/redirect page instead of the image bytes
Fix checklist
- Confirm the image was actually downloaded as binary in n8n
- Use a normal
.jpg or .png
- Set
Content-Disposition with a valid filename and extension
- Set correct
Content-Type
- Test with a known working JPEG first
Once this step succeeds, WordPress returns a media object with an id, for example:
Save that ID. You need it for featured_media.
Step 4: Assign the Uploaded Image as the Post’s Featured Image
Uploading media does not automatically assign it as the featured image.
You must update the post separately.
Endpoint
POST /wp-json/wp/v2/posts/{post_id}
Example:
POST /wp-json/wp/v2/posts/5437
Body
{
"featured_media": 5436
}
If the request succeeds, the response should show:
"featured_media": 5436
Important note
Even if the image exists in Media Library, it will not appear on the post unless this field is explicitly updated.
Step 5: Why Yoast SEO Fields Don’t Save Through the Standard Post Endpoint (Usually)
This is where many workflows break.
You might try to update a post with something like:
{
"meta": {
"_yoast_wpseo_title": "My SEO Title",
"_yoast_wpseo_metadesc": "My description",
"_yoast_wpseo_focuskw": "focus keyword"
}
}
And WordPress returns 200 OK—but Yoast fields do not save.
Why?
The reason
Yoast stores these values in post meta, but:
- those meta keys are protected (
_yoast_wpseo_*)
- WordPress REST will ignore protected meta keys unless they are explicitly registered and exposed in REST
In some environments, registering them via register_post_meta() works. In others, the keys still do not appear in the REST schema due to theme/plugin/security stack behavior.
If your GET response (?context=edit) does not show the Yoast meta keys under meta, the standard /wp/v2/posts/{id} endpoint will not save them.
The Free Reliable Solution: Create a Tiny Custom REST Endpoint for Yoast Fields
Instead of relying on the default post endpoint to accept protected meta keys, create your own endpoint that directly updates post meta using WordPress functions.
This avoids paid plugins and gives you full control.
Add this code to your child theme functions.php (or Code Snippets plugin)
<?php
/**
* Free custom REST endpoint to update Yoast SEO meta.
* Route: POST /wp-json/intelligex/v1/yoast-meta
*/
add_action('rest_api_init', function () {
register_rest_route('intelligex/v1', '/yoast-meta', [
'methods' => 'POST',
'callback' => 'intelligex_update_yoast_meta',
'permission_callback' => function (WP_REST_Request $request) {
$post_id = (int) $request->get_param('post_id');
return $post_id > 0 && current_user_can('edit_post', $post_id);
},
'args' => [
'post_id' => [
'required' => true,
'type' => 'integer',
],
'seo_title' => [
'required' => false,
'type' => 'string',
],
'meta_description' => [
'required' => false,
'type' => 'string',
],
'focus_keyword' => [
'required' => false,
'type' => 'string',
],
'canonical' => [
'required' => false,
'type' => 'string',
],
],
]);
});
function intelligex_update_yoast_meta(WP_REST_Request $request) {
$post_id = (int) $request->get_param('post_id');
if (!$post_id || get_post($post_id) === null) {
return new WP_REST_Response([
'success' => false,
'message' => 'Invalid post_id',
], 400);
}
$updated = [];
if ($request->has_param('seo_title')) {
$value = sanitize_text_field((string) $request->get_param('seo_title'));
update_post_meta($post_id, '_yoast_wpseo_title', $value);
$updated['_yoast_wpseo_title'] = $value;
}
if ($request->has_param('meta_description')) {
$value = sanitize_textarea_field((string) $request->get_param('meta_description'));
update_post_meta($post_id, '_yoast_wpseo_metadesc', $value);
$updated['_yoast_wpseo_metadesc'] = $value;
}
if ($request->has_param('focus_keyword')) {
$value = sanitize_text_field((string) $request->get_param('focus_keyword'));
update_post_meta($post_id, '_yoast_wpseo_focuskw', $value);
$updated['_yoast_wpseo_focuskw'] = $value;
}
if ($request->has_param('canonical')) {
$value = esc_url_raw((string) $request->get_param('canonical'));
update_post_meta($post_id, '_yoast_wpseo_canonical', $value);
$updated['_yoast_wpseo_canonical'] = $value;
}
clean_post_cache($post_id);
wp_update_post([
'ID' => $post_id,
]);
return new WP_REST_Response([
'success' => true,
'post_id' => $post_id,
'updated' => $updated,
'saved_meta_check' => [
'_yoast_wpseo_title' => get_post_meta($post_id, '_yoast_wpseo_title', true),
'_yoast_wpseo_metadesc' => get_post_meta($post_id, '_yoast_wpseo_metadesc', true),
'_yoast_wpseo_focuskw' => get_post_meta($post_id, '_yoast_wpseo_focuskw', true),
'_yoast_wpseo_canonical'=> get_post_meta($post_id, '_yoast_wpseo_canonical', true),
],
], 200);
}
Step 6: Create the n8n Node to Update Yoast SEO Fields
Add a new HTTP Request node in n8n.
Settings
- Method:
POST
- URL:
https://yourdomain.com/wp-json/intelligex/v1/yoast-meta
- Auth: same WordPress credentials
- Headers:
Content-Type: application/json
- Body Content Type: JSON
Example body
{
"post_id": 5437,
"seo_title": "An Introduction to Human-in-the-Loop (HITL) Workflow Design",
"meta_description": "Learn how to design Human-in-the-Loop AI workflows that improve accuracy, trust, and continuous learning.",
"focus_keyword": "human in the loop workflow design"
}
With n8n expressions
Replace values with expressions from your prior nodes (AI output / spreadsheet / parser node).
For example:
post_id from Create Post node
seo_title from AI metadata generation node
meta_description from parsed fields
focus_keyword from your keyword logic
Important Clarification: yoast_head_json Is Output, Not Storage
When debugging, many people see this in the WordPress REST response:
yoast_head
yoast_head_json
And assume Yoast metadata must be saved there.
That is not the case.
What yoast_head_json is
It is a rendered, read-only SEO output object generated by Yoast for the post. It may include:
- title
- og title
- og description
- canonical URL
- image
- robots tags
- schema graph
Yoast can generate these values from:
- post title
- excerpt
- featured image
- permalink
- default Yoast templates
So seeing a title or description inside yoast_head_json does not prove your custom SEO fields were saved.
This distinction matters a lot when troubleshooting.
How to Confirm Everything Works
After your workflow runs, verify all three layers:
1) Post exists
Check the post URL or call:
GET /wp-json/wp/v2/posts/{id}
2) Featured image is assigned
Confirm:
featured_media is not 0
yoast_head_json.og_image points to your uploaded image
- front-end post shows the correct image (depending on theme behavior)
3) Yoast SEO fields saved
Best options:
- Check in the WordPress admin editor (Yoast panel)
- Inspect the custom endpoint response (
saved_meta_check)
- Confirm final rendered
yoast_head_json reflects your custom values (may need cache clear/refresh)
Suggested n8n Workflow Sequence (Recommended)
For reliability, split the workflow into separate nodes:
- Prepare content
- Create post
- Download image
- Upload media
- Update post featured image
- Update Yoast SEO meta
- Optional final validation GET
- Optional notification (Slack/email)
Why separate nodes are better
- Easier to debug failures
- Easier retry logic
- Clear audit trail in execution logs
- You can re-run only failed steps (e.g., reassign image without recreating post)
Common Pitfalls and How to Avoid Them
1) Upload succeeds but image is not featured
You uploaded to /media but forgot to update the post with featured_media.
Fix: Add a separate post update step.
2) WordPress says wrong file type
Usually header mismatch or binary is incorrect.
Fix: Validate binary data and send correct filename + content type.
3) Yoast fields “seem” to work because yoast_head_json has values
Yoast is often generating defaults from the post content.
Fix: Verify saved values through your endpoint response or in the WP admin editor.
The standard post endpoint can silently ignore protected meta fields.
Fix: Use a custom REST endpoint that calls update_post_meta() directly.
Caching (theme cache, page cache, object cache, CDN cache, or Yoast indexables timing) can delay visible changes.
Fixes:
- Refresh the post in admin
- Clear site cache/CDN cache
- Re-fetch the post after a short delay
- Keep your workflow idempotent (safe to retry)
Security and Maintenance Notes
Since you are exposing a custom REST endpoint, keep it secure.
Best practices
- Use WordPress auth (Application Passwords)
- Restrict permissions with
current_user_can('edit_post', $post_id)
- Sanitize all fields
- Only allow the fields you need
- Do not create a public unauthenticated endpoint for post updates
Maintenance
If you later want to support more Yoast fields, you can add them easily:
- canonical
- social title/description
- noindex flags (with care)
- schema-related metadata (advanced)
Keep the endpoint small and explicit.
Optional Enhancements for a Production Pipeline
Once the core workflow works, you can improve it further.
1) Add slug control
Send a custom slug when creating the post:
{
"slug": "designing-human-in-the-loop-ai-workflows"
}
Use taxonomy IDs from your own category/tag mapping logic.
Generate:
- page title
- meta description
- excerpt
- focus keyword in one AI prompt, then parse into n8n fields.
4) Add alt text/caption to the uploaded media
After media upload, update the media item:
alt_text
caption
description
This is especially useful for accessibility and SEO.
5) Build a “publish or draft” switch
In n8n, use a variable or conditional:
draft for review mode
publish for auto-publish mode
6) Add a final QA node
Run a GET request and validate:
- post status
- featured image assigned
- Yoast title exists
- meta description length is within your target
Example End-to-End Publish Flow (Conceptual)
Imagine this input from your content generation step:
- Post title: An Introduction to Human-in-the-Loop (HITL) Workflow Design
- HTML body
- Featured image URL
- SEO title
- Meta description
- Focus keyword
Your n8n workflow then:
- Creates the post → gets
post_id = 5437
- Downloads image → binary file
- Uploads image → gets
media_id = 5436
- Updates post with
"featured_media": 5436
- Calls
/wp-json/intelligex/v1/yoast-meta with:
post_id = 5437
seo_title = ...
meta_description = ...
focus_keyword = ...
- Optionally validates the final post response
Result:
- WordPress post published
- Featured image assigned
- Yoast SEO fields saved
- Fully automated, no paid plugin required
Final Thoughts
Automating WordPress publishing with n8n is absolutely possible, but the trick is treating it as a sequence of explicit API operations instead of expecting one node to handle everything.
The biggest unlocks are:
- Use the Media endpoint for image upload
- Use a separate post update for
featured_media
- Use a small custom REST endpoint for Yoast SEO fields
Once that is in place, you have a robust and scalable content pipeline that can support:
- AI-generated content
- editorial approvals
- multilingual content publishing
- structured SEO metadata
- batch publishing workflows
Most importantly, you avoid paying for a connector plugin just to write a few post meta fields.
If you are already using n8n as the orchestration layer for content operations, this setup gives you a clean foundation for a much more advanced publishing system.
Quick Implementation Checklist
Use this checklist while building your workflow: