How do I automate my content publishing workflow with AI
AI drafts the copy. n8n files a CMS draft and ships live only after a named human approves that version. Publish stays a gated write, not a model call.
William Spurlock Founder — Spurlock Studios 25 MIN
You automate a content publishing workflow with AI by splitting the job: the model drafts, n8n files a CMS draft and waits, and a named human approves that record + version before any node is allowed to write publish, published, or live. The public write is a gated API call. It is not a chat completion.
This spoke sits under the Production n8n handbook. It is the go-live rail. The cousin post, content repurposing pipelines, is one approved source into many surface drafts. Do not mash those jobs into one workflow. A publishing rail that also “makes LinkedIn variants” will ship the wrong object on retry.
The short answer
- AI drafts. n8n ships. The model never holds the CMS token that can go live.
- Draft first, always. WordPress
status: "draft", Ghoststatus: "draft", Webflow stagedPOST /items— never/items/livefrom the generator path. - Approve the child, not the idea.
humanStatus === "approved"on thiscontentId+version. Source-ready is not publish-ready. - Timeout is hold, not ship. n8n Wait
Limit Wait Timethat continues into a publish node is a bug. - Do not score volume. Unused drafts and editor minutes tell the truth. “Posts generated” does not.
| Stage | Who | Allowed write |
|---|---|---|
| Brief / outline | Human or AI | Internal only |
| Draft body | AI | CMS draft / git branch / Docs |
| Edit | Human | Same draft id |
| Approve | Named human | Status flag on that version |
| Go live | n8n | Public CMS / merge / schedule |
| Notify | n8n | Slack, IndexNow, sitemap ping |
If you cannot name the draft id, the version, and the human who flipped it, you do not have a publishing workflow. You have a prompt with an API key.
Publishing is not repurposing
A publishing workflow takes one piece from “ready to write” to “it is live on the site, the calendar slot is closed, and the URL is logged.” Repurposing takes one already-approved parent and mills newsletter, social, and talk-track drafts. Both need a human on public writes. They do not share a trigger.
| Job | Input | Output of the AI step | Public write |
|---|---|---|---|
| Publishing (this post) | Calendar slot + brief + facts | One CMS/git draft | One URL goes live |
| Repurposing (the cousin) | Approved source asset | Many surface drafts | Many channels, each gated |
In scope here
- Filing a draft in the CMS or opening a git PR
- Waiting on a named approver
- Publishing or scheduling that same record
- Writing
liveUrlback to the calendar row - Optional: IndexNow / sitemap ping after a confirmed live URL
Out of scope here
- Turning a webinar into five LinkedIn posts (repurposing)
- Auto-posting to LinkedIn or X from the same execution as CMS publish
- Letting the model invent numbers, testimonials, or prices
- Treating “the AI said it was done” as a publish event
| Decision | Publishing rail | Do not do this |
|---|---|---|
| Brief is thin | Block generate; send it back | Fill holes with “color” |
| Editor is out | Hold; escalate the SLA | Auto-publish on Wait timeout |
| CMS has a live-create endpoint | Ban it on the AI credential | One token that can draft and go live |
| Slot moved | Update publishWindow; do not republish | Fire go-live because the old cron fired |
One rail, one blast radius. If you need derivatives, run the repurposing workflow after this URL is live and the parent is marked approved — not inside the same execution.
How do I automate my content publishing workflow with AI?
You automate it as a state machine, not as “ChatGPT posts to WordPress.” The calendar row is the parent. The CMS draft (or git branch) is the child. AI may write the child. Only n8n may change the child’s public status, and only after the flag.
Minimum states:
idea— slot exists, no draftbrief_ready— facts, CTA, embargo, ownerdrafting— model runningneeds_review— CMS draft or PR filed; vendor id storedapproved— named human, this versionscheduledorlivesuperseded— facts changed; old approval is dead
Procedure that actually ships:
- Human (or an intake form) marks the calendar row
brief_readywithcontentId,owner,publishWindow,cta,forbiddenClaims[]. - n8n Webhook or status-watch starts. Payload must include those fields. A Dropbox file event is not a brief.
- HTTP Request calls the model with the brief object, not a folder of PDFs. Require structured output:
title,slug,body,excerpt,unresolved[]. - If
unresolved[]is non-empty, fileneeds_reviewwith[[FACT CHECK]]markers. Do not publish. - Create draft only in the CMS or open a git PR. Persist
cmsDraftId/prNumberbefore any retry. - Notify the owner. Pause on Wait (
$execution.resumeUrl) or Slack Send and Wait for Response. - On approve, re-read the child. Confirm version match. Then PATCH status / publish endpoint / merge.
- Store
liveUrl. Close the calendar slot. Attach an error workflow so a failed publish is loud.
| Trigger | Use | Skip |
|---|---|---|
Status brief_ready | Yes | — |
| Cron “every morning post something” | No | Invents fake urgency |
| Model finished a completion | No | Completions are not editorial states |
| Wait timeout | Hold + page the backup | Continue into publish |
publishingEnabled=false | Drafts may file | All public writes |
The model is a typist with a schema. n8n is the shipping clerk. The editor is still the publisher.
What should AI draft, and what should n8n ship?
Give the model the jobs that are reversible. Give n8n the jobs that hit the public internet. Split the credentials so a leaked prompt cannot go live.
| Task | AI | n8n | Human |
|---|---|---|---|
| Outline / title options | Yes | No | Picks or edits |
| Body draft from the brief | Yes | Files it | Edits in CMS/PR |
| Alt text, excerpt, slug candidates | Yes | Writes fields | Confirms slug |
| Invent a statistic | Never | Never | Supply it in the brief or cut it |
| Create CMS draft | No | Yes | — |
| Publish / schedule / merge | No | Yes, after flag | Flips the flag |
| Kill switch | No | Reads publishingEnabled | Sets it |
Hard rules in the draft template:
- No claims absent from the brief’s
keyPoints/quotes - If unsure, emit
[[FACT CHECK]]plus the missing slot - Slug is a suggestion; collision check is n8n’s job (GET by slug before create)
- Banned brand adjectives live in the voice card, not in a Slack nag
| Risk class | AI may | Publish policy |
|---|---|---|
| Low — internal changelog | Rewrite freely | Edit-then-approve still required for v1 |
| Medium — marketing blog | Rearrange, shorten | Approve this version |
| High — pricing, testimonials, regulated | Quote-only or refuse | Human-write; automation files only |
n8n’s human-in-the-loop for AI tool calls can pause an Agent before a dangerous tool runs. Use it if you insist on an Agent. Prefer a dumb HTTP draft step plus a Wait. Agents that also hold cms:write live scopes will eventually click themselves live.
Credential split:
-
cms_drafttoken: create/update draft only -
cms_publishtoken: used only on nodes behind the approval IF - Model key: no CMS auth in that credential
- Git: open PR with a bot; merge uses a different path or a human click
If one Application Password can both draft and publish, the IF node is the only lock. Locks fail. Split the keys.
How do I implement this in n8n?
Build one workflow with a hard cut between “file draft” and “go live.” Native CMS nodes are fine when they exist. The HTTP Request node is the one you can pin to a documented path when a vendor ships a breaking change.
Recommended skeleton:
- Trigger — Webhook (header auth) or Airtable/status watch on
brief_ready. - Load brief — GET the calendar row. Fail if
owner,publishWindow, orctais empty. - Draft — HTTP to your model endpoint.
JSON.parsethe response. On throw, error workflow, stop. - Validate — slug format, forbidden-claim scan,
unresolved[]empty or explicitly allowed. - Idempotency — if
cmsDraftIdalready exists forcontentId@version, PATCH the draft; do not POST a second post. - File draft — CMS create-as-draft or git branch + PR.
- Persist ids — write
cmsDraftId,prUrl,draftedAtto the calendar row before notify. - Notify + Wait — Slack Send and Wait, or Wait on webhook/form with auth.
- IF approved — else mark
rejected/needs_revisionand stop. - Re-GET the child — confirm body/version still match what was approved.
- Go live — publish endpoint /
status: publish/ merge PR. - Post-live — store
liveUrl; optional IndexNow; Slack “it’s up.” - Error workflow — every publish node. Silent 201-then-drop is how you double-post.
| Node class | Reads approval? | Side effect |
|---|---|---|
| Trigger → draft → file | No | Internal / draft |
| Wait / Send and Wait | Collects it | None |
IF humanStatus=approved AND version match | Yes | Gate |
| HTTP publish / merge | Must | Public |
| IndexNow / Slack live | After live URL exists | Notice, not truth |
Wait details that bite:
- Resume URL is
$execution.resumeUrl(webhook) or$execution.resumeFormUrl(form). It is unique per execution (Wait node). - Set authentication on the resume webhook: Basic, Header, or JWT.
Noneplus a forwarded email is a forwarded publish. - Self-hosted: if
WEBHOOK_URLstill points atlocalhost, approval buttons do nothing off-box. Fix the base URL before you trust the rail. - Waits longer than 65 seconds offload execution data to the database. That is normal. Time-based resume uses server time, not the workflow timezone setting.
Limit Wait Timeauto-resumes. Route the timeout output to hold + escalate, never to the publish node.
Do not start this from a chat Agent that “decides it is time.” Start from editorial state.
Where does the editorial calendar sit?
The calendar is the system of record for when and whether. The CMS is the system of record for the body. If those disagree, the calendar wins on scheduling, the CMS wins on what readers see — and your n8n graph must copy liveUrl back so they reconverge.
Airtable is a fine board until the 5 requests/second per base ceiling starts shaping the product. Create rows with create records. Batch writes. A 429 wants a 30-second wait. Do not open one HTTP node per field in a tight loop.
| Calendar field | Why it exists | Fake substitute |
|---|---|---|
contentId | Idempotency parent | Title string |
version | Stale-approval killer | “latest” |
slotAt / publishWindow | When go-live is allowed | “sometime Tuesday” |
owner + backup | Who gets the Wait | A Slack channel |
brief / keyPoints[] | Model input | A Drive folder |
cmsDraftId | Retry key | Search by title |
humanStatus | Gate | Emoji reaction |
publishingEnabled | Kill switch | Unplugging n8n |
liveUrl | Done means a URL | “workflow green” |
- Slot has a window, not only a date
- Owner can approve this week; backup is named (runbooks)
- Embargo / legal review flag when needed
- One primary CTA href that you HEAD-check before go-live
-
forbiddenClaims[]seeded for priced or testimonial posts -
publishingEnableddefaults true and is tested false in staging
A calendar without a kill switch will publish through a PR crisis. A kill switch that only lives in someone’s head is not a switch.
Cron vs slot:
| Pattern | Use when | Failure |
|---|---|---|
Slot-driven (slotAt entered) | Real editorial calendar | None if window is checked |
| “Post at 9am if anything is approved” | Backup drain of a ready queue | Ships a stale approval |
| “Post at 9am no matter what” | Never | Hallucinated cadence |
n8n may wait until slotAt with Wait → At Specified Time after approval. Do not wait until 9am and then ask the model to invent a post because the queue was empty.
Which CMS APIs draft vs go live?
Every serious CMS separates “exists” from “readers can see it.” Your AI path may call the first. Your publish path may call the second, and only after the flag. Pass status explicitly. Defaults move.
WordPress
POST /wp/v2/posts accepts status: publish, future, draft, pending, private. Core wp_insert_post defaults post_status to draft. Still send "status": "draft" on create. Auth with Application Passwords over HTTPS (Authorization: Basic username + app password). The same password can publish if the user can publish_posts — that is why you split users or wrap publish in a second workflow.
To go live after approval: POST /wp/v2/posts/{id} with "status": "publish". To schedule: "status": "future" plus a future date. Store the numeric id from create.
Ghost
Create is POST /admin/posts/ with required title. The docs show samples with "status": "published" — that is a live post on create. Your rail sends "status": "draft". Publish is PUT /admin/posts/{id}/ with "status": "published" and required updated_at for collision detection (updating a post). Schedule is status: "scheduled" plus future published_at. Always GET before PUT so updated_at is current. A stale updated_at is a failed publish, not a license to retry-until-overwrite.
Webflow
Staged create: POST https://api.webflow.com/v2/collections/{collection_id}/items (manage items). That is a draft. Live create: POST /v2/collections/{collection_id}/items/live “immediately published to the live site.” Ban /items/live on the AI credential. After approval: POST /v2/collections/{collection_id}/items/publish with itemIds (publish item), scope cms:write. Site-wide publish is a different call with a tighter rate limit — do not use it to ship one blog row.
Sanity
Drafts are documents whose _id starts with drafts.. Create the draft only. Publish with the Actions API sanity.action.document.publish (publishedId + draft/version id) via POST /data/actions/{dataset} (action reference). Writing the published id with createOrReplace and deleting the draft is the older mutate pattern. Either way: do not create the unprefixed id from the model step.
Contentful
Create an entry (draft). Publish is a separate PUT to /spaces/{space_id}/environments/{environment_id}/entries/{entry_id}/published with X-Contentful-Version set to the current sys.version. Contentful optimistic locking rejects a stale version. GET immediately before publish. Do not cache version 3 from draft-create and publish it ten minutes later after an editor saved version 5.
Git-based (markdown in repo)
The PR is the draft. Merge (plus your existing deploy) is the publish. n8n may create a pull request (POST /repos/{owner}/{repo}/pulls) from a bot branch. A human reviews and merges. Do not let the same bot push main from the draft step.
| CMS | Draft call | Live call | Trap |
|---|---|---|---|
| WordPress | POST /wp/v2/posts status: draft | status: publish or future | App password with publish cap |
| Ghost | POST /admin/posts/ status: draft | PUT status: published + updated_at | Create samples use published |
| Webflow | POST /v2/.../items | POST .../items/publish | /items/live on create |
| Sanity | _id: drafts.{id} | sanity.action.document.publish | Mutate to published id in step 1 |
| Contentful | Create entry | PUT .../published + version header | Stale X-Contentful-Version |
| Git | Branch + PR | Merge | Bot push to default branch |
If the vendor added a “create and publish” convenience flag, treat it as /items/live. It does not belong on the generator credential.
How do you keep a human on every publish?
Every go-live node reads one flag: humanStatus === "approved" on this contentId + version. Not “the brief was approved.” Not “an editor liked the outline in Slack.” Brief approval means the model may run. It does not mean the WordPress child may flip to publish.
| Gate | Blocks | Timeout |
|---|---|---|
brief_ready | Draft generate | No run |
| Draft JSON valid | CMS create | Error workflow |
Child needs_review | Go live | Wait; page backup |
Child approved + version match | Nothing — may publish | n/a |
publishingEnabled=false | All public writes | Drafts may still file |
publishWindow in the future | Go live | Hold until window |
embargoUntil in the future | Go live | Hold |
Two pause patterns, both valid:
| Pattern | How | Use when |
|---|---|---|
| Slack/Gmail Send and Wait for Response | n8n pauses until Approve / Deny / form (Slack node) | Editor lives in Slack |
| Wait On Webhook Call / On Form Submitted | Editor clicks $execution.resumeUrl with auth | You need a CMS-side button or JWT |
- Approver is
ownerorbackup, not “anyone with the link” - Resume webhook has auth
- Approval payload includes
contentIdandversion(echo them in the message) - Deny / request-changes does not fall through to publish
- Timeout output pages the backup and leaves status
needs_review -
publishingEnabledis checked again after the Wait, not only at start
Editors will edit the CMS draft during the wait. That is expected. Re-GET before go-live. If updatedAt moved and nobody re-approved that revision, send it back to needs_review. Ghost’s updated_at collision is trying to tell you this. Listen.
Do not auto-publish on timeout. A missed Thursday slot is recoverable. A model draft that went live at 2am because Wait expired is an incident.
How do you stop a draft from going live on retry?
Publishing rails fail like payments fail: double-clicks, webhook retries, and “I thought that was the new one.” The blast radius is a live URL, not a double charge. The fix is the same shape.
Idempotency key: contentId + version. Graph you must store:
contentId → version → cmsDraftId → humanStatus → liveUrl → lastPublishRequestId
| Event | Correct write | Wrong write |
|---|---|---|
| Editor clicks Run twice | Second run PATCHes existing cmsDraftId | Two WordPress drafts, two slugs |
| Brief facts change | Bump version; old child superseded | Silent overwrite of an approved draft |
| Publish HTTP 201 then timeout | Reuse stored liveUrl; GET before POST | Second status: publish on a new id |
| Ghost PUT 409 collision | GET, merge, new approval if body changed | Retry with stale updated_at until it “works” |
| Contentful version mismatch | GET sys.version, stop if unpublished edits exist | PUT with the create-time version |
| PR already merged | Skip merge; write liveUrl from default branch | Open PR #2 for the same slug |
| Wait resumed twice | Second resume no-ops if already live | Twin IndexNow + twin Slack |
Procedure on every publish node:
- Lock the calendar row (
publishingstate) or use a compare-and-set onhumanStatus. - GET the CMS child. If already live for this version, persist
liveUrland exit 0. - If the child body hash ≠ approved hash, refuse. Do not “publish anyway.”
- Call the live endpoint once. Persist vendor response id.
- Only then notify.
n8n Retry On Fail on a publish node is how you get twins. Retry GET/validate. Do not retry the live POST unless the first response was a documented retryable 429/5xx and a GET still shows draft. Prefer an error workflow + human.
Airtable 5 req/s still applies if the calendar lives there. Batch. Wait. The n8n rate-limit notes are Loop Over Items + Wait, not unbounded retry.
What should happen after go-live?
After a confirmed public URL exists, n8n may notify people and ping indexes. Those steps are followers. They are not how you know it shipped. liveUrl on the calendar row is how you know it shipped.
| Step | When | Do not |
|---|---|---|
Write liveUrl + wentLiveAt | Immediately on success | Trust a 201 without a GET |
| Slack “live” to owner | After URL stored | Before URL stored |
| Close calendar slot | After URL stored | Close on draft-create |
| IndexNow POST | Optional, after URL stored | Treat 200 as “Google has it” |
| Sitemap ping / rebuild | If your stack needs it | Skip XML sitemaps because you POSTed IndexNow |
| Kick the repurposing rail | Only if you want derivatives | Bundle social posts into this execution |
IndexNow submits URLs to participating engines (GET /indexnow?url=&key= or POST /indexnow with urlList, up to 10,000 URLs). 200 means the engine received the URL. 202 means received, key validation pending. 403 is a bad key file. Host {key}.txt at the site root. IndexNow is not a Google Search API.
Google’s Indexing API is a different product. Google documents it for pages with JobPosting or BroadcastEvent inside a VideoObject — not generic blog posts. Do not point your publishing rail at https://indexing.googleapis.com/v3/urlNotifications:publish for a marketing article. Use a sitemap and Search Console for Google discovery.
-
liveUrlHEAD-checks 200 before you Slack “it’s up” - IndexNow key file is on the same host as the URL
- Failures in IndexNow do not roll back the CMS publish (log them)
- Repurposing, if any, starts from the live parent id
If go-live succeeded and IndexNow 429’d, you still published. Queue the ping. Do not unpublish to “retry the whole graph.”
What breaks this in production?
Content pipes fail as a polite page on the homepage. Money pipes fail as a ticket. Budget for the quiet ones.
| Failure | What you see | What you do |
|---|---|---|
| Create-live endpoint on the AI path | Draft step publishes | Split credentials; delete /items/live from that workflow |
| Wait timeout continues | 2am mystery post | Timeout → hold + backup; never publish |
| Stale approval | v1 approved, v2 body live | Approve by contentId@version only; re-GET |
Ghost without updated_at | PUT rejected or clobber | GET then PUT; if body drifted, re-review |
Contentful VersionMismatch | Publish 409 | GET version; if editor wrote, new approval |
WEBHOOK_URL=localhost | Buttons no-op | Set public webhook base before production |
| Unauthenticated resume URL | Intern publishes last week’s embargo | Header/JWT auth; IP allowlist if you can |
| Retry On Fail on publish | Twin posts / twin slugs | Retry GET; publish once |
| Empty queue + morning cron | AI-invented “thought leadership” | No brief, no run |
| Model wraps JSON in fences | Validate throws | Strip once, then fail closed |
| Link rot | CTA 404 after a site ship | HEAD-check hrefs before go-live |
| One token for draft + live | Leak = defacement | Split Application Passwords / tokens |
| No named owner | Wait sits until someone “feels it” | Owner + backup on the calendar row |
Webhook intake still needs auth. A public “run publish” URL is how a curious teammate republishes an embargoed draft.
The failure we see most on first installs is not the model. It is status omitted or live-create convenience on the file-draft node. The complementary beehiiv lesson applies here: pass status explicitly even when the vendor currently defaults to draft. Defaults move.
Attach the error workflow to publish nodes. A failed Ghost PUT must page a human. A successful file-draft can notify quietly.
How do I measure whether it is working?
“Posts generated” goes up when the pipe is wrong. Measure whether editors still have a job, and whether incidents stay rare. I will not quote a fabricated weekly publish count. Your denominator is slots you intended to fill, not completions the model returned.
| Metric | What it tells you | Lie it replaces |
|---|---|---|
Time from brief_ready to draft filed | Generate + CMS health | “The workflow ran” |
Time from draft filed to approved | Editorial SLA, not AI | “AI is slow” |
| Percent of filed drafts that go live | Demand vs overproduction | “We created 40 posts” |
| Median editor minutes per post | Voice card / brief quality | Unmeasured “time saved” |
| Unused drafts after 14 days | You generated past capacity | Activity |
| Incidents (wrong URL, bad claim, double live) | Real cost | Silence |
| Timeout-to-publish count | Must stay zero | “It unblocked itself” |
If editor minutes stay high, fix the brief schema or the voice card. Do not add a second model call to “polish.” Polish without facts is how [[FACT CHECK]] disappears.
Weekly checklist:
- Which children went live, which died in
needs_review - Any incident or near-miss (timeout that almost published)
- Queue depth vs owner SLA — cut generation before you cut review
- Kill switch flipped in staging this month
- Idempotency: no two
liveUrls for onecontentId@version
Unused drafts are inventory. Inventory expires. Tune volume to the humans who still approve.
When to add the next CMS: when this one’s incident count is boring and unused-draft count is low. Not when a vendor demo looks fast.
When should I hire vs DIY this automation?
DIY if you have one CMS, one calendar, one owner who can pause it Monday, and you are willing to pass draft on every create. Hire (or book the audit) when the blast radius includes more than one live surface, when claims are regulated, or when nobody can name the publish credential.
| Situation | DIY | Hire / pair |
|---|---|---|
| WordPress blog, you are the editor | Yes — draft + Wait + publish | Optional review of the gate |
| Git markdown, PRs already required | Yes — bot opens PR, you merge | If you want n8n to watch merges |
| Three CMSs + newsletter + social in one graph | No | Split rails; production patterns |
| Testimonials / pricing in the body | Human-write | Automation files only |
| Agency-built Zap “auto posts at 9” | Pause it | Rebuild with ownership (runbooks) |
| No backup owner | Do not go live | Name two humans first |
You want an Agent with cms:write | Don’t | Dumb HTTP + Wait |
Spurlock Studios has shipped 500+ automations; the ones that survive are the ones with a named owner and a half-page runbook, not the ones with the cleverest prompt. A publishing rail is an irreversible write with a URL. Treat it like a payment rail that happens to ship HTML.
DIY week-one test (staging site or a private Ghost install):
- Brief → draft → you approve → live URL stored.
- Click Run again. Second execution PATCHes, does not twin.
- Flip
publishingEnabled=false. Approve. Confirm it does not go live. - Let Wait time out. Confirm it does not go live.
- Change the draft after approve, before publish. Confirm it refuses.
If those five fail, you do not have a workflow. You have a demo.
What should I skip if I only have a week?
Skip every follower. Ship one CMS draft path with a human gate. IndexNow, social, multi-locale, and Agents can wait.
| Do this week | Skip this week |
|---|---|
Calendar row with contentId, owner, window | Fancy scoring dashboards |
Model → JSON → status: draft | /items/live, status: published on create |
Store cmsDraftId | Retry On Fail on publish |
| Slack Send and Wait or Wait + auth | Unauthenticated “approve” links |
| Publish IF approved + version + window | Morning cron that invents posts |
| Error workflow on the live node | IndexNow, Google Indexing API, Buffer |
| Kill switch | Second CMS |
| Staging proof of the five tests above | Production on the first try |
Procedure for a seven-day slice:
- Day 1 — pick one destination (WordPress or Ghost or git PR). Write the state list on the calendar.
- Day 2 — HTTP draft + validate + create-as-draft. No publish node in the workflow yet.
- Day 3 — persist ids, notify, Wait. Practice deny and timeout.
- Day 4 — add the publish node behind IF. Split credentials if you can.
- Day 5 — idempotency + re-GET. Run the twin-click test.
- Day 6 — kill switch + window check. Staging only.
- Day 7 — one real post on a low-risk slot. Watch it. Do not batch ten.
If day 7 tempts you to “just add LinkedIn,” stop. That is the repurposing post, and it needs its own approval flag.
When is this not worth doing yet?
Skip the rail until a human already publishes on a calendar you can name. Automation copies a process. It will not invent one.
Not worth it yet:
- Nobody can point at the CMS draft UI and say where an editor actually edits
- There is no owner who can pause n8n this afternoon
- The “calendar” is a Slack thread
- Legal/pricing copy is still being decided in the same week as the slot
- You want the model to pick topics from thin air because the queue is empty
- You refuse to keep a human on go-live “because AI should just handle it”
- Credentials live in a personal Gmail (fix ownership first)
| Symptom | Wait on automation | Do this instead |
|---|---|---|
| Inconsistent voice | Voice card + editor | More model calls |
| Missed slots | A real calendar + SLA | 9am cron |
| Fear of the CMS API | Manual publish, AI in Docs | — |
| Agency Zap already posting | Pause it; map states | New Zap on top |
| “We need 30 posts a week” | Capacity math with editors | A generator with no reviewers |
Worth it when: slots exist, briefs can be written, one CMS has a draft state, and two humans will share the Wait. Then the rail deletes blank-page time and keeps the brand off the incident channel.
If you are still arguing whether a human should see the post, you are not ready to automate publish. You are ready to automate draft filing. That is a smaller workflow, and it is the right first ship.
FAQ
How do I automate my content publishing workflow with AI?
Split draft from go-live: the model writes from a structured brief, n8n files a CMS draft or git PR and waits, and a named human approves that contentId + version before any live call. Only then run publish / published / /items/publish / merge. The model does not hold the live credential.
How do I measure whether automating my content publishing workflow with AI is working?
Track time from brief_ready to draft filed, time to approval, percent of drafts that actually go live, median editor minutes, unused drafts after two weeks, and incidents (including timeout-to-publish, which must stay zero). Ignore “posts generated.” If unused inventory grows, cut generation, not review.
What usually fails first when teams try this?
The file-draft node goes live: omitted status, Ghost create with published, or Webflow /items/live. Second is Wait timeout continuing into publish. Third is stale approval after a regen. Fix credentials and gates before you add IndexNow or a second CMS.
How long does this take to show results?
A single-CMS staging rail with Wait and a kill switch is a week if the calendar already exists. You will feel it on the first post that files without a blank page, and on the first timeout that does not ship. Broader “content engine” claims wait until unused-draft count and incident count are boring. I will not invent a posts-per-week number for you.
What should I skip if I only have a week?
Skip IndexNow, social, Agents, and a second CMS. Do one destination, explicit draft status, stored vendor id, authenticated Wait, publish behind versioned approval, error workflow, kill switch, and the five staging tests (happy path, double run, kill switch, timeout, drifted body).
When is this not worth doing yet?
When there is no calendar, no named owner, no CMS draft UI, or you want the model to invent topics to fill empty slots. Automate draft filing first, or pause an existing auto-poster until ownership and a half-page runbook exist. A human on every publish is the product, not a phase-two extra.
CTA
AI types. n8n ships. A human still says live.
Read the handbook, then use automation or book the audit to install a publish rail that cannot go live without a name on the approval.
What questions does this article answer?
- How do I automate my content publishing workflow with AI?
- Split draft from go-live: the model writes from a structured brief, n8n files a CMS draft or git PR and waits, and a named human approves that `contentId` + `version` before any live call. Only then run `publish` / `published` / `/items/publish` / merge. The model does not hold the live credential.
- How do I measure whether automating my content publishing workflow with AI is working?
- Track time from `brief_ready` to draft filed, time to approval, percent of drafts that actually go live, median editor minutes, unused drafts after two weeks, and incidents (including timeout-to-publish, which must stay zero). Ignore “posts generated.” If unused inventory grows, cut generation, not review.
- What usually fails first when teams try this?
- The file-draft node goes live: omitted `status`, Ghost create with `published`, or Webflow `/items/live`. Second is Wait timeout continuing into publish. Third is stale approval after a regen. Fix credentials and gates before you add IndexNow or a second CMS.
- How long does this take to show results?
- A single-CMS staging rail with Wait and a kill switch is a week if the calendar already exists. You will feel it on the first post that files without a blank page, and on the first timeout that *does not* ship. Broader “content engine” claims wait until unused-draft count and incident count are boring. I will not invent a posts-per-week number for you.
- What should I skip if I only have a week?
- Skip IndexNow, social, Agents, and a second CMS. Do one destination, explicit draft status, stored vendor id, authenticated Wait, publish behind versioned approval, error workflow, kill switch, and the five staging tests (happy path, double run, kill switch, timeout, drifted body).
- When is this not worth doing yet?
- When there is no calendar, no named owner, no CMS draft UI, or you want the model to invent topics to fill empty slots. Automate draft filing first, or pause an existing auto-poster until ownership and a half-page runbook exist. A human on every publish is the product, not a phase-two extra.
- n8n.io
- docs.n8n.io
- docs.n8n.io
- docs.n8n.io
- docs.n8n.io
- docs.n8n.io
- docs.n8n.io
- airtable.com
- airtable.com
- developer.wordpress.org
- developer.wordpress.org
- developer.wordpress.org
- docs.ghost.org
- docs.ghost.org
- docs.ghost.org
- docs.ghost.org
- developers.webflow.com
- developers.webflow.com
- developers.webflow.com
- sanity.io
- sanity.io
- contentful.com
- docs.github.com
- docs.n8n.io
- indexnow.org
- developers.google.com
- api.webflow.com
- indexing.googleapis.com
Last reviewed
Automation
Automation After the show is not you at 1 a.m.
Post-show onboarding — thank-you, join path, merch nudge — belongs in a human-gated n8n rail, not your thumb at load-out.
Automation Paperwork that is not the plant
Invoice and PO matching, intake, and support triage in n8n with Metrc fences — the paperwork operators hate, not a menu widget.
Automation Saturday still books — the missed-call rail for trades
A missed-call text-back that routes zip and books a slot beats voicemail and Saturday desk coverage you cannot keep staffed. If a kid is cheaper, say so.
Automation Why is one giant n8n canvas a production liability
One giant n8n canvas is a production liability because debug, credentials, retries, and deploys share one blast radius. Split it with named contracts.
Will's Journal in your inbox.
What I learned this week building for shops, floors, and houses.
You're on the list.
Sign-up failed — try again.
By subscribing, you agree to the Privacy Policy.