The Full Pipeline — How It Works
The workflow chains five nodes in sequence. Each node does one thing well and passes its output to the next. The trigger accepts a JSON brief via webhook. Claude handles all the creative and analytical work across three dedicated nodes. A final HTTP node pushes the finished, SEO-optimised post to your CMS.
Each Claude node uses a separate, focused system prompt — ideation, drafting, and SEO are deliberately kept apart so each prompt can be tuned independently. The outputs chain: the ideation node's structured outline becomes the drafting node's input; the draft becomes the SEO node's input. This sequential chaining is what produces consistent, high-quality output rather than asking one prompt to do everything at once.
publish_at timestamp. Send it to publish immediately or at a future date — your CMS handles the scheduling.Prerequisites & Stack Setup
Before building, get these in order. The whole pipeline takes under 45 minutes once these are ready — don't skip the setup.
| Tool | What you need | Cost |
|---|---|---|
| n8n | Self-hosted (Docker) or n8n Cloud. Version 1.40+ recommended for the HTTP node improvements. | Free (self-hosted) / $20+/mo cloud |
| Anthropic API Key | Starts with sk-ant-. Get from console.anthropic.com. Set a usage cap before starting. |
Pay-per-token |
| CMS with REST API | WordPress (WP REST API v2), Ghost (Admin API), or any CMS accepting JSON via HTTP POST. | Depends on CMS |
| n8n Claude credential | In n8n, go to Credentials → New → HTTP Header Auth. Set header x-api-key to your Anthropic key. |
— |
x-api-key. Value: your Anthropic key. Save.Webhook Trigger Node
The pipeline starts with an n8n Webhook node. It listens for an HTTP POST containing your content brief as JSON. This is what you send each time you want to kick off a new content run — manually, from a Slack command, a Notion button, a cron job, or any other trigger.
The webhook expects a JSON payload in this shape. All fields except publish_at are required.
{
"topic": "How to reduce churn in B2B SaaS",
"audience": "SaaS founders and heads of customer success",
"tone": "Direct, data-led, no fluff. Use second person.",
"content_type": "blog_post",
"word_count": 1400,
"keywords": ["churn reduction", "customer retention SaaS", "churn prevention"],
"publish_at": "2026-03-20T09:00:00Z" // optional — omit to publish immediately
}
Claude Ideation Node
The ideation node receives the brief and produces a structured content plan: three potential angles, a recommended angle with rationale, and a detailed section-by-section outline. This becomes the drafting node's structural input — critically, the draft never starts from a blank page.
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"system": "You are an expert content strategist. Your job is to produce a structured content plan given a brief. Respond ONLY with valid JSON matching the schema in the user message. No preamble, no markdown fences.",
"messages": [
{
"role": "user",
"content": "Brief:\n- Topic: {{ $json.body.topic }}\n- Audience: {{ $json.body.audience }}\n- Tone: {{ $json.body.tone }}\n- Content type: {{ $json.body.content_type }}\n- Target length: {{ $json.body.word_count }} words\n- Seed keywords: {{ $json.body.keywords }}\n\nReturn JSON:\n{\n \"angles\": [{\"angle\": \"...\", \"hook\": \"...\"}],\n \"recommended_angle\": \"...\",\n \"rationale\": \"...\",\n \"outline\": [\n {\"section\": \"...\", \"purpose\": \"...\", \"key_points\": [\"...\"]}\n ]\n}"
}
]
}
{{ $json.content[0].text }}. Add a Set node immediately after the HTTP Request to parse this: set a field ideation_plan = {{ JSON.parse($json.content[0].text) }}. This makes the structured plan accessible by name in all downstream nodes.// Set node configuration — add these fields: ideation_plan = {{ JSON.parse($node["Ideation"].json.content[0].text) }} topic = {{ $node["Webhook"].json.body.topic }} audience = {{ $node["Webhook"].json.body.audience }} tone = {{ $node["Webhook"].json.body.tone }} word_count = {{ $node["Webhook"].json.body.word_count }} content_type = {{ $node["Webhook"].json.body.content_type }} publish_at = {{ $node["Webhook"].json.body.publish_at }}
Claude Drafting Node
The drafting node takes the ideation plan — specifically the recommended angle and the full section outline — and writes the complete article. By injecting the structured outline into the system prompt, you ensure the draft always follows the planned structure. Claude writes section by section, hits the target word count, and maintains the specified tone throughout.
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"system": "You are an expert content writer. Write exactly to the provided outline and word count. Use the specified tone throughout. Return ONLY the article in markdown — no preamble, no meta-commentary, no word count note at the end.",
"messages": [
{
"role": "user",
"content": "Write a {{ $json.content_type }} on this topic: {{ $json.topic }}\n\nAudience: {{ $json.audience }}\nTone: {{ $json.tone }}\nTarget length: {{ $json.word_count }} words\n\nUse this angle: {{ $json.ideation_plan.recommended_angle }}\n\nFollow this outline exactly:\n{{ JSON.stringify($json.ideation_plan.outline, null, 2) }}\n\nWrite the full article now."
}
]
}
max_tokens to 6,000. The 4,096 default is suitable for posts up to ~1,600 words.After the drafting HTTP Request, add another Set node to extract and store the draft:
// Merge with previous fields, add: draft_markdown = {{ $node["Drafting"].json.content[0].text }} // Pass through all previous fields unchanged: topic = {{ $json.topic }} audience = {{ $json.audience }} tone = {{ $json.tone }} content_type = {{ $json.content_type }} publish_at = {{ $json.publish_at }} ideation_plan = {{ $json.ideation_plan }}
SEO & Meta Generation Node
The SEO node reads the finished draft and generates all the metadata needed for publication: an SEO-optimised title tag, meta description, focus keyword, secondary keyword list, and JSON-LD Article schema markup. Claude reads the actual content — not just the topic — so the SEO output reflects what's really in the draft.
{
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"system": "You are an expert SEO specialist. Generate SEO metadata for the article provided. Return ONLY valid JSON. No markdown fences. No preamble.",
"messages": [
{
"role": "user",
"content": "Article:\n{{ $json.draft_markdown }}\n\nSeed keywords to incorporate: {{ $json.ideation_plan.outline[0].key_points }}\n\nReturn JSON:\n{\n \"title_tag\": \"60 chars max, includes focus keyword\",\n \"meta_description\": \"155 chars max, compelling CTA\",\n \"focus_keyword\": \"primary keyword phrase\",\n \"secondary_keywords\": [\"kw1\", \"kw2\", \"kw3\", \"kw4\", \"kw5\"],\n \"slug\": \"url-friendly-slug\",\n \"json_ld_schema\": { \"@context\": \"https://schema.org\", \"@type\": \"Article\", \"headline\": \"...\", \"description\": \"...\" },\n \"og_title\": \"Social share title\",\n \"og_description\": \"Social share description under 200 chars\"\n}"
}
]
}
Add a final Set node after the SEO node to consolidate everything into the publish_payload object used by the publish node:
seo_data = {{ JSON.parse($node["SEO"].json.content[0].text) }} draft_markdown = {{ $json.draft_markdown }} publish_at = {{ $json.publish_at }} content_type = {{ $json.content_type }} // Convenience shortcuts used by the publish node: post_title = {{ JSON.parse($node["SEO"].json.content[0].text).title_tag }} post_slug = {{ JSON.parse($node["SEO"].json.content[0].text).slug }} post_excerpt = {{ JSON.parse($node["SEO"].json.content[0].text).meta_description }}
CMS Publish & Scheduling
The final node posts the completed, SEO-enriched article to your CMS. The examples below cover WordPress REST API v2 and Ghost Admin API — the two most common targets. The pattern is identical for any CMS with an HTTP endpoint: construct the post body, set the auth header, fire the POST.
{
"title": "{{ $json.post_title }}",
"content": "{{ $json.draft_markdown }}",
"excerpt": "{{ $json.post_excerpt }}",
"slug": "{{ $json.post_slug }}",
"status": "{{ $json.publish_at ? 'future' : 'publish' }}",
"date": "{{ $json.publish_at || new Date().toISOString() }}",
"meta": {
"_yoast_wpseo_title": "{{ $json.seo_data.title_tag }}",
"_yoast_wpseo_metadesc": "{{ $json.seo_data.meta_description }}",
"_yoast_wpseo_focuskw": "{{ $json.seo_data.focus_keyword }}"
},
"yoast_head_json": {{ JSON.stringify($json.seo_data.json_ld_schema) }}
}
{
"posts": [{
"title": "{{ $json.post_title }}",
"markdown": "{{ $json.draft_markdown }}",
"slug": "{{ $json.post_slug }}",
"custom_excerpt": "{{ $json.post_excerpt }}",
"status": "{{ $json.publish_at ? 'scheduled' : 'published' }}",
"published_at": "{{ $json.publish_at || new Date().toISOString() }}",
"og_title": "{{ $json.seo_data.og_title }}",
"og_description": "{{ $json.seo_data.og_description }}",
"meta_title": "{{ $json.seo_data.title_tag }}",
"meta_description": "{{ $json.seo_data.meta_description }}",
"codeinjection_head": "<script type='application/ld+json'>{{ JSON.stringify($json.seo_data.json_ld_schema) }}</script>"
}]
}
post_title, draft_markdown, and seo_data as the content fields. The upstream nodes stay identical.Full Workflow JSON & Customisations
The importable n8n workflow JSON below wires all eight nodes together with the correct data mappings. Import it via Workflows → Import from File in your n8n instance, then update three things before activating: your Claude credential, your CMS credential, and your CMS endpoint URL.
{
"name": "Content Pipeline — n8n + Claude",
"nodes": [
{ "id": "1", "name": "Webhook", "type": "n8n-nodes-base.webhook", "position": [240, 300] },
{ "id": "2", "name": "Ideation", "type": "n8n-nodes-base.httpRequest", "position": [460, 300] },
{ "id": "3", "name": "Set Ideation", "type": "n8n-nodes-base.set", "position": [680, 300] },
{ "id": "4", "name": "Drafting", "type": "n8n-nodes-base.httpRequest", "position": [900, 300] },
{ "id": "5", "name": "Set Draft", "type": "n8n-nodes-base.set", "position": [1120, 300] },
{ "id": "6", "name": "SEO", "type": "n8n-nodes-base.httpRequest", "position": [1340, 300] },
{ "id": "7", "name": "Set SEO", "type": "n8n-nodes-base.set", "position": [1560, 300] },
{ "id": "8", "name": "Publish to CMS", "type": "n8n-nodes-base.httpRequest", "position": [1780, 300] }
],
"connections": {
"Webhook": { "main": [[{ "node": "Ideation", "type": "main", "index": 0 }]] },
"Ideation": { "main": [[{ "node": "Set Ideation", "type": "main", "index": 0 }]] },
"Set Ideation": { "main": [[{ "node": "Drafting", "type": "main", "index": 0 }]] },
"Drafting": { "main": [[{ "node": "Set Draft", "type": "main", "index": 0 }]] },
"Set Draft": { "main": [[{ "node": "SEO", "type": "main", "index": 0 }]] },
"SEO": { "main": [[{ "node": "Set SEO", "type": "main", "index": 0 }]] },
"Set SEO": { "main": [[{ "node": "Publish to CMS", "type": "main", "index": 0 }]] }
},
"active": false
}
Forks & extensions worth building next
Once the core pipeline runs reliably, these are the highest-value extensions: