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
sandboxaccount: 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
- In n8n open Credentials, create a new Header Auth credential and call it
PostSail API. - Name:
Authorization. Value:Bearerfollowed 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.
{
"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). Replacebrand-xandbrand-blueskywith 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.
- Manual Trigger ("When clicking 'Execute workflow'"). No settings.
- Edit Fields (Set), named
Post content. In Fields to Set add:text(String),link(String),x_account(String),bluesky_account(String),external_id(String) anddry_run(Boolean, on). - 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:
- Method: POST. URL:
{{ 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 } ] }) }}- Wait: Resume After Time Interval, Wait Amount 10, Wait Unit Seconds.
- HTTP Request, named
PostSail: get result: Method GET, the same credential, and as 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.
Links on X cost more
- X's API pricing page charges $0.015 per post and $0.200 when the post contains a URL (checked 11 November 2026, docs.x.com).
- In our own test on 10 November 2026, a text post cost $0.015 and a short video post about $0.03. A reply that carries the link is still a post with a URL, so it is billed at $0.20 too. Details: what posting to X through the API costs.
- PostSail removes links from the main X post. Plans include X posts and first comments with links a month: Starter 100 and 10, Pro 500 and 40. A first comment counts as one more X 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
sandboxaccount do not count against your plan. - Plans: Starter $19/month (10 accounts), Pro $49/month (50 accounts); 7-day free trial.