Spurlock Studios
Contact
Share LinkedIn X
A scuffed work smartphone with a blank glowing circular button. Thesis: AUTOMATE CONTENT PUBLISHING WORKFLOW AI.

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", Ghost status: "draft", Webflow staged POST /items — never /items/live from the generator path.
  • Approve the child, not the idea. humanStatus === "approved" on this contentId + version. Source-ready is not publish-ready.
  • Timeout is hold, not ship. n8n Wait Limit Wait Time that continues into a publish node is a bug.
  • Do not score volume. Unused drafts and editor minutes tell the truth. “Posts generated” does not.
StageWhoAllowed write
Brief / outlineHuman or AIInternal only
Draft bodyAICMS draft / git branch / Docs
EditHumanSame draft id
ApproveNamed humanStatus flag on that version
Go liven8nPublic CMS / merge / schedule
Notifyn8nSlack, 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.

JobInputOutput of the AI stepPublic write
Publishing (this post)Calendar slot + brief + factsOne CMS/git draftOne URL goes live
Repurposing (the cousin)Approved source assetMany surface draftsMany 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 liveUrl back 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
DecisionPublishing railDo not do this
Brief is thinBlock generate; send it backFill holes with “color”
Editor is outHold; escalate the SLAAuto-publish on Wait timeout
CMS has a live-create endpointBan it on the AI credentialOne token that can draft and go live
Slot movedUpdate publishWindow; do not republishFire 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:

  1. idea — slot exists, no draft
  2. brief_ready — facts, CTA, embargo, owner
  3. drafting — model running
  4. needs_review — CMS draft or PR filed; vendor id stored
  5. approved — named human, this version
  6. scheduled or live
  7. superseded — facts changed; old approval is dead

Procedure that actually ships:

  1. Human (or an intake form) marks the calendar row brief_ready with contentId, owner, publishWindow, cta, forbiddenClaims[].
  2. n8n Webhook or status-watch starts. Payload must include those fields. A Dropbox file event is not a brief.
  3. HTTP Request calls the model with the brief object, not a folder of PDFs. Require structured output: title, slug, body, excerpt, unresolved[].
  4. If unresolved[] is non-empty, file needs_review with [[FACT CHECK]] markers. Do not publish.
  5. Create draft only in the CMS or open a git PR. Persist cmsDraftId / prNumber before any retry.
  6. Notify the owner. Pause on Wait ($execution.resumeUrl) or Slack Send and Wait for Response.
  7. On approve, re-read the child. Confirm version match. Then PATCH status / publish endpoint / merge.
  8. Store liveUrl. Close the calendar slot. Attach an error workflow so a failed publish is loud.
TriggerUseSkip
Status brief_readyYes—
Cron “every morning post something”NoInvents fake urgency
Model finished a completionNoCompletions are not editorial states
Wait timeoutHold + page the backupContinue into publish
publishingEnabled=falseDrafts may fileAll 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.

TaskAIn8nHuman
Outline / title optionsYesNoPicks or edits
Body draft from the briefYesFiles itEdits in CMS/PR
Alt text, excerpt, slug candidatesYesWrites fieldsConfirms slug
Invent a statisticNeverNeverSupply it in the brief or cut it
Create CMS draftNoYes—
Publish / schedule / mergeNoYes, after flagFlips the flag
Kill switchNoReads publishingEnabledSets 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 classAI mayPublish policy
Low — internal changelogRewrite freelyEdit-then-approve still required for v1
Medium — marketing blogRearrange, shortenApprove this version
High — pricing, testimonials, regulatedQuote-only or refuseHuman-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_draft token: create/update draft only
  • cms_publish token: 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:

  1. Trigger — Webhook (header auth) or Airtable/status watch on brief_ready.
  2. Load brief — GET the calendar row. Fail if owner, publishWindow, or cta is empty.
  3. Draft — HTTP to your model endpoint. JSON.parse the response. On throw, error workflow, stop.
  4. Validate — slug format, forbidden-claim scan, unresolved[] empty or explicitly allowed.
  5. Idempotency — if cmsDraftId already exists for contentId@version, PATCH the draft; do not POST a second post.
  6. File draft — CMS create-as-draft or git branch + PR.
  7. Persist ids — write cmsDraftId, prUrl, draftedAt to the calendar row before notify.
  8. Notify + Wait — Slack Send and Wait, or Wait on webhook/form with auth.
  9. IF approved — else mark rejected / needs_revision and stop.
  10. Re-GET the child — confirm body/version still match what was approved.
  11. Go live — publish endpoint / status: publish / merge PR.
  12. Post-live — store liveUrl; optional IndexNow; Slack “it’s up.”
  13. Error workflow — every publish node. Silent 201-then-drop is how you double-post.
