Case studies
CASE STUDY — Marketing

Content publishing pipeline — telling the channels you can automate from the ones you can't

Deployed at Media / Content5 days to build
NotionPostgresChannel publish APIsDiscord

One article got posted again to the blog, again to social, again to the newsletter. Reformatting per channel, tidying links, and separately recording how each performed — every time.

Approve once, publish everywhere

The moment a page flips to approved in Notion, the workflow starts. Content is transformed and published per channel, the team channel is told, and results are collected in one place.

content-pipeline.workflowLIVE
NTNotion approvalPLRoute channelsPBPublishDCNotify team

First, the uncomfortable part — not every channel can be automated

The most important thing in this case study comes first. Some platforms provide no public API that lets an outside service post on your behalf. No workflow design changes that. The honest answer to "can it publish to Naver Blog?" — the first question Korean readers ask — is that automated publishing is not available.

  • Channels you can automate — your own blog (WordPress, Ghost, Webflow), newsletter services, and social platforms with public posting APIs. The workflow carries these all the way through.
  • Channels you can't — platforms with no public write API. This workflow doesn't even attempt them; it posts a "publish this one manually" note to your team channel instead.
  • Encoding that distinction in code is the point. Draw an unautomatable channel as if it were automatable and posts that fail to publish quietly disappear. Silent omission is the worst possible outcome.
So the channel list at the top of Plan Publish Targets marks each channel automated: true | false. That's the configuration point for this workflow. Keep only the channels you use, and check whether each one genuinely offers a public publishing API. Leaving something as `true` without checking is the most dangerous thing you can do here.

The approval gate already exists — it's the Notion status

Resistance to publishing AI drafts unreviewed is real and reasonable — *"I can't fully trust work a machine churned out"* is a common reaction from people who run the channel. This pipeline doesn't route around it.

  • The workflow only fetches pages marked approved. Drafts and in-review pages never publish, however long they sit.
  • **Changing that status in Notion *is* the approval gate.** No separate approval system is needed; the human judgement happens in the tool people already use.
  • Publishing stops being a task and becomes a state change. Marketers focus on making and judging; only the moving is automated.
  • For tighter control, split it in two: post the pending publish list to the team channel first, then publish after confirmation.

The Notion database schema

Your content database needs these properties. Rename them freely — just update the mapping in the Fetch Approved Posts node to match.

title              Title (text)
status             Status (select) — draft | in review | approved | published
channels           Channels (multi-select) — wordpress, newsletter, x, naver_blog ...
publish_at         Scheduled date (date, optional)
og_image           Cover image (file, optional)
body               Body (page content)
Making channels a multi-select is the key decision. Where a piece goes varies per piece, so don't pin channels to the workflow — let each post carry its own destinations. Then adding a channel never means editing the workflow.

Preventing duplicate publishing

In a workflow polling every ten minutes, the biggest risk is posting the same piece twice. The fix is recording publications per (post, channel).

CREATE TABLE IF NOT EXISTS content_publications (
  page_id      text        NOT NULL,
  channel      text        NOT NULL,
  published_at timestamptz NOT NULL DEFAULT now(),
  status       text        NOT NULL DEFAULT 'published',
  PRIMARY KEY (page_id, channel)
);

Per (post, channel), not per post — that's what matters. When the blog succeeds and the newsletter fails, the next run retries only the newsletter. Recording at post level would republish to the blog on every retry.

What you need

  • n8n — self-hosted or Cloud.
  • One Postgres database — a single publication-record table.
  • Access to Notion (or whatever content tool you use) — enough to query approved pages. It needn't be Notion: any tool that can return a list of approved posts via API works, changing only the first node.
  • API credentials for each channel you publish tobut first confirm each one actually offers a public publishing API.
  • A team webhook URL — Discord or Slack.

Which metrics, collected when

