n8n Multi-Channel Social Publisher — cover
← downloads · 2026-04-20 · workflow

n8n Multi-Channel Social Publisher

One webhook → publishes the same post to X, Bluesky, Threads, and LinkedIn with per-channel formatting. The publish half of my content pipeline.

This is the publish side of my content pipeline. One webhook call fans a post out to whichever social channels you list in the request, formatting each one to that channel’s own rules before it goes out.

How it works

A POST to /webhook/content-publish triggers it. A Set node pulls slug, channels, posts, and image_url off the request body, then IF branches check whether channels contains x, threads, bluesky, or linkedin and skip whichever aren’t listed. Each enabled branch posts on its own:

  • X: goes through n8n’s built-in Twitter node with a single text field. No image parameter is wired in this version, so an X post is text-only regardless of image_url.
  • Threads: a Code node that calls the Graph API directly: creates a media container, polls it for readiness (up to 6 attempts), then publishes.
  • Bluesky: a Code node that logs in, uploads an image blob if one’s provided, and posts a full reply chain from posts.bluesky.thread, an array of strings rather than one block of text.
  • LinkedIn: a Code node that posts through the UGC Posts API using a personal access token and author URN. LinkedIn gates API posting access, so you need an approved app with the w_member_social scope.

A Merge node waits for all enabled branches, then Respond to Webhook returns { slug, results }, one result object per channel that ran.

The template wires four channels end-to-end: X, Bluesky, Threads, and LinkedIn. In my own running pipeline I post to LinkedIn by hand and keep Threads off, the setup the pipeline guide describes; the public template leaves all four wired so you can turn on whichever you have access to.

What you’ll need

  • X (Twitter): a real n8n credential, type OAuth2, created through the X Developer Portal with read and write access.
  • Threads: not an n8n credential at all. The Code node reads THREADS_USER_ID and THREADS_ACCESS_TOKEN from n8n’s instance Variables (Settings → Variables), sourced from the Meta Developer Portal.
  • Bluesky: same pattern, different variables: BSKY_HANDLE and BSKY_APP_PASSWORD, from an app password you generate at bsky.app → Settings → App passwords.
  • LinkedIn: variables again, LINKEDIN_ACCESS_TOKEN and LINKEDIN_AUTHOR_URN (e.g. urn:li:person:xxxx), from a LinkedIn Developer app with the w_member_social scope. LinkedIn gates posting access, so approval isn’t instant.

The one credential placeholder in the imported JSON is REPLACE_WITH_TWITTEROAUTH2API_CREDENTIAL_ID on the X node. Swap it for your own credential, then set the two variable pairs above before you activate the workflow.

Request shape

{
  "slug": "2026-05-22-eval-rubric",
  "channels": ["x", "threads", "bluesky", "linkedin"],
  "posts": {
    "x": { "text": "post text, 280 characters or under" },
    "threads": { "text": "post text" },
    "bluesky": { "thread": ["first post", "reply to the first post"] },
    "linkedin": { "text": "post text" }
  },
  "image_url": "https://example.com/cover.png"
}

image_url is shared across channels rather than per-post: Threads attaches it to the post, Bluesky attaches it to the first post in the thread. Leave a channel out of channels[] and its branch skips cleanly, no error.

How it fits

This is the publish half of the content pipeline behind this site; podcast intake is the companion workflow that fills the queue it empties.