Auto-posting to Bluesky with n8n and AI
Published 20 August 2026 · 5 min read
Bluesky passed 35 million accounts in early 2026, carried by a technical and journalistic user base disproportionate to its size. Martin Kleppmann and co-authors (including Jay Graber, Bluesky's CEO), in Bluesky and the AT Protocol: Usable Decentralized Social Media (ACM CoNEXT 2024), describe an architecture designed from day one so that any third-party client can read and write to the network without going through an official app — exactly what makes Bluesky, unlike Threads or X, this straightforward to automate from n8n. This guide covers both ways to do it: through the existing community node, and directly against the AT Protocol for full control.
What the AT Protocol exposes
Bluesky has no "classic" REST API: the network runs on the AT Protocol, where each account owns a data repository hosted on a server (PDS, Personal Data Server) — bsky.social by default unless you self-host your own. Three XRPC calls are enough to publish:
com.atproto.server.createSession: authenticates with anidentifier(handle or email) and an app password, never the account's main password. Returns anaccessJwt(valid a few minutes) and arefreshJwt(valid roughly 90 days).com.atproto.repo.uploadBlob: sends a binary file (image, currently up to 1 MB on the official server) directly in the request body — no public URL needed, unlike Instagram, Threads or X, which all require an already-hosted image.com.atproto.repo.createRecord: creates the actual post, withcollection: "app.bsky.feed.post"and the text content, anyembed(image, external link card) andfacets.
On the limits side: a post is capped at 300 graphemes and 3000 bytes of text, up to 4 images per post, and the API enforces a rolling limit of roughly 5,000 points per hour (one content creation costs 3 points, so about 1,666 posts/hour) and 35,000 points per day — a ceiling almost no editorial account ever gets close to.
Community node or HTTP Request: the two options in n8n
The community node @muench-dev/n8n-nodes-bluesky (and its enhanced fork n8n-nodes-bluesky-enhanced) already covers the essentials: post creation with image and alt text, replies, quotes, link cards with title and description. Install it in two minutes from Settings > Community Nodes — see our complete n8n community nodes guide for the how-to and the security precautions to take before installing a third-party package in production, especially one handling credentials.
Calling the AT Protocol directly with HTTP Request still earns its place when your pipeline needs fine-grained control over facets (links and mentions computed dynamically by the AI) or when you'd rather not depend on a third-party maintainer to stay current with the API. That is the approach detailed below; it maps just as easily onto the community node for simpler cases.
The pipeline in n8n
1. Scheduled trigger and today's topic
A Schedule Trigger reads a "ready to publish" row from the same editorial calendar used for other channels — see our article on the Notion editorial calendar for social media.
2. Drafting the text with an AI Agent
An AI Agent node writes the post from the topic, with a system prompt that sets the tone (Bluesky skews technical and conversational, less promotional than Instagram) and the hard 300-grapheme constraint. A Structured Output Parser locks the output to text, links (URL plus display text) and alt_text fields for any image, keeping the agent from slipping in Markdown that Bluesky won't render.
3. Optional image, uploaded as binary
For a post with a visual, an OpenAI node (Image resource) generates the illustration — see the guide to generating images with AI in n8n. Unlike the Instagram or Threads pipeline, no public bucket is needed: the binary file goes straight to uploadBlob, which simplifies this step considerably.
4. Human review before publishing
The draft (text, detected links, image) goes to Slack for approval, using the human-in-the-loop pattern described in our article on human approval with the Wait node and Slack buttons: a Wait node in On Webhook Call mode blocks the workflow until someone clicks "Publish" or "Reject".
5. Authenticate, upload, then create the post
1) POST https://bsky.social/xrpc/com.atproto.server.createSession
Body: { "identifier": "myaccount.bsky.social", "password": "<app_password>" }
→ returns { "accessJwt": "...", "did": "did:plc:..." }
2) POST https://bsky.social/xrpc/com.atproto.repo.uploadBlob (if there is an image)
Headers: Authorization: Bearer <accessJwt>, Content-Type: image/jpeg
Body: the raw binary file
→ returns { "blob": { "$type": "blob", "ref": {...}, "mimeType": "image/jpeg", "size": ... } }
3) POST https://bsky.social/xrpc/com.atproto.repo.createRecord
Body: {
"repo": "<did>",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "{{ $json.text }}",
"createdAt": "{{ $now.toISO() }}",
"langs": ["en"],
"facets": [ { "index": { "byteStart": 42, "byteEnd": 61 },
"features": [{ "$type": "app.bsky.richtext.facet#link", "uri": "https://..." }] } ],
"embed": { "$type": "app.bsky.embed.images#main",
"images": [{ "image": "<blob>", "alt": "{{ $json.alt_text }}" }] }
}
}
As with any quota-bound asynchronous API call, apply the HTTP Request retry and timeout best practices to the createRecord call, particularly to absorb an occasional 429 without duplicating the post on the next retry.
The facets trap: bytes, not characters
Bluesky does not parse any Markdown in text: a clickable link or a mention must be declared separately, in the facets array, with a byteStart/byteEnd position measured in UTF-8 bytes of the text, start inclusive, end exclusive. This is where most automated pipelines on non-English content trip up first: an accented or multi-byte character occupies more than one byte in UTF-8. Computing positions by counting the characters of the string rather than its byte representation silently shifts every facet located after the first such character — the link then points at the wrong text fragment, or at nothing at all. Inside a Code node, encode the text as UTF-8 before searching for positions (Buffer.from(text, "utf8") in JavaScript) rather than indexing the source string directly.
Authentication: no token-renewal workflow to orchestrate
Unlike Threads or X, which require exchanging and then periodically refreshing a long-lived token, Bluesky's app password never changes: simply recreate a session (createSession) at the start of each workflow run instead of persisting a short-lived accessJwt. The cost is negligible — 30 session creations allowed per 5 minutes — and it removes an entire refresh mechanism you'd otherwise have to monitor.
Common pitfalls
- Using the account's main password instead of a dedicated app password, generated and revocable independently from Bluesky's settings.
- Counting facet positions in characters instead of bytes, the most frequent mistake on non-English content.
- Exceeding the 3000-byte text limit by only tracking the 300 visible graphemes: a post heavy on emoji or accented characters can hit the byte limit before the character limit.
- Forgetting the
createdAtfield, required in everyapp.bsky.feed.postrecord, in ISO 8601 format.
Going further
The pipeline described here — editorial calendar, AI generation constrained by structured output, Slack review before publishing — reuses the same building blocks as our guides on Threads and X (Twitter): if either of those pipelines already runs on your side, most of it carries over as-is to Bluesky, only the API call layer changes shape. To start from an AI-assisted triage and drafting base that's already built, the Inbox AI Pack (€79) provides the same generation-and-human-review patterns, applied here to a mailbox instead of a social feed.
FAQ
Frequently asked questions
Is there a native n8n node for Bluesky?
There is no official node maintained by the n8n team, but unlike Threads or X, Bluesky benefits from a mature community node, @muench-dev/n8n-nodes-bluesky (and its enhanced fork n8n-nodes-bluesky-enhanced), which covers post creation, image uploads with alt text, replies and quotes. For full control over facets and rate limiting, calling the AT Protocol directly via HTTP Request remains the most reliable alternative.
Should I use my main Bluesky password in n8n?
No, and you specifically should not. Bluesky requires an "app password" generated from Settings > Privacy and Security, revocable independently from the main password and without access to account settings. That app password is what belongs in the n8n credential, never the primary login.
What is the character limit and posting rate limit on Bluesky?
A post is capped at 300 graphemes and 3000 bytes of text. On the rate limit side, the API applies a rolling points-based limit of roughly 5,000 points per hour and 35,000 per day per account, which in practice allows around 1,666 content creations per hour on a rolling window — far more than any normal editorial cadence needs.
How do I handle links and mentions in an AI-generated post?
Bluesky does not parse Markdown: every link or mention must be declared separately in a `facets` array, with a start and end position expressed in UTF-8 bytes of the text, not in character count. This is a frequent trap on non-English content, since accented characters occupy two bytes each in UTF-8, silently shifting any position computed by counting characters instead of bytes.
Bundle FlowKit Complet
€269