n8n workflow

n8n workflow: post to X, Bluesky and LinkedIn at once

Point an RSS Feed Trigger at your feed and send each new item to one HTTP Request node that calls OpenTweet's POST /api/v1/posts with "platforms": ["x", "bluesky", "linkedin"]. One request publishes to all three networks and returns a result for each. Import the JSON below, add your API key, and activate.

No LinkedIn developer app, no Bluesky app password, no X developer account. You connect the three accounts once in OpenTweet, and the workflow only holds an OpenTweet API key.

Set it up in four steps

  1. 1

    Connect your accounts and create an API key

    In OpenTweet, connect X, Bluesky and LinkedIn. LinkedIn posts go to your personal profile. Then create an API key; it starts with ot_.

  2. 2

    Import the workflow

    Copy the JSON below and paste it onto the n8n canvas. You get an RSS Feed Trigger wired to an HTTP Request node.

  3. 3

    Set the feed and the key

    Put your feed URL in the RSS Feed Trigger. In the HTTP Request node, replace ot_your_api_key in the Authorization header, or move it into a Header Auth credential so it is not stored in the workflow.

  4. 4

    Test, then activate

    Run the workflow once and read the results array in the HTTP Request output. When it looks right, activate the workflow and it polls the feed every hour.

Why an HTTP Request node and not the OpenTweet community node

The n8n-nodes-opentweet community node on npm does not have a platforms field yet, as of September 2026. The built-in HTTP Request node calls the same API and can send platforms today.

The workflow JSON

Two nodes: n8n-nodes-base.rssFeedReadTrigger polling every hour, connected to n8n-nodes-base.httpRequest. Copy it, then paste it onto the canvas or save it as a file and import it.

rss-to-x-bluesky-linkedin.json
{
  "name": "RSS to X, Bluesky and LinkedIn",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            { "mode": "everyHour" }
          ]
        },
        "feedUrl": "https://example.com/feed.xml"
      },
      "id": "5b8f2c1e-3a47-4d6b-9e10-2f7c8a1d4b01",
      "name": "RSS Feed Trigger",
      "type": "n8n-nodes-base.rssFeedReadTrigger",
      "typeVersion": 1,
      "position": [0, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://opentweet.io/api/v1/posts",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            { "name": "Authorization", "value": "Bearer ot_your_api_key" }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: $json.title + ' ' + $json.link, platforms: ['x', 'bluesky', 'linkedin'], publish_now: true }) }}",
        "options": {}
      },
      "id": "9c3e7a2d-6f14-4b8a-a5c2-7d1e0b9f3c02",
      "name": "Post to X, Bluesky and LinkedIn",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [280, 0]
    }
  ],
  "connections": {
    "RSS Feed Trigger": {
      "main": [
        [
          { "node": "Post to X, Bluesky and LinkedIn", "type": "main", "index": 0 }
        ]
      ]
    }
  },
  "settings": { "executionOrder": "v1" }
}

What the HTTP Request node sends

Method POST, URL https://opentweet.io/api/v1/posts, header Authorization: Bearer ot_..., and a JSON body built by an expression:

n8n expression
{{ JSON.stringify({
  text: $json.title + ' ' + $json.link,
  platforms: ['x', 'bluesky', 'linkedin'],
  publish_now: true
}) }}

JSON.stringify builds the body so a quote or a line break in a feed title cannot break the JSON. The text is the item title, a space, and the item link. To schedule instead of posting straight away, replace publish_now with a scheduled_date, shown further down.

Every post in this workflow contains a link

Posts with links are on the Advanced and Agency plans, not Pro. On Pro, OpenTweet removes the URL from the text before publishing, on every network, so the post goes out without its link. A trial includes a small number of link posts, and URL post credits cover posts past the daily allowance. Compare plans.

Long titles and Bluesky

X counts any link as 23 characters. Bluesky counts it at full length when posted through the API and allows 300 graphemes. A long title plus a long URL can land on X and LinkedIn and be skipped on Bluesky, with the reason in its result. See the limits side by side.

Reading the per-network results

With publish_now, the response comes back after the post has been sent, and posts[0].results has one entry per network with a status of published, failed or skipped:

