Post Multi-Image Bluesky Updates Without Losing Your Mind
Post Multi-Image Bluesky Updates Without Losing Your Mind
TL;DR: This n8n workflow automates posting 1-4 images to Bluesky using their native API, turning a tedious manual process into a single click. Perfect for content creators who've migrated from Twitter and refuse to spend their mornings manually uploading vacation photos to yet another social platform. You'll authenticate once with an app password, define your images and caption, and let the workflow handle the blob upload dance that Bluesky requires.
| Difficulty | ⭐⭐ Level 2 |
| Who's it for? | Content creators, social media managers, anyone sharing visual content on Bluesky |
| Problem solved | Manual image posting is repetitive and time-consuming |
| n8n workflow | Simple Bluesky multi-image post |
| Tools | Bluesky, n8n |
| Setup time | 15 minutes |
| Time saved | 5-10 minutes per multi-image post |
The Problem with Platform Migration
David discovered Bluesky last month with the enthusiasm of someone who just found out you can skip ads on YouTube. He spent an entire weekend setting up his profile, importing his Twitter archive, and crafting the perfect bio that balanced "tech entrepreneur" with "doesn't take himself too seriously." Then Monday morning arrived, and he wanted to post photos from a conference.
Twenty minutes later, he was still uploading images one at a time, copying captions between browser tabs, and muttering about how Twitter may have been a dumpster fire but at least the dumpster was convenient. By image three, he'd accidentally posted without a caption. By image four, he'd given up entirely and just tweeted instead.
This is where most people stay stuck. They migrate platforms for the right reasons but never automate the boring parts. So they revert to old habits because those old habits, at least, were muscle memory.
What This Workflow Does
This workflow takes the manual labor out of multi-image Bluesky posts. You give it a caption and up to four image URLs. It authenticates with your Bluesky account, downloads each image, uploads them individually as "blobs" to Bluesky's servers, aggregates the blob references, and creates a single post with all images embedded properly.
The workflow handles Bluesky's quirk where images aren't attached directly to posts but must first be uploaded as separate blob objects and then referenced in the post's embed structure. It's the API equivalent of having to RSVP to a party and then bring a gift, rather than just showing up with wine like a normal person.
Once configured, this becomes a one-click solution. You can trigger it manually for ad-hoc posts, schedule it for regular content drops, or hook it to a webhook so your CMS can auto-post when you publish new content. The workflow doesn't care where your images live—hosted URLs, cloud storage, whatever. It fetches them, converts them, and posts them.
Quick Start Guide
Before you dive into n8n, head over to your Bluesky settings and generate an app password. Not your regular password—Bluesky has specific app passwords designed for API access, found under Settings → App Passwords. This is good security hygiene, same reason you don't use your email password for every random service. Generate one, give it a memorable name like "n8n poster," and copy it somewhere safe. You'll need it in about three minutes.
Import the workflow template into n8n and open it up. You'll see a chain of nodes that looks intimidating at first glance but breaks down simply: authenticate, prepare images, upload each image, aggregate results, post. The "Define Credentials" node is where you paste your Bluesky username and that app password you just created. The "Set Caption" node holds your post text—300 characters max, and yes, that includes hashtags and alt text, so budget accordingly. The "Set Images" node contains an array of image URLs. Swap out the placeholder URLs with your actual images.
Run the workflow once manually to verify everything works. If Bluesky returns your post with images intact, you're golden. Now adapt the manual trigger node to whatever fits your use case. Schedule trigger for daily updates? Webhook trigger for CMS integration? HTTP request for Zapier handoffs? Pick your poison.
Step-by-Step Tutorial
The workflow begins with a manual trigger, which is n8n's way of saying "click this button to start." You'll replace this later with something useful, but for initial testing, manual triggers let you verify each step without worrying about external dependencies. When you click Test Workflow, execution begins.
The first real work happens in the Define Credentials node. This is a Set node configured to output JSON containing your Bluesky identifier—your full username like "username.bsky.social"—and your app password. Hardcoding credentials in workflows is generally frowned upon in production, but for personal automations or proof-of-concept builds, it's acceptable as long as you're not sharing the workflow file publicly. If you plan to share this or run it in a team environment, migrate these values to n8n's credentials system or environment variables.
For Advanced Readers: The credentials JSON structure looks like this:
{
"credentials": {
"identifier": "username.bsky.social",
"password": "xxxx-yyyy-zzzz-xxxx"
}
}Next comes Create Bluesky Session, an HTTP Request node that hits Bluesky's session creation endpoint. It sends your credentials and receives back an access token and DID—a decentralized identifier that uniquely represents your account. The access token is a JWT that subsequent requests use for authentication. This token is short-lived, which is why you create a fresh session at the start of each workflow run rather than caching tokens.
For Advanced Readers: The session endpoint is https://bsky.social/xrpc/com.atproto.server.createSession and returns JSON containing accessJwt and did. The workflow references these later via expressions like {{ $('Create Bluesky Session').item.json.accessJwt }}.
With authentication handled, the workflow moves to content preparation. Set Caption defines your post text. This is a simple Set node that creates a field called "Caption Text" with whatever you want to say. Keep it under 300 characters. Bluesky counts graphemes, not bytes, so emoji count as single characters, but combined emoji or special Unicode might surprise you. When in doubt, test.
Set Images follows immediately after. This node outputs a JSON array called "photos," each item containing a URL property pointing to an image. The template includes four placeholder URLs using Lorem Picsum for testing. Replace these with your actual image URLs. They can be publicly accessible HTTPS links, pre-signed S3 URLs, whatever—as long as n8n can fetch them without authentication. If your images require auth, you'll need to modify the Download Images node to include necessary headers.
For Advanced Readers: The images array structure:
{
"photos": [
{"url": "https://example.com/image1.jpg"},
{"url": "https://example.com/image2.jpg"}
]
}Now comes the interesting part. Split Out takes that photos array and creates individual execution items for each URL. This is necessary because Bluesky requires each image to be uploaded separately. You can't batch upload. So if you have four images, Split Out creates four parallel execution branches, one per image.
Download Images is another HTTP Request node, this time configured to fetch binary data from each image URL. The node runs once per execution item, so four images means four downloads. The output is raw image data stored in n8n's binary data format. This binary data is what gets uploaded to Bluesky.
Post Image to Bluesky uploads each downloaded image as a blob to Bluesky's upload endpoint. This node sends an authenticated POST request with the image binary as the body. Bluesky processes the upload and returns a blob reference—a JSON object containing properties like ref, mimeType, and size. You don't interact with these directly, but the workflow needs them for the final post.
For Advanced Readers: The upload endpoint is https://bsky.social/xrpc/com.atproto.repo.uploadBlob. The Authorization header must include Bearer [accessJwt]. The response blob object looks like:
{
"blob": {
"$type": "blob",
"ref": {...},
"mimeType": "image/jpeg",
"size": 123456
}
}After each image uploads, a Code node transforms the blob response into the structure Bluesky expects for embedded images. This node runs JavaScript that maps each blob into an object with alt text and the blob's image data. The alt text defaults to a dash—not ideal for accessibility, but acceptable for a template. In production, you'd dynamically set meaningful alt text per image.
For Advanced Readers: The Code node JavaScript:
return $input.all().map(item => ({
alt: "-",
image: {
...item.json.blob
}
}));Aggregate collects all execution branches back into a single item. Remember, Split Out created multiple branches for parallel image uploads. Aggregate merges them into one data structure containing all processed images. This merged data becomes the images array that gets embedded in the post.
Finally, Post to Bluesky creates the actual post. This HTTP Request node hits the record creation endpoint with a JSON payload containing your DID, the post text from Set Caption, a timestamp, and an embed object referencing all uploaded images. The embed type is app.bsky.embed.images, and the images array contains all those blob references collected earlier.
For Advanced Readers: The post payload structure:
{
"repo": "did:plc:...",
"collection": "app.bsky.feed.post",
"record": {
"$type": "app.bsky.feed.post",
"text": "Your caption here",
"createdAt": "2026-02-10T08:00:00.000Z",
"embed": {
"$type": "app.bsky.embed.images",
"images": [...]
}
}
}If everything executes cleanly, Bluesky returns a success response with your new post's URI and CID. You're live. The post appears on your profile with all images attached.
Key Learnings
This workflow teaches three core no-code concepts worth internalizing. First, parallel execution via Split Out and Aggregate. Many automation tasks involve processing multiple items individually then combining results. This pattern appears everywhere—processing Airtable records, sending batch emails, resizing images. Split and aggregate is the fundamental shape of batch operations.
Second, working with binary data. Most no-code tools default to JSON and text. But real-world automations often involve files, images, PDFs, audio. Understanding how to fetch binary data, pass it between nodes, and upload it to APIs unlocks entire categories of automation. This workflow downloads images as binary and uploads them as binary. That's a transferable pattern.
Third, API authentication flows. Bluesky requires creating a session first, then using the returned token for subsequent requests. This multi-step authentication dance is common across APIs. Some platforms use OAuth, others use API keys, others use session tokens. The underlying principle remains: prove who you are once, receive credentials, include those credentials in later requests. Master this and you can integrate almost any API.
What's Next
You've built a workflow that posts images to Bluesky on demand. That's useful, but automation truly shines when it removes decisions, not just clicks. So the next step is eliminating the part where you manually trigger the workflow.
If you publish content regularly—blog posts, podcast episodes, YouTube videos—connect this workflow to your CMS via webhook. When you hit Publish, your CMS calls n8n, n8n pulls your featured image and excerpt, and Bluesky gets updated automatically. No context switching, no forgetting to post, no manually reformatting content for each platform.
If you manage a brand with scheduled content, replace the manual trigger with a schedule trigger and pull images from a Google Sheet or Airtable. Your marketing team updates the sheet with next week's posts, and the workflow runs daily at 9 AM, checking for scheduled content and posting it. You've just built a social media scheduler without paying for Buffer.
Or get weird with it. Hook this to an RSS feed monitor. When your favorite blog publishes a new post, automatically share it to Bluesky with the article's featured image. Curate without lifting a finger. David would probably set this up and then forget it exists, which, ironically, is exactly the point of good automation. It should fade into infrastructure you rely on but never think about.
Build it. Ship it. Then go do something more interesting than manually uploading images.
