Cookbook
Write a Blog Post
Research, draft, and polish a blog post using LLM nodes.
A form captures a title, brief, and tone. An HTTP node fetches research. Two LLM nodes draft and then edit the post.
form blog_request {
label: "Blog Request"
enabled: true
schema: @json {
{
"type": "object",
"required": ["title", "brief"],
"properties": {
"title": { "type": "string", "title": "Title" },
"brief": { "type": "string", "title": "Brief or outline" },
"tone": { "type": "string", "title": "Tone", "enum": ["professional", "casual", "technical"] }
},
"additionalProperties": false
}
}
}
graph write_blog {
label: "Write Blog Post"
description: "Research, draft, and edit a blog post"
root {
type: code
label: "Normalize"
inputSchema: @json {
{
"type": "object",
"required": ["title", "brief"],
"properties": {
"title": { "type": "string" },
"brief": { "type": "string" },
"tone": { "type": "string" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["title", "brief", "tone"],
"properties": {
"title": { "type": "string" },
"brief": { "type": "string" },
"tone": { "type": "string" }
},
"additionalProperties": false
}
}
code: @ts {
const { title, brief, tone } = context.nodes.root.input
return {
title: title.trim(),
brief: brief.trim(),
tone: (tone ?? "professional").trim(),
}
}
}
node research {
type: http
label: "Fetch research"
method: "POST"
url: @ts {
return "https://api.tavily.com/search"
}
headers: {
"Content-Type": "application/json"
}
body: @ts {
return JSON.stringify({
api_key: context.secrets.TAVILY_API_KEY,
query: context.nodes.root.output.title,
max_results: 5
})
}
schema: @json {
{ "type": "object", "properties": { "results": { "type": "array" } } }
}
}
node draft {
type: ai
label: "Draft post"
kind: object
model: "anthropic/claude-3.5-sonnet"
prompt: @ts {
const { title, brief, tone } = context.nodes.root.output
const research = JSON.stringify(context.nodes.research?.output?.results ?? [])
return `Write a blog post.
Title: ${title}
Brief: ${brief}
Tone: ${tone}
Research context: ${research}
Structure with an introduction, subheadings, and conclusion.
Output in Markdown. Target 800-1200 words.`
}
temperature: 0.7
maxTokens: 4000
schema: @json {
{
"type": "object",
"required": ["content"],
"properties": { "content": { "type": "string" } }
}
}
}
node edit {
type: ai
label: "Edit and polish"
kind: object
model: "anthropic/claude-3.5-sonnet"
prompt: @ts {
const draft = context.nodes.draft.output.content
const tone = context.nodes.root.output.tone
return `Edit this blog post for clarity, flow, and grammar.
Tighten verbose sentences. Ensure the tone is ${tone}.
Draft:
${draft}`
}
temperature: 0.3
schema: @json {
{
"type": "object",
"required": ["content"],
"properties": { "content": { "type": "string" } }
}
}
}
flow {
root -> research
research -> draft
draft -> edit
}
}
trigger on_blog_request {
form:blog_request -> write_blog
enabled: true
}How it works
- The form collects a title, brief, and optional tone (defaults to
professional). - The root node trims whitespace and fills in the default tone.
- The
researchHTTP node queries Tavily and returns up to five results as context. - The
draftAI node writes an 800-1200 word Markdown post using the title, brief, tone, and research. - The
editAI node polishes the draft for clarity, flow, and grammar at a lower temperature. - The trigger fires the graph on every form submission.
Run it
swirls login
swirls secret set TAVILY_API_KEY <your-key>
swirls deployOpen the Portal, navigate to the form, and submit a request. Check the Executions view to inspect each node's output.
Customize
- Add a review step between
draftandeditto collect human feedback before the editing pass. - Swap Tavily for another search API by updating the
researchnode URL and body. - Add an HTTP node after
editto publish the finished post to your CMS. - Add persistence to build a library of generated posts.
- Add a schedule trigger for recurring content generation.