Post to X and Bluesky from n8n

Import one n8n workflow, keep your key in an n8n credential, check the post with a dry run and publish to X and Bluesky with one request.

Before you start

  • n8n, cloud or self-hosted.
  • A PostSail workspace and an API key. Create the key in the dashboard under API. Start with a key limited to the sandbox account: posts to it go nowhere, but they run through the real queue and checks.
  • Your X and Bluesky accounts connected in PostSail. You do not need your own X developer app.
  • What works today: X and Bluesky. The other platforms and where their reviews stand: platform status.

1. Keep the key in an n8n credential

  1. In n8n open Credentials, create a new Header Auth credential and call it PostSail API.
  2. Name: Authorization. Value: Bearer followed by a space and your PostSail key.

n8n stores the key encrypted. The workflow below contains no key, so you can share or export it safely.

2. Import the workflow

Copy the JSON and paste it on an empty n8n canvas (or use Import from File). Then open both PostSail nodes and pick the PostSail API credential.

n8n workflow (JSON)
{
  "name": "PostSail: post to X and Bluesky",
  "nodes": [
    {
      "parameters": {},
      "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000001",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000011",
              "name": "text",
              "value": "Our new clip is out.",
              "type": "string"
            },
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000012",
              "name": "link",
              "value": "https://example.com/clip",
              "type": "string"
            },
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000013",
              "name": "x_account",
              "value": "brand-x",
              "type": "string"
            },
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000014",
              "name": "bluesky_account",
              "value": "brand-bluesky",
              "type": "string"
            },
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000015",
              "name": "external_id",
              "value": "clip-launch-1",
              "type": "string"
            },
            {
              "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000016",
              "name": "dry_run",
              "value": true,
              "type": "boolean"
            }
          ]
        },
        "options": {}
      },
      "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000002",
      "name": "Post content",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [220, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://postsail.app/api/v1/posts",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ dry_run: $json.dry_run, external_id: $json.dry_run ? undefined : $json.external_id, targets: [ { account: $json.x_account, text: $json.text, first_comment: $json.link ? 'Watch it here: ' + $json.link : undefined }, { account: $json.bluesky_account, text: $json.link ? $json.text + ' ' + $json.link : $json.text } ] }) }}",
        "options": {}
      },
      "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000003",
      "name": "PostSail: create post",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [440, 0]
    },
    {
      "parameters": {
        "amount": 10,
        "unit": "seconds"
      },
      "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000004",
      "name": "Wait 10 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [660, 0]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://postsail.app/api/v1/posts/{{ $json.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "7c1e0b2a-5f3d-4c8e-9b1a-000000000005",
      "name": "PostSail: get result",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 0]
    }
  ],
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Post content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post content": {
      "main": [
        [
          {
            "node": "PostSail: create post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PostSail: create post": {
      "main": [
        [
          {
            "node": "Wait 10 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 10 seconds": {
      "main": [
        [
          {
            "node": "PostSail: get result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
  • Post content holds the text, the link and your account slugs (from GET /v1/accounts). Replace brand-x and brand-bluesky with yours.
  • PostSail: create post sends one request with one target per account. On X the link goes in first_comment, a reply; on Bluesky it stays in the text.
  • PostSail: get result reads the post 10 seconds later and shows the status, link or error on each account.

If the import does not work: build it by hand

Add these nodes in this order and connect each one to the next. Field names are the ones n8n shows.

  1. Manual Trigger ("When clicking 'Execute workflow'"). No settings.
  2. Edit Fields (Set), named Post content. In Fields to Set add: text (String), link (String), x_account (String), bluesky_account (String), external_id (String) and dry_run (Boolean, on).
  3. HTTP Request, named PostSail: create post:
    • Method: POST. URL: https://postsail.app/api/v1/posts
    • Authentication: Generic Credential Type. Generic Auth Type: Header Auth. Credential: PostSail API.
    • Send Body: on. Body Content Type: JSON. Specify Body: Using JSON.
    • JSON: switch the field to Expression and paste:
JSON (expression)
{{ JSON.stringify({ dry_run: $json.dry_run, external_id: $json.dry_run ? undefined : $json.external_id, targets: [ { account: $json.x_account, text: $json.text, first_comment: $json.link ? 'Watch it here: ' + $json.link : undefined }, { account: $json.bluesky_account, text: $json.link ? $json.text + ' ' + $json.link : $json.text } ] }) }}
  1. Wait: Resume After Time Interval, Wait Amount 10, Wait Unit Seconds.
  2. HTTP Request, named PostSail: get result: Method GET, the same credential, and as URL (Expression):
URL (expression)
https://postsail.app/api/v1/posts/{{ $json.id }}

3. Dry run first

dry_run is on in Post content. A dry run runs every check (platform rules, length, blocked words) and stores what would be sent, without calling X or Bluesky. Each target shows dry_done and a dry_preview with the final text.

When the preview looks right, set dry_run to false and run it again to publish. The workflow sends external_id only when publishing: a dry run with an external_id would reserve it, and the same external_id never creates a second post.

Run it on a schedule or from a feed

Replace Run manually with a Schedule Trigger or an RSS Feed Trigger. Set external_id from the item, for example the RSS item's link, so a retried run never posts twice. To publish later instead of now, add publish_at (ISO 8601, at most 30 days ahead) to the request.

Why not n8n's own X node?

  • n8n has an X node, and community nodes exist for Bluesky. For one network, that may be all you need. For X you then need your own X developer app and you pay X directly.
  • PostSail is one request for both networks, with a dry run, retries that never post twice, links on X handled for you and one result per account. We build PostSail, so weigh that with the limits below.

Limits

  • Today PostSail posts to X and Bluesky only; this workflow uses those two.
  • Each API key can create 30 new posts an hour; dry runs and the sandbox account do not count against your plan.
  • Plans: Starter $19/month (10 accounts), Pro $49/month (50 accounts); 7-day free trial.