Module 10 · ai · intermediate
Lesson 10 · AI Content Repurposer
Goal
Use an LLM as a reliable function: text in, strictly structured JSON out. You'll build the engine that turns each module's blog post into short-video scripts and social posts, the content side of "learn → demo → sell".
Prerequisites
- Module 06 (OpenAI credential), Module 02 (HTTP Request)
Concepts
- Switch: like IF, but with any number of named outputs, plus a fallback for "none matched".
- HTML → Markdown: fetched pages are mostly markup. Converting (and ignoring
script, style, nav, footer…) gives the model clean text and saves tokens. - Basic LLM Chain vs AI Agent: a chain is a single prompt → answer, with no tools and no loop. Use it whenever the task is fixed.
- Structured Output Parser: give a JSON example; n8n tells the model the schema and parses the result into
$json.output. Downstream nodes get real objects and arrays, not a string to parse. - Prompt as spec: audience, voice rules, banned words, hard limits (words, characters, counts), and "use only facts from the article". The test checks these limits automatically.
Build it
Step 1: Input
- Webhook
Repurpose API: POST, pathrepurpose, respond via node. - Switch
Input Type(Rules mode), with renamed outputs:url:{{ String($json.body.url ?? '') }}matches regex^https?://markdown:{{ String($json.body.markdown ?? '').trim().length }}≥200- Options → Fallback Output: Extra Output, renamed
invalid→ Respond to Webhook 400.
Step 2: Get clean text
- url → HTTP Request
Fetch Article(GET{{ $json.body.url }}, Response Format Text, timeout 15s) → MarkdownHTML to Markdown(HTML{{ $json.data }}, destinationmarkdown, Options → Ignore:script,style,nav,footer,header,aside,form,svg) → Edit FieldsSource from URL:source_text={{ $json.markdown.slice(0, 15000) }},source= the URL. - markdown → Edit Fields
Source from Markdown:source_text={{ $json.body.markdown.slice(0, 15000) }},source=pasted markdown.
Step 3: Write
Basic LLM Chain Write Content, prompt Define below:
- Audience and CTA from the request (with defaults)
- Voice: practitioner sharing what they built; concrete; no hype words
- Hooks under 12 words; only facts from the article
- Exactly 3 shorts with different angles; 80–120-word scripts; LinkedIn 120–200 words; X thread 4–6 posts under 270 characters; newsletter subject under 60 characters
ARTICLE:followed by{{ $json.source_text }}
Turn on Require Specific Output Format → connect Structured Output Parser Content Format (Generate From JSON Example) with an example containing shorts[] (angle, hook, script, on_screen, caption), linkedin_post, x_thread[], newsletter {subject, blurb}. Connect OpenAI Chat Model (gpt-5-mini).
Step 4: Respond
Respond to Webhook: {{ { source: $("Source from URL").isExecuted ? $("Source from URL").item.json.source : "pasted markdown", generated_at: $now.toISO(), ...$json.output } }}
Test it
./test/test.sh # input validation
WITH_CREDS=1 ./test/test.sh # repurpose Module 01's blog + a URL, checking counts and length limits
Use it for the course:
jq -n --rawfile md modules/02-api-digest/sell/blog.md '{markdown: $md}' \
| curl -s -X POST http://localhost:5678/webhook/repurpose -H 'Content-Type: application/json' -d @- | jq .
Common problems
- "Could not parse LLM output": the model returned extra text. Keep the JSON example small and valid. Exercise 3 adds automatic fixing.
- Hooks too long / thread posts over 280 chars: limits in the prompt aren't guarantees. The test catches it; tighten the wording or validate and retry (Exercise 3).
- URL returns garbage: some sites need JavaScript or block bots. Paste the markdown instead.
- Hallucinated stats: keep "use only facts from the article" and review before posting.