Cookbook
Publishing a LinkedIn Post
Draft, review, and publish a LinkedIn post with an LLM.
This workflow takes a topic and audience, drafts a LinkedIn post with an LLM, pauses for human review, then publishes via the LinkedIn API.
The .swirls file
form linkedin_post {
label: "LinkedIn Post"
enabled: true
schema: @json {
{
"type": "object",
"required": ["topic", "audience"],
"properties": {
"topic": { "type": "string", "title": "Topic" },
"audience": { "type": "string", "title": "Target Audience" },
"call_to_action": { "type": "string", "title": "Call to Action" }
},
"additionalProperties": false
}
}
}
graph draft_linkedin_post {
label: "Draft LinkedIn Post"
description: "Drafts a post with an LLM, pauses for review, then publishes via API"
root {
type: code
label: "Normalize input"
inputSchema: @json {
{
"type": "object",
"required": ["topic", "audience"],
"properties": {
"topic": { "type": "string" },
"audience": { "type": "string" },
"call_to_action": { "type": "string" }
},
"additionalProperties": false
}
}
outputSchema: @json {
{
"type": "object",
"required": ["topic", "audience", "call_to_action"],
"properties": {
"topic": { "type": "string" },
"audience": { "type": "string" },
"call_to_action": { "type": "string" }
},
"additionalProperties": false
}
}
code: @ts {
const { topic, audience, call_to_action } = context.nodes.root.input
return {
topic: topic.trim(),
audience: audience.trim(),
call_to_action: (call_to_action ?? "").trim(),
}
}
}
node draft {
type: ai
label: "Draft post"
kind: object
model: "anthropic/claude-3.5-sonnet"
prompt: @ts {
const { topic, audience, call_to_action } = context.nodes.root.output
return `Write a LinkedIn post about "${topic}" for ${audience}.
Tone: professional and engaging.
${call_to_action ? `Call to action: ${call_to_action}` : ""}
Keep it under 1300 characters.`
}
temperature: 0.8
schema: @json {
{
"type": "object",
"required": ["post"],
"properties": {
"post": { "type": "string" }
}
}
}
}
node review_draft {
type: code
label: "Review draft"
outputSchema: @json {
{ "type": "object", "required": ["post"], "properties": { "post": { "type": "string" } }, "additionalProperties": false }
}
code: @ts {
return context.nodes.draft.output
}
review: {
enabled: true
title: "Review LinkedIn post"
description: "Approve to publish, or request changes."
schema: @json {
{
"type": "object",
"required": ["approved"],
"properties": {
"approved": { "type": "boolean", "title": "Approve and publish" },
"feedback": { "type": "string", "title": "Requested changes" }
},
"additionalProperties": false
}
}
}
}
node route {
type: switch
label: "Approved?"
cases: ["publish", "revise"]
router: @ts {
return context.reviews.review_draft?.approved ? "publish" : "revise"
}
}
node publish {
type: http
label: "Publish to LinkedIn"
method: "POST"
url: @ts {
return "https://api.linkedin.com/v2/ugcPosts"
}
headers: {
"Authorization": @ts {
return `Bearer ${context.secrets.LINKEDIN_TOKEN}`
}
"Content-Type": "application/json"
}
body: @ts {
return JSON.stringify({
author: "urn:li:person:YOUR_PERSON_ID",
lifecycleState: "PUBLISHED",
specificContent: {
"com.linkedin.ugc.ShareContent": {
shareCommentary: { text: context.nodes.draft.output.post },
shareMediaCategory: "NONE"
}
},
visibility: { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" }
})
}
}
node revise {
type: ai
label: "Revise draft"
kind: object
model: "anthropic/claude-3.5-sonnet"
prompt: @ts {
const post = context.nodes.draft.output.post
const feedback = context.reviews.review_draft?.feedback ?? ""
return `Revise this LinkedIn post based on feedback.
Original post: ${post}
Feedback: ${feedback}
Keep it under 1300 characters.`
}
schema: @json {
{
"type": "object",
"required": ["post"],
"properties": { "post": { "type": "string" } }
}
}
}
node publish_revised {
type: http
label: "Publish revised"
method: "POST"
url: @ts { return "https://api.linkedin.com/v2/ugcPosts" }
headers: {
"Authorization": @ts { return `Bearer ${context.secrets.LINKEDIN_TOKEN}` }
"Content-Type": "application/json"
}
body: @ts {
return JSON.stringify({
author: "urn:li:person:YOUR_PERSON_ID",
lifecycleState: "PUBLISHED",
specificContent: {
"com.linkedin.ugc.ShareContent": {
shareCommentary: { text: context.nodes.revise.output.post },
shareMediaCategory: "NONE"
}
},
visibility: { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" }
})
}
}
flow {
root -> draft
draft -> review_draft
review_draft -> route
route -["publish"]-> publish
route -["revise"]-> revise
revise -> publish_revised
}
}
trigger on_linkedin_post {
form:linkedin_post -> draft_linkedin_post
enabled: true
}How it works
- The form captures
topic,audience, and an optionalcall_to_action. The root node trims and normalizes each field before passing them downstream. - The
draftAI node generates a LinkedIn post under 1300 characters. Temperature is set to0.8for varied, engaging output. - The
review_draftnode pauses execution. The draft appears in your Inbox for approval or feedback. - The
routeswitch reads the review result.approved: truegoes topublish. Any other response routes torevise. - The
reviseAI node rewrites the post using the reviewer's feedback, thenpublish_revisedsends it to the LinkedIn API.
Run it
swirls env set LINKEDIN_TOKEN=your-token
swirls worker start
swirls graph execute draft_linkedin_postCustomize
- Swap
anthropic/claude-3.5-sonnetforgpt-4oor another provider. - Add a
streamnode beforedraftto inject past high-performing posts as few-shot examples. - Remove
review_draftand therouteswitch for fully automated posting. - Add a schedule trigger to generate and publish recurring content without manual input.