Node classReads approval?Side effect
Trigger → draft → fileNoInternal / draft
Wait / Send and WaitCollects itNone
IF humanStatus=approved AND version matchYesGate
HTTP publish / mergeMustPublic
IndexNow / Slack liveAfter live URL existsNotice, 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. None plus a forwarded email is a forwarded publish.
  • Self-hosted: if WEBHOOK_URL still points at localhost, 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 Time auto-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 fieldWhy it existsFake substitute
contentIdIdempotency parentTitle string
versionStale-approval killer“latest”
slotAt / publishWindowWhen go-live is allowed“sometime Tuesday”
owner + backupWho gets the WaitA Slack channel
brief / keyPoints[]Model inputA Drive folder
cmsDraftIdRetry keySearch by title
humanStatusGateEmoji reaction
publishingEnabledKill switchUnplugging n8n
liveUrlDone 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
  • publishingEnabled defaults 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:

PatternUse whenFailure
Slot-driven (slotAt entered)Real editorial calendarNone if window is checked
“Post at 9am if anything is approved”Backup drain of a ready queueShips a stale approval
“Post at 9am no matter what”NeverHallucinated 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.

CMSDraft callLive callTrap
WordPressPOST /wp/v2/posts status: draftstatus: publish or futureApp password with publish cap
GhostPOST /admin/posts/ status: draftPUT status: published + updated_atCreate samples use published
WebflowPOST /v2/.../itemsPOST .../items/publish/items/live on create
Sanity_id: drafts.{id}sanity.action.document.publishMutate to published id in step 1
ContentfulCreate entryPUT .../published + version headerStale X-Contentful-Version
GitBranch + PRMergeBot 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.

GateBlocksTimeout
brief_readyDraft generateNo run
Draft JSON validCMS createError workflow
Child needs_reviewGo liveWait; page backup
Child approved + version matchNothing — may publishn/a
publishingEnabled=falseAll public writesDrafts may still file
publishWindow in the futureGo liveHold until window
embargoUntil in the futureGo liveHold

Two pause patterns, both valid:

PatternHowUse when
Slack/Gmail Send and Wait for Responsen8n pauses until Approve / Deny / form (Slack node)Editor lives in Slack
Wait On Webhook Call / On Form SubmittedEditor clicks $execution.resumeUrl with authYou need a CMS-side button or JWT
  • Approver is owner or backup, not “anyone with the link”
  • Resume webhook has auth
  • Approval payload includes contentId and version (echo them in the message)
  • Deny / request-changes does not fall through to publish
  • Timeout output pages the backup and leaves status needs_review
  • publishingEnabled is 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

EventCorrect writeWrong write
Editor clicks Run twiceSecond run PATCHes existing cmsDraftIdTwo WordPress drafts, two slugs
Brief facts changeBump version; old child supersededSilent overwrite of an approved draft
Publish HTTP 201 then timeoutReuse stored liveUrl; GET before POSTSecond status: publish on a new id
Ghost PUT 409 collisionGET, merge, new approval if body changedRetry with stale updated_at until it “works”
Contentful version mismatchGET sys.version, stop if unpublished edits existPUT with the create-time version
PR already mergedSkip merge; write liveUrl from default branchOpen PR #2 for the same slug
Wait resumed twiceSecond resume no-ops if already liveTwin IndexNow + twin Slack

Procedure on every publish node:

  1. Lock the calendar row (publishing state) or use a compare-and-set on humanStatus.
  2. GET the CMS child. If already live for this version, persist liveUrl and exit 0.
  3. If the child body hash ≠ approved hash, refuse. Do not “publish anyway.”
  4. Call the live endpoint once. Persist vendor response id.
  5. 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.

