Automated WordPress Publishing from Google Sheets (AI + Thumbnail)

1) Overview

This n8n workflow automates:

  1. Read rows from Google Sheets where status = ready
  2. Pick one valid row (lowest number, and post_id is empty)
  3. Generate a thumbnail image with Gemini → upload to WordPress Media → update image SEO fields
  4. Generate post content with OpenAI (GPT-4.1)
  5. Merge image + content → create a WordPress page with featured image
  6. Call a custom WP endpoint to change the created page into a post
  7. Update Google Sheet: post_id, url, status = done
  8. Send a Slack notification (optional)

2) Prerequisites

Google Sheet (required columns)

Your sheet should include at least:

  • number (used for priority sorting)
  • title
  • summary
  • image_prompt
  • status (workflow filters ready)
  • post_id (used to prevent duplicates)
  • row_number (read-only field from the n8n Sheets node; used for updates)
  • url (workflow writes back)

Status convention

  • ready: waiting to be processed
  • done: completed

WordPress

Must have:

  • Working authentication (Basic Auth in your HTTP nodes)
  • REST endpoints:

– Media upload: /wp-json/wp/v2/media – Media update: /wp-json/wp/v2/media/{id} – Custom endpoint (your site): /wp-json/custom/v1/change-post-type/{id}

Credentials in n8n

  • Google Sheets OAuth2
  • OpenAI API
  • Google Gemini(PaLM) API
  • WordPress API credentials (WordPress node)
  • HTTP Basic Auth (HTTP Request nodes)
  • Slack API (optional)

3) High-level Flow

Schedule Trigger
  → Readsheet (Google Sheets: filter status=ready)
    → extract_item (Code: filter + sort + pick 1 row)
      → extract_data (Set: normalize fields)
        ├─ create_image (Gemini: generate thumbnail)
        │   → upload_image (HTTP: upload to WP media)
        │     ├─ update_media_id (Set: media_id)
        │     │   → Merge (combineAll)
        │     └─ Update media SEO (HTTP: alt/caption/desc)
        └─ create_post_content (OpenAI: write content)
            → Merge (combineAll)
              → prepare_post (Set: map title/content/media)
                → create_post (WordPress: Create page)
                  → HTTP Request (custom: change post_type=post)
                    → Update row in sheet (write post_id/url/status)
                      → Send a message (Slack)

4) Node-by-node Documentation

4.1 Schedule Trigger

Node: Schedule Trigger

Purpose: Runs on a schedule (configured to run every “minutes”).


4.2 Readsheet (Google Sheets)

Node: Readsheet

Purpose: Reads rows from the spreadsheet.

Filter: status = ready

Output: Multiple items (each item = one row).


4.3 extract_item (Code)

Node: extract_item

Purpose: Select exactly one row to process.

Logic:

  • Keep rows where:

status equals readypost_id is empty

  • Sort by number ascending
  • Return only the first row
  • If no rows match, return [] (workflow ends naturally)

Code (from your workflow):

// Collect rows
const rows = items.map(i => i.json);

// Filter: status=ready and post_id empty
const filtered = rows.filter(r => {
  const status = String(r.status || '').trim().toLowerCase();
  const postId = r.post_id === undefined || r.post_id === null || String(r.post_id).trim() === '';
  return status === 'ready' && postId;
});

// Stop if none
if (filtered.length === 0) return [];

// Sort by number asc
filtered.sort((a, b) => Number(a.number) - Number(b.number));

// Return one row
return [{ json: filtered[0] }];

4.4 extract_data (Set)

Node: extract_data

Purpose: Normalize the row into a clean JSON payload used by downstream nodes.

Creates:

  • title
  • summary
  • image_prompt
  • rowNumber (from row_number)

Note: Later, the sheet update uses $('extract_item').item.json.row_number (which is correct for matching).


4.5 create_image (Google Gemini)

Node: create_image

Purpose: Generate a modern blog thumbnail.

Inputs: title, image_prompt

Style rules: minimalist, tech-focused, 16:9, no text in image, strong padding/negative space.


4.6 upload_image (HTTP → WP Media Upload)

Node: upload_image

Request: POST https://<your-domai>/wp-json/wp/v2/media

Auth: HTTP Basic Auth

Body: binary image data field named data

