Post, search, follow and read on Bluesky and any AT Protocol PDS — with rich-text facets built correctly and sessions that refresh rather than re-authenticate.
Bluesky ships in the w6w first-party pack. It declares 21 actions, 3 health checks, and the host runs its code in a sandbox that never sees the credential.
io.w6w.blueskyBluesky connects a workflow to the AT Protocol social network — Bluesky’s own service or any self-hosted PDS — to post, search, follow and read without re-implementing the protocol’s quirks. Posts, likes, reposts and follows are built as real network records rather than plain API calls, and rich-text links, mentions and hashtags are computed as proper facets so they render live instead of as plain grey text.
Session handling is built around Bluesky’s tight sign-in limit: an app password is exchanged once at connect time, and every subsequent call refreshes the session rather than re-authenticating, so a workflow that runs many times a day doesn’t burn through the daily quota. Actions cover posting and replies with correct thread roots, searching and reading profiles and feeds, following and reacting, and reading notifications without accidentally marking them seen.
The app also smooths over protocol details a naive integration gets wrong: resolving mentions to the account’s own identifier rather than trusting a cached handle, treating deletes as idempotent, and reporting posts and thread nodes that come back missing, blocked or taken down rather than silently misaligning them with the wrong record.
Three routes to the same 21 actions. The Workflow tab is generated from Bluesky's own manifest and carries its real ids, so it is copy-pasteable; the Code and CLI examples are the same call for any action on any app, so every app-specific value in them is a blank you fill in.
blob-upload Upload bytes and get a blob reference. Nothing is visible until a post EMBEDS the reference — an unreferenced blob is collected as garbage.
feed-author One account's feed. Defaults to excluding replies, unlike the API — and reposts arrive as posts by their ORIGINAL author, which is easy to misread.
feed-get Read a feed generator. These are third-party services — an empty page can mean the generator is having trouble rather than that the feed has ended.
feed-timeline The connected account's following feed. For 'what is new', remember the newest post's URI and stop when you reach it — a cursor from a previous run points into a shifted feed.
follow-create Follow someone. The record stores their DID rather than their handle, so the handle is resolved first — the DID is the account, the handle is a rented name.
follow-delete Unfollow. Takes the FOLLOW record's URI, or a handle or DID — given an account it finds your own follow record and deletes that.
followers-list Who follows an account, 100 at a time. The profile's `followersCount` will not match the number of accounts this returns — it counts some that are not shown.
follows-list The accounts someone follows — the opposite direction from `followers-list`, which the two endpoint names make easy to confuse.
like-create Like a post. This creates a LIKE RECORD in your own repository — the URI returned is the like's, not the post's, and it is what unliking needs.
like-delete Remove a like. Takes the LIKE record's URI, or the post's — given a post it finds your own like and removes that, because deleting by post URI is what fails confusingly.
notification-count The unread badge number — one cheap call, so it is what a schedule should poll before deciding whether to fetch the list. It cannot be filtered by kind.
notification-list Likes, replies, follows, mentions and quotes. Listing does NOT mark them read; marking is opt-in and uses the newest returned item's timestamp so nothing unseen gets cleared.
post-create Post to Bluesky. Links, mentions and hashtags are detected and turned into real facets — without them Bluesky renders them as plain text, silently.
post-delete Remove a post from your repository. Copies already pulled from the firehose by relays and mirrors are outside anyone's control — a delete is a request, not an erasure.
post-get Fetch posts by URI, up to 25. Deleted or blocked posts are ABSENT rather than null, so the URIs that did not come back are reported.
post-search Full-text search across Bluesky. Requires a session — unauthenticated this endpoint answers with an HTML 403 from an edge proxy rather than a JSON error.
profile-get Profiles for up to 25 accounts, each with the `viewer` block that says whether this connection follows, is followed by, or is blocked by them.
profile-search Find accounts by handle, display name or description. Only a custom-domain handle carries any identity claim — a display name is free text.
repost-create Repost. Like a like, this is a RECORD IN YOUR REPOSITORY pointing at the post — the URI returned is the repost's own. To add a comment, quote it with `post-create` instead.
repost-delete Remove a repost. Takes the REPOST record's URI, or the post's — given a post it finds your own repost and removes that.
thread-get A post with its ancestors and replies. Blocked and deleted nodes appear as their own kinds rather than as posts — they are counted here instead of crashing a walk.
A workflow step names the app and the action, and the editor fills in the
connection when you pick one. This is the Step shape from the
workflow spec, carrying Bluesky's real ids.
{
"manifestVersion": "2",
"name": "bluesky-example",
"steps": [
{
"id": "blob-upload",
"uses": {
"app": "io.w6w.bluesky",
"action": "blob-upload",
"connection": "conn_YOUR_CONNECTION_ID"
},
"with": {
"data": "<data>"
}
}
]
}blob-upload feed-author feed-get feed-timeline follow-create +16 more actions available
Every app-specific value here is a blank you have to fill in. An
app action is reached through the connection that authenticates it, so the
address is a connection id, not the app id — and connections belong to your account,
so a public page cannot know yours. Create one for Bluesky, then fill in
the three blanks: conn_YOUR_CONNECTION_ID, the action key, and the
parameters that action declares. The call itself is real — the shape is transcribed
from the studio's own snippet builder, which prints the same kind of blanks — but
nothing in it is specific to Bluesky. The Workflow tab is where this app's
real ids are.
npm install @w6w/sdkyarn add @w6w/sdkpnpm add @w6w/sdkdeno add npm:@w6w/sdkimport { W6wClient, isActionRun } from "@w6w/sdk";
// Reads W6W_BASE_URL and W6W_TOKEN from the environment when omitted.
const client = new W6wClient();
const envelope = await client.run({
urn: "conn_YOUR_CONNECTION_ID",
action: "blob-upload",
payload: {
data: "<value>",
// mimeType: "<value>",
// alt: "<value>",
},
});
if (isActionRun(envelope)) console.log(envelope.value); npm install -g @w6w/cli w6w run conn_YOUR_CONNECTION_ID --action blob-upload --payload '{"data":"<value>"}' Give an AI agent Bluesky — without giving it Bluesky's credentials. One MCP endpoint exposes every app, function and workflow the caller is entitled to, as tools it can discover and run. Access is granted per team while we onboard.
One tool call{
"name": "w6w_invoke",
"arguments": {
"ref": "app:io.w6w.bluesky#blob-upload",
"input": {
"data": "<data>"
}
}
}
Every tool names its target with a single ref. The
app: form above doesn't name a connection at all — the
host resolves which of the caller's Bluesky connections to sign
with, and refuses rather than guesses when the answer is ambiguous.
The token is attached host-side, at the moment of the call. It is never a tool argument, never in the model's context, and never in a transcript — so a prompt injection has nothing to exfiltrate.
Tools are derived per end user from what that person has actually connected and is entitled to — not one shared bot identity carrying the union of everyone's access.
Multi-step work runs on the workflow engine and returns a run handle the agent can poll — retries, branching and state survive the conversation that started them.
Bluesky's declared health checks are on the surface too, so an agent can tell "the vendor is down" from "your credential expired" before it burns a retry on either.
The MCP surface is part of the hosted platform. Bluesky itself is MIT, and the runtime that executes it is source-available (FSL).
Bluesky declares its own checks, so its health is a property of the app rather than something the host guesses at.
Whether the server this account lives on is answering. Unauthenticated, so an expired session does not read as an outage.
Remaining requests in the current five-minute window, read from the PDS's own rate-limit headers. The much tighter sign-in limit is documented rather than probed — probing it would consume it.