StepWhenDo not
Write liveUrl + wentLiveAtImmediately on successTrust a 201 without a GET
Slack “live” to ownerAfter URL storedBefore URL stored
Close calendar slotAfter URL storedClose on draft-create
IndexNow POSTOptional, after URL storedTreat 200 as “Google has it”
Sitemap ping / rebuildIf your stack needs itSkip XML sitemaps because you POSTed IndexNow
Kick the repurposing railOnly if you want derivativesBundle 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.

  • liveUrl HEAD-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.

FailureWhat you seeWhat you do
Create-live endpoint on the AI pathDraft step publishesSplit credentials; delete /items/live from that workflow
Wait timeout continues2am mystery postTimeout → hold + backup; never publish
Stale approvalv1 approved, v2 body liveApprove by contentId@version only; re-GET
Ghost without updated_atPUT rejected or clobberGET then PUT; if body drifted, re-review
Contentful VersionMismatchPublish 409GET version; if editor wrote, new approval
WEBHOOK_URL=localhostButtons no-opSet public webhook base before production
Unauthenticated resume URLIntern publishes last week’s embargoHeader/JWT auth; IP allowlist if you can
Retry On Fail on publishTwin posts / twin slugsRetry GET; publish once
Empty queue + morning cronAI-invented “thought leadership”No brief, no run
Model wraps JSON in fencesValidate throwsStrip once, then fail closed
Link rotCTA 404 after a site shipHEAD-check hrefs before go-live
One token for draft + liveLeak = defacementSplit Application Passwords / tokens
No named ownerWait 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.

MetricWhat it tells youLie it replaces
Time from brief_ready to draft filedGenerate + CMS health“The workflow ran”
Time from draft filed to approvedEditorial SLA, not AI“AI is slow”
Percent of filed drafts that go liveDemand vs overproduction“We created 40 posts”
Median editor minutes per postVoice card / brief qualityUnmeasured “time saved”
Unused drafts after 14 daysYou generated past capacityActivity
Incidents (wrong URL, bad claim, double live)Real costSilence
Timeout-to-publish countMust 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 one contentId@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.

SituationDIYHire / pair
WordPress blog, you are the editorYes — draft + Wait + publishOptional review of the gate
Git markdown, PRs already requiredYes — bot opens PR, you mergeIf you want n8n to watch merges
Three CMSs + newsletter + social in one graphNoSplit rails; production patterns
Testimonials / pricing in the bodyHuman-writeAutomation files only
Agency-built Zap “auto posts at 9”Pause itRebuild with ownership (runbooks)
No backup ownerDo not go liveName two humans first
You want an Agent with cms:writeDon’tDumb 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):

  1. Brief → draft → you approve → live URL stored.
  2. Click Run again. Second execution PATCHes, does not twin.
  3. Flip publishingEnabled=false. Approve. Confirm it does not go live.
  4. Let Wait time out. Confirm it does not go live.
  5. 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 weekSkip this week
Calendar row with contentId, owner, windowFancy scoring dashboards
Model → JSON → status: draft/items/live, status: published on create
Store cmsDraftIdRetry On Fail on publish
Slack Send and Wait or Wait + authUnauthenticated “approve” links
Publish IF approved + version + windowMorning cron that invents posts
Error workflow on the live nodeIndexNow, Google Indexing API, Buffer
Kill switchSecond CMS
Staging proof of the five tests aboveProduction on the first try

Procedure for a seven-day slice:

  1. Day 1 — pick one destination (WordPress or Ghost or git PR). Write the state list on the calendar.
  2. Day 2 — HTTP draft + validate + create-as-draft. No publish node in the workflow yet.
  3. Day 3 — persist ids, notify, Wait. Practice deny and timeout.
  4. Day 4 — add the publish node behind IF. Split credentials if you can.
  5. Day 5 — idempotency + re-GET. Run the twin-click test.
  6. Day 6 — kill switch + window check. Staging only.
  7. 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)
SymptomWait on automationDo this instead
Inconsistent voiceVoice card + editorMore model calls
Missed slotsA real calendar + SLA9am cron
Fear of the CMS APIManual publish, AI in Docs—
Agency Zap already postingPause it; map statesNew Zap on top
“We need 30 posts a week”Capacity math with editorsA 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.

FAQ

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.
Sources

Last reviewed

More from this lane

Automation

All →
Book the audit