Headers:

  • Content-Disposition: generates a slug filename from title
  • Content-Type: uses the image mimeType

Output: WordPress media object including id (attachment ID).


4.7 Update media SEO (HTTP)

Node: Update media SEO

Request: POST https://<your-domai>/wp-json/wp/v2/media/{{ $json.id }}

Purpose: Set:

  • alt_text
  • caption
  • description

All set to the post title for better SEO consistency.


4.8 update_media_id (Set)

Node: update_media_id

Purpose: Create a clean field for merging:

  • media_id = $json.id

4.9 create_post_content (OpenAI GPT-4.1)

Node: create_post_content

Model: gpt-4.1

Purpose: Generate the full article content.

System prompt: Technical blogger, SEO-friendly, Gutenberg-ready HTML.

User prompt: Uses title + summary as context, but must NOT repeat them verbatim in the body.

Output is referenced later as:

  • {{$json.output[0].content[0].text}}

4.10 Merge (combineAll)

Node: Merge

Mode: combineAll

Purpose: Merge outputs from:

  • create_post_content (text content)
  • update_media_id (featured media id)

So downstream nodes have both content + media_id.


4.11 prepare_post (Set)

Node: prepare_post

Purpose: Assemble the final payload for WordPress creation.

Sets:

  • title = $('extract_data').item.json.title
  • content = $json.output[0].content[0].text
  • media = $json.media_id

⚠️ Potential improvement: media is typed as string in your workflow. WordPress often expects a number ID. If you see issues, change the type to number.


4.12 create_post (WordPress)

Node: create_post

Resource: page

Operation: create

Fields:

  • title = $json.title
  • content = $json.content
  • status = publish
  • featuredMediaId = $json.media

Output includes:

  • id
  • link
  • date
  • title.raw

4.13 HTTP Request (Custom: change post type)

Node: HTTP Request

Request: POST https://<your-domai>/wp-json/custom/v1/change-post-type/{{ $json.id }}

Body:

{ "post_type": "post" }

Purpose: Convert the created “page” into a “post”.


4.14 Update row in sheet (Google Sheets)

Node: Update row in sheet

Operation: update

Matching column: row_number

Writes back:

  • row_number = $('extract_item').item.json.row_number
  • post_id = {{$json.post_id}} ⚠️ (see note below)
  • url = $('create_post').item.json.link
  • status = done

⚠️ Important: post_id mapping likely wrong

After the custom HTTP request, $json probably does not contain post_id.

✅ Safer mapping:

  • post_id = {{$('create_post').item.json.id}}

That guarantees you store the WP post ID.


4.15 Send a message (Slack)

Node: Send a message

Posts to #general:

  • Title: $('create_post').item.json.title.raw
  • Date: $('create_post').item.json.date
  • Link: $('create_post').item.json.link

Optional: you can remove this node if you don’t need notifications.


5) Data Contracts (Input/Output)

Example input row (from Sheet)

{
  "number": 1,
  "title": "Example Post",
  "summary": "Short summary...",
  "image_prompt": "Laptop + cloud icon...",
  "status": "ready",
  "post_id": "",
  "row_number": 12
}

Key final outputs

  • WordPress Post ID: $('create_post').item.json.id
  • WordPress URL: $('create_post').item.json.link
  • Sheet updated:

post_id: WP ID – url: WP link – status: done


6) Operational Checklist

  • At least one sheet row has status=ready
  • The row has empty post_id
  • Gemini image generation works
  • WP media upload endpoint works (Basic Auth)
  • WP create page works
  • Custom change-post-type endpoint works
  • Sheet update writes back post_id, url, and sets status=done
  • Slack notification works (optional)

7) Recommended Improvements (Optional)

1) Prevent double-processing

  • Immediately set status = processing after selecting the row (before calling AI)
  • On success → done
  • On failure → error

2) Create a Post directly

  • If your WordPress node supports creating posts, use that instead of creating a page + custom conversion endpoint.

3) Ensure featured media ID type

  • Cast media_id to a number before sending to WP.

4) Add error branches

  • For media upload failures, GPT failures, WP failures → update sheet status=error and log the error.
Automated WordPress Publishing from Google Sheets (AI + Thumbnail)
Automated WordPress Publishing from Google Sheets (AI + Thumbnail)