How to Automate Content Briefs with n8n
The Problem
Writing a good content brief means opening the top ten results for a keyword, copying their headings, estimating how long each article is, and noting which subtopics everyone covers. Done by hand it takes close to an hour per keyword, so briefs become the bottleneck and writers start without direction. This guide shows how to build an n8n workflow that watches a keyword sheet, pulls SERP data from an API, parses the top results for headings, word counts, and shared topics, and writes a structured brief into an output sheet the moment a new keyword is added.
What You Will Need
- A self-hosted n8n instance (Community Edition is free) or an n8n Cloud trial
- A Google Sheet with a Keywords tab (columns keyword, intent, status) and a Briefs tab, plus Google Sheets OAuth2 credentials
- An account with a SERP API provider that returns organic results as JSON, with the API key stored as an n8n credential
- About 60 minutes of setup time
Step-by-Step Workflow Setup
1 Trigger on a new keyword row
The Google Sheets Trigger node in n8n polls a spreadsheet and starts the workflow whenever a row is added. Each new keyword becomes one workflow execution, so the brief appears without anyone clicking a button.
Add a Google Sheets Trigger node to a new n8n workflow and configure:
Node: Google Sheets Trigger
Event: Row Added
Document: By ID (your spreadsheet ID)
Sheet: Keywords
Poll Times: Every Minute
Options > Columns to Watch: keyword
Polling every minute is fine for a small sheet. Every poll counts toward Google API quota, so use Every 15 Minutes for busy documents in n8n.
{
"row_number": 14,
"keyword": "n8n error handling",
"intent": "informational",
"status": ""
}
2 Request SERP results
The HTTP Request node calls your SERP API. The parameter names differ by provider, but most accept a query, a location, and a result count. The example below uses a generic structure you can adapt.
Add an HTTP Request node to the n8n canvas and configure:
Node: HTTP Request
Method: GET
URL: https://api.your-serp-provider.com/search
Authentication: Generic Credential Type > Header Auth
Send Query Parameters: On
q: {{ $json.keyword }}
num: 10
gl: us
hl: en
Response Format: JSON
Store the key in an n8n Header Auth credential rather than typing it into the node, so it does not leak when you export the n8n workflow JSON.
{
"organic_results": [
{ "position": 1, "title": "Error Handling in n8n", "link": "https://example.com/error-handling", "snippet": "Use the Error Trigger node..." },
{ "position": 2, "title": "Retry On Fail Guide", "link": "https://example.org/retry", "snippet": "Configure retries..." }
]
}
3 Fetch each result page
Snippets are too thin for analysis, so the n8n workflow needs the full pages. First add a Split Out node to turn the results array into one item per result, then another HTTP Request node to download each page.
Node: Split Out
Fields To Split Out: organic_results
Include: No Other Fields
Node: HTTP Request (page fetch)
Method: GET
URL: {{ $json.link }}
Response Format: Text
Put Output in Field: html
Options > Batching > Items per Batch: 2
Options > Batching > Batch Interval: 1500
Settings > On Error: Continue
Some pages will block the request or time out. On Error: Continue lets n8n carry on with whatever pages it did retrieve, and the Code node in the next step skips empty HTML.
4 Extract headings and word counts
An n8n Code node reads each page, pulls out H2 and H3 headings, and estimates the visible word count. Using regular expressions avoids installing extra packages on your n8n server.
Add a Code node and configure:
Node: Code
Mode: Run Once for Each Item
Language: JavaScript
const html = $json.html || '';
const body = html.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ');
const headings = [];
const re = /<h([23])[^>]*>([\s\S]*?)<\/h\1>/gi;
let m;
while ((m = re.exec(body)) === null ? false : true) {
const t = m[2].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
if (t) headings.push({ level: Number(m[1]), text: t });
}
const words = body.replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length;
return { json: { url: $json.link, title: $json.title, headings, wordCount: words } };
The backreference \1 in the regular expression makes sure an opening h2 only pairs with a closing h2.
{
"url": "https://example.com/error-handling",
"title": "Error Handling in n8n",
"headings": [
{ "level": 2, "text": "Using the Error Trigger node" },
{ "level": 2, "text": "Retry On Fail settings" }
],
"wordCount": 1820
}
5 Find common topics and gaps
Now an aggregate pass in n8n. Another Code node in Run Once for All Items mode counts how many competing pages share each heading keyword. Topics that appear on at least three pages are must cover subjects. Topics that appear on only one page are gap opportunities.
Node: Code
Mode: Run Once for All Items
const stop = new Set(['the','a','and','to','of','in','for','how','with','is','your','what']);
const counts = {};
const pages = $input.all();
for (const p of pages) {
const seen = new Set();
for (const h of p.json.headings) {
for (const w of h.text.toLowerCase().split(/[^a-z0-9]+/)) {
if (w.length > 3 && stop.has(w) === false) seen.add(w);
}
}
seen.forEach(w => { counts[w] = (counts[w] || 0) + 1; });
}
const common = Object.keys(counts).filter(w => counts[w] >= 3).sort((a, b) => counts[b] - counts[a]).slice(0, 12);
const gaps = Object.keys(counts).filter(w => counts[w] === 1).slice(0, 8);
const wc = pages.map(p => p.json.wordCount).filter(n => n > 200).sort((a, b) => a - b);
const median = wc[Math.floor(wc.length / 2)] || 0;
return [{ json: { common, gaps, median, urls: pages.map(p => p.json.url), headings: pages.flatMap(p => p.json.headings.map(h => h.text)).slice(0, 20) } }];
In n8n, counting a word once per page, using the seen set, prevents one long article from dominating the tally.
6 Structure the brief with Set
The Set node (called Edit Fields in recent n8n versions) shapes the final brief fields. Reference the keyword from the trigger by node name, because the data on the main path now comes from the Code node.
Add a Set node and configure:
Node: Set (Edit Fields)
Mode: Manual Mapping
targetKeyword: {{ $('Google Sheets Trigger').item.json.keyword }}
suggestedHeadings: {{ $json.headings.join(' | ') }}
mustCoverTopics: {{ $json.common.join(', ') }}
gapTopics: {{ $json.gaps.join(', ') }}
wordCountRange: {{ Math.round($json.median * 0.9) }} to {{ Math.round($json.median * 1.2) }}
competitorUrls: {{ $json.urls.join('\n') }}
createdAt: {{ $now.toISO() }}
{
"targetKeyword": "n8n error handling",
"mustCoverTopics": "error, trigger, retry, workflow",
"gapTopics": "alerts, sentry",
"wordCountRange": "1638 to 2184",
"createdAt": "2026-11-04T09:30:00.000-08:00"
}
7 Write the brief to the output sheet
The final n8n Google Sheets node appends the brief. A follow up node can then set the original keyword row status to done.
Node: Google Sheets
Resource: Sheet Within Document
Operation: Append Row
Sheet: Briefs
Mapping Column Mode: Map Automatically
Because the Set node field names match the sheet headers, Map Automatically fills every column. Writers can open the Briefs tab fed by n8n and start drafting straight away.
Troubleshooting
The n8n trigger fires but the workflow receives an empty keyword
The Columns to Watch option compares header names exactly. If the header has a trailing space or different case, n8n reads nothing. Retype the header in the sheet and reselect the column in the node.
Every n8n word count comes back as zero or a tiny number
The target page is probably rendered with JavaScript, so the HTTP response holds only an empty shell. The median calculation already ignores values under 200 words, but if most results are shells, switch to a SERP provider that returns page content.
The n8n brief shows the same topics for unrelated keywords
The Code node in step 5 likely reads items from a cached previous run. Confirm it uses $input.all() and not a hard coded node reference, then clear pinned data on the n8n nodes.
Free n8n Workflow Resources
Get started with the free workflow pack on the LightCube Labs templates page.
Production-Ready Workflow Packs
Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.
View the Starter ($29) and Pro ($99) PacksFrequently Asked Questions
Which SERP source works best with this n8n workflow?
Any provider that returns organic results as JSON will work, because the HTTP Request node is generic. Compare the pricing per search, since each keyword consumes one SERP call plus ten page fetches.
How does n8n calculate the suggested word count range?
In n8n, the Code node takes the median word count of the top results, ignoring pages under 200 words. The Set node then sets the range from 90 percent to 120 percent of that median, which gives writers a realistic target without copying any one competitor.
Does the n8n brief copy competitor content?
No. The n8n workflow records heading text and topic counts only, as research input. Writers should produce original text and use the gap topics to cover something the ranking pages miss.
Can the n8n workflow add an intent or audience field?
Yes. Pass the intent column from the trigger into the n8n Set node, or add an LLM API call through another HTTP Request node to draft an angle statement from the common and gap topics.