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
textfield. No image parameter is wired in this version, so an X post is text-only regardless ofimage_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_socialscope.
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_IDandTHREADS_ACCESS_TOKENfrom n8n’s instance Variables (Settings → Variables), sourced from the Meta Developer Portal. - Bluesky: same pattern, different variables:
BSKY_HANDLEandBSKY_APP_PASSWORD, from an app password you generate at bsky.app → Settings → App passwords. - LinkedIn: variables again,
LINKEDIN_ACCESS_TOKENandLINKEDIN_AUTHOR_URN(e.g.urn:li:person:xxxx), from a LinkedIn Developer app with thew_member_socialscope. 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.