First-party app
Bluesky

Bluesky

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.

stable Social MediaCommunication

About

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.

App id
io.w6w.bluesky
Version
0.1.1
Author
w6w
Licence
MIT
Categories
Social Media · Communication

Overview

Bluesky 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.

Build with Bluesky

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.

Upload an image

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.

Get an account's posts

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.

Read a custom feed

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.

Get the timeline

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 an account

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.

Unfollow an account

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.

List followers

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.

List who an account follows

follows-list

The accounts someone follows — the opposite direction from `followers-list`, which the two endpoint names make easy to confuse.

Like a post

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.

Unlike a post

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.

Count unread notifications

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.

List notifications

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.

Create a post

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.

Delete a post

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.

Get posts

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.

Search posts

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.

Get profiles

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.

Search accounts

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

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.

Remove a repost

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.

Get a thread

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>"
      }
    }
  ]
}

Here are some of the things you can do

  • Upload an image

    perform
    blob-upload
  • Get an account's posts

    read
    feed-author
  • Read a custom feed

    read
    feed-get
  • Get the timeline

    read
    feed-timeline
  • Follow an account

    perform
    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.

Install
npm install @w6w/sdk
yarn add @w6w/sdk
pnpm add @w6w/sdk
deno add npm:@w6w/sdk
Code
import { 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);
Install the CLI
npm install -g @w6w/cli
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.

What the agent gets

Credentials it can't read

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.

A tool surface scoped to the caller

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.

A durable workflow in one call

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.

Health-aware discovery

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).

Request MCP access

Health checks

Bluesky declares its own checks, so its health is a property of the app rather than something the host guesses at.

service

Bluesky service status

dependency

PDS reachable

Whether the server this account lives on is answering. Unauthenticated, so an expired session does not read as an outage.

quota

Request headroom

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.