"Collect the results" needs to be specific too. This pipeline records the fact of publication; performance metrics are gathered on a separate cadence.

  • Recorded at publish timepage_id, channel, published_at, and the published URL. The workflow does this automatically.
  • Collected later — views, reactions, and referrals per channel. All zero immediately after publishing, so there's no point reading them then. A separate workflow that collects once at seven days is the practical arrangement.
  • Referral tracking — put UTM parameters on published URLs. Without them you cannot later tell which channel produced the traffic. Channel name as utm_source and the content slug as utm_content is the standard shape.
  • What to compare — referrals per publication by channel drives decisions better than raw view counts. The channel with the biggest audience is not necessarily the one that converts.
⬇︎ Download the workflow (content-pipeline.json)
One Postgres credential and the publication table gets channel routing, duplicate prevention, and manual-channel handoff running. Fill in the content query node and your per-channel publish nodes.
Being precise about what was verified. The channel-routing logic was executed in Node.js across 6 scenarios and passed all of them — a post mixing automatable and non-automatable channels splitting correctly into publish jobs and handoffs; already-published channels skipped so only the remainder retries (the partial-failure re-run case); a fully published post generating no work at all; posts with no channels, an unknown channel, and a missing page_id/title each surfaced as problems; and an empty array returned — sending no notification — when there is nothing to do. Node types and versions match our already-published workflows. Not verified: live Notion integration, live channel publishing, live Discord posting. And whether any given platform offers a public API changes over time, so the `CHANNELS` list in the workflow is a starting reference only — confirm each channel against its current developer documentation before you build. Last verified: 2026-08-15.

Setup (30 minutes)

  1. Check the channels first — read each target platform's developer documentation to confirm whether external publishing is possible. This is step one of the whole case.
  2. Create the table — run the content_publications SQL above.
  3. Tidy the Notion database — create the properties above, with channels as multi-select.
  4. Register the Postgres credential — n8n → Credentials → PostgresTest.
  5. Import the workflow — n8n → Workflows → ...Import from File.
  6. Fill in the content query node — configure Fetch Approved Posts against your content database, filtered to approved, mapping the response to page_id, title, body, channels[], published_channels[].
  7. Edit the CHANNELS list — in Plan Publish Targets, keep only the channels you use and set each automated value to match what you confirmed in step one.
  8. Fill in the publish nodes — one per automated channel; use a Switch node to fan out if there are several.
  9. Set the team webhook — put your Discord or Slack URL into Notify Team.
  10. Test the manual path first — approve a post whose only channel is a non-automatable one. Nothing should publish; only the "publish manually" notice should appear.
  11. Test for duplicates — leave an already-published post in place and run the workflow again. Nothing should happen. If it posts twice, your publication-record mapping is wrong.
  12. Activate — it checks for approvals every ten minutes.

What happens in edge cases

  • Some channels fail — only the successes are recorded, so the next run retries only the failures. Nothing rolls back: re-sending to a channel that missed out is safer than trying to retract a post that already went live.
  • A post including a non-automatable channel — the automatable ones publish and that channel joins the "publish manually" list. Nothing is silently dropped.
  • A post with no channels selected — approved but with channels empty gets reported under "needs attention." Approving and forgetting to pick channels happens constantly.
  • A channel name that isn't in the list — a typo or a new channel. No publish is attempted; it's reported as an unknown channel.
  • A post with no image — this workflow doesn't require one. If a channel mandates a cover image, add that check to Plan Publish Targets.
  • Channel rate limits — approving several posts at once fires several requests in quick succession. A 429 means that publication isn't recorded, so it retries next run. If approvals cluster at your organisation, add a delay to the publish nodes.
  • Republishing an edited post — the publication record prevents it going out again. To republish deliberately, delete that (page_id, channel) row.
One approval
publishes everywhere
Zero
duplicate posts
Explicit
manual-channel handoff
30 min
setup time
One button and it's done. All the per-channel copy-pasting just vanished.Content manager

Your operation belongs
in here, too.

Tell us the most repetitive task you have. We'll map an automation scenario for it.