HTTP Request output
{
  "success": true,
  "count": 1,
  "posts": [
    {
      "id": "65f1a2b3c4d5e6f7a8b9c0d1",
      "status": "posted",
      "platforms": ["x", "bluesky", "linkedin"],
      "posted": true,
      "results": [
        {
          "platform": "x",
          "status": "published",
          "url": "https://x.com/you/status/1834000000000000000"
        },
        {
          "platform": "bluesky",
          "status": "skipped",
          "skip_reason": "This post is 312 characters, over the 300 Bluesky allows."
        },
        {
          "platform": "linkedin",
          "status": "published",
          "url": "https://www.linkedin.com/feed/update/urn:li:share:7300000000000000000/"
        }
      ]
    }
  ]
}
ResponseWhat it means
201, every result "published"The post is live on all three networks. Each result has a url.
201, a result "skipped" or "failed"The post went out where it fit. skip_reason or error on that entry says why the rest did not.
502, code publish_failedNothing was published. The top-level results array still has one entry per network with the reason.
409, error duplicate_contentThe same text was already posted or scheduled on your account, so it was not posted again.
402, trial_url_post_limit_reachedYour trial has used its link posts. The post was not created.

The HTTP Request node treats a non-2xx response as an error and stops the execution, so the 502, 409 and 402 cases show up as failed executions. A skip is a 201, so it does not. To catch skips, add an IF node after the HTTP Request node that checks whether this expression is not empty, and send its true branch to Slack or email:

n8n expression
{{ $json.posts[0].results
  .filter(r => r.status !== 'published')
  .map(r => r.platform + ': ' + (r.skip_reason || r.error))
  .join(', ') }}

Scheduling instead of posting now

Send an ISO 8601 scheduled_date in place of publish_now. The create response then confirms the post without results, because nothing has been sent yet. Read them after the scheduled time with GET /api/v1/posts/{id}, which returns the same results array.

n8n expression
{{ JSON.stringify({
  text: $json.title + ' ' + $json.link,
  platforms: ['x', 'bluesky', 'linkedin'],
  scheduled_date: '2026-10-01T15:00:00Z'
}) }}

No feed? Use a Schedule Trigger and Google Sheets

Replace the RSS Feed Trigger with a Schedule Trigger followed by a Google Sheets node that reads the next row. Point the body expression at the column that holds the text, for example $json.text, and keep the rest of the HTTP Request node as it is.

Why not n8n's native LinkedIn node

n8n's LinkedIn node sends a LinkedIn API version that is fixed in its source. When LinkedIn retires that version, every workflow using the node fails at once with HTTP 426 NONEXISTENT_VERSION until n8n ships an update and you install it. It has happened more than once: see n8n issues #14670, #14737 and #28660.

With this workflow, n8n only calls /api/v1/posts. Keeping up with LinkedIn's API versions is OpenTweet's job, not your workflow's. More on the error itself in LinkedIn API 426 NONEXISTENT_VERSION.

Frequently asked questions

Can n8n post to LinkedIn without the native LinkedIn node?

Yes. An HTTP Request node that calls OpenTweet's POST /api/v1/posts with "linkedin" in platforms publishes to your LinkedIn personal profile. You connect LinkedIn once in OpenTweet, and the workflow only holds an OpenTweet API key.

Can n8n post to Bluesky?

Yes, with the same node: add "bluesky" to platforms. Bluesky allows 300 graphemes and counts a link at its full length when you post through the API, so long titles with long URLs are the posts most likely to be skipped there.

Can I send different text to each network?

Not in one call. The v1 API takes one shared text per call. For a longer LinkedIn version, add a second HTTP Request node with "platforms": ["linkedin"] and its own text.

What happens if an RSS title is too long for one network?

That network is skipped with a skip_reason and the post still goes out on the others. Nothing is truncated.

Why not use the OpenTweet community node?

n8n-nodes-opentweet 0.1.0, the current release on npm as of September 2026, has no platforms field. The HTTP Request node reaches the same API with the platforms field today.

What does it cost?

OpenTweet starts at $11.99/mo, with Bluesky and LinkedIn on every plan and a 7-day free trial. Posts with links are on the Advanced and Agency plans, which matters here because every RSS post carries a link. URL post credits cover posts beyond the daily allowance if you buy them.

One n8n node, three networks

Connect X, Bluesky and LinkedIn once, create an API key, and import the workflow. From $11.99/mo, with Bluesky and LinkedIn on every plan.

7-day free trial. Cancel anytime.