The constraint

Running a weekly content programme without dedicated publishing headcount means every manual step between writing and publishing compounds across the year. Formatting a post for a headless CMS, converting markdown to Portable Text, updating a status flag, scheduling three LinkedIn posts with the correct timing — none of it is hard on its own. In aggregate, it is the kind of work that does not get done at 9pm when the post is late.

The pipeline described below handles these steps automatically. A post written in markdown lands in Sanity, gets converted to the correct Portable Text structure, and has its Buffer schedule queued before the file is committed.

The problem

Sanity's content model does not accept markdown. It uses Portable Text — a block-based structure where headings, paragraphs, bullet lists, and inline marks (bold, italic, links) are all typed objects with keys. Writing a post in markdown and pasting it into Sanity Studio manually means either using the Studio's rich-text editor (slow, context-switching) or writing a conversion script every time the schema changes.

Buffer's scheduling API expects a specific GraphQL mutation with the post text, channel ID, and a UTC timestamp for each scheduled post. Getting the NZ timezone offset right at the boundary between NZST and NZDT is the kind of edge case that quietly breaks things twice a year.

The GitHub commit that flips a post from status: draft to status: published needs to happen after Sanity has accepted the content, not before or in parallel — otherwise the status and the live content can drift.

The solution structure

Three n8n workflows handle the pipeline:

wf-content-orchestrator runs on a Thursday cron. It reads the content calendar (content/calendar.json), finds the next upcoming entry with status: approved, and dispatches to the two sub-workflows below. It supports a posts[] array in the calendar entry for weeks with more than one post, processing each in sequence.

wf-publish-blog-sanity (7 nodes) receives a blog post as raw markdown. It:
- Parses the YAML frontmatter (title, slug, publish date, excerpt, pillar)
- Converts the markdown body to Portable Text, handling headings, bullet lists, numbered lists, inline marks, and links
- Maps the pillar to the Sanity category field
- Builds and submits the Sanity mutation
- Flips the frontmatter status: approvedstatus: published and commits that change back to GitHub

wf-schedule-social-buffer (8 nodes) schedules three LinkedIn posts per publication: a Tuesday post on the Pattern company page, a Wednesday amplification post, and a Thursday engagement post from the author's profile. It calculates the UTC timestamps for 9am NZT dynamically, accounting for the NZST/NZDT offset, and submits each to Buffer's GraphQL API using the stored credential.

Where it breaks

The Sanity insight schema accepts block and image types only — no table block type. Posts that include markdown tables get converted to bulleted lists with field: value pairs. This is lossy. We have accepted the limitation and structure posts to avoid tables where possible. The tradeoff against writing posts directly in Sanity Studio is worth it.

The quality gate is occasionally conservative. Before wf-publish-blog-sanity runs, the orchestrator passes the draft through a quality judge (a separate Claude call with a rubric). The judge has held posts for issues that, on review, were not actually problems — a heading structure it read as awkward, a paragraph it flagged as lacking specificity. We have kept the gate because it catches things we would have missed, but it requires occasional manual override.

Schema drift. If a Sanity custom field is renamed or a Customer.io attribute changes, the pipeline continues running without error and silently drops the field. We catch this eventually when a post is missing a category or an attribute is not syncing — not in real time. A nightly schema-validation workflow is on the build list.

Practical implications

The pipeline described above is not complex to build — all three workflows together are around 55 nodes. What it does is move the recurring friction out of the publish moment and into a one-time setup cost. Once running, a new post requires writing the markdown, adding a status: approved flag, and pushing to the repo. Everything else is handled.

We have more to learn about where this approach hits its limits, particularly as the content programme scales beyond a weekly cadence. Once we have clearer data on what breaks at higher volume, we'll share what we find.

The pipeline blueprint below includes the n8n workflow JSONs, the blog system prompt, and the quality judge prompt, along with deployment notes on credentials and what the schema-validation gap looks like in practice.

If you are working through a similar content pipeline — particularly the Sanity integration or the Buffer scheduling — we are happy to talk through what we would do differently if we were starting it now.