SWIRLS_
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 optional call_to_action. The root node trims and normalizes each field before passing them downstream.
  • The draft AI node generates a LinkedIn post under 1300 characters. Temperature is set to 0.8 for varied, engaging output.
  • The review_draft node pauses execution. The draft appears in your Inbox for approval or feedback.
  • The route switch reads the review result. approved: true goes to publish. Any other response routes to revise.
  • The revise AI node rewrites the post using the reviewer's feedback, then publish_revised sends it to the LinkedIn API.

Run it

swirls env set LINKEDIN_TOKEN=your-token
swirls worker start
swirls graph execute draft_linkedin_post

Customize

  • Swap anthropic/claude-3.5-sonnet for gpt-4o or another provider.
  • Add a stream node before draft to inject past high-performing posts as few-shot examples.
  • Remove review_draft and the route switch for fully automated posting.
  • Add a schedule trigger to generate and publish recurring content without manual input.

On this page