# Bluesky Posts, Threads, Feeds & Trending Topics (no login) (`yadroo/bluesky-public-posts`) Actor

Public Bluesky data as rows: an account's posts, the replies and quote posts of any post, or any public custom feed - with text, language, engagement counts, hashtags and links. Plus today's trending topics with post counts, and bulk account profiles. Keyless AT Protocol API, UTC dates.

- **URL**: https://apify.com/yadroo/bluesky-public-posts.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** Social media, News, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 bluesky data rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Public Bluesky data as rows: an account's posts, the replies and quote posts of any post, or any public custom feed - with text, language, engagement counts, hashtags and links. Plus today's trending topics with post counts, and bulk account profiles. Keyless AT Protocol API, UTC dates.

Five questions, one run each: **what an account posted**, **what people replied to one post**, **who quote-posted it**, **what is in a public feed**, **which topics are trending right now**, and **how big the audience of a handle is**. Everything is read from the public AT Protocol AppView, which answers JSON without an account, a key or a cookie, so there is no login, no browser and no proxy in the way. Rows carry the post text, the declared language, likes, reposts, replies, quotes and bookmarks, hashtags, mentions, linked pages and the post link; every timestamp is UTC ISO-8601.

> **Need it tailored to your workflow?** Missing a field, a filter, another source or a different output format? [Open an issue](https://apify.com/yadroo/bluesky-public-posts/issues) and describe what you need — we read every request and adapt the actor.

### Use cases

- **Brand and media monitoring after a forum closes its feeds**: list the posts of the accounts you watch (`accountPosts`) on a schedule with `sinceHours: 24` and `onlyNew`, and every run holds only what is new.
- **Community and support work**: turn one post link into its discussion (`threadReplies`) with the depth of every reply, so you can answer the whole branch instead of the newest comment.
- **PR after an announcement**: collect who quote-posted it and what they wrote (`includeQuotes`), the reaction a reply tree never shows.
- **Dataset and agent input**: harvest any public feed by its link (`customFeed`), filtered to the languages you can process, and keep the columns small with `fields`.
- **Social listening and newsrooms**: read the topics the network is discussing this hour with a post count and a start time (`trendingTopics`), then feed a topic's `feedUri` back into `customFeed` to read the posts behind it.
- **Influencer and competitor research**: resolve a list of handles to followers, following and post counts in one go (`profiles`, 25 accounts per request).

### Input

Examples live in `prefill`: the Console form opens filled in, so a first run from the Console works without editing anything. `prefill` is a Console feature only - a run started from the API, a schedule, an integration or MCP gets the defaults below and nothing else, so it names its own `handles`, `postUrls` or `feedUrls` (without them the run ends at once with a message that names the missing field). Defaults only change behaviour, never the subject of a search.

| Field | Type | Default | Used by | Notes |
|---|---|---|---|---|
| `mode` | string | `accountPosts` | - | `accountPosts`, `threadReplies`, `customFeed`, `trendingTopics`, `profiles` - see [Modes](#modes) |
| `handles` | string\[] | prefill `["bsky.app","nytimes.com"]` | `accountPosts`, `profiles` | Handles (`nytimes.com`), `@handle`, profile links or DIDs |
| `accountFilter` | string | `posts_no_replies` | `accountPosts` | Which half of the timeline - see [Account filters](#account-filters) |
| `postUrls` | string\[] | prefill one post link | `threadReplies` | Web links (`bsky.app/profile/<handle>/post/<id>`) or AT-URIs |
| `threadDepth` | number | `3` | `threadReplies` | 0 = the post alone, 1 = direct replies, max 10 |
| `includeQuotes` | boolean | `false` | `threadReplies` | Also write the posts quoting the given post |
| `feedUrls` | string\[] | prefill the "What's Hot" feed | `customFeed` | Web links (`bsky.app/profile/<handle-or-did>/feed/<id>`) or AT-URIs |
| `trendStatus` | string | `any` | `trendingTopics` | `any`, `trending`, `cooling` - see [Topic statuses](#topic-statuses) |
| `languages` | string\[] | all | post modes | Keep posts whose first declared language matches - see [Languages](#languages) |
| `minLikes` | number | none | post modes | Like floor at fetch time |
| `sinceHours` | number | none | post modes | Rolling UTC window, 1-8760 hours; in `accountPosts` paging stops at its edge, in the other post modes it filters what the source returns |
| `onlyNew` | boolean | `false` | post modes | Skip posts written by earlier runs of the same actor or task; only posts actually written are remembered |
| `includeReposts` | boolean | `false` | `accountPosts`, `customFeed` | Keep what the account amplified, marked `isRepost: true` |
| `maxItems` | number | `50` | - | Hard row cap, 1-1000; split over the accounts, feeds or post links you named (in `profiles`: the number of accounts read) |
| `fields` | string\[] | all | - | Keep only these columns, in this order; the identifying columns always stay (see below) |

The closed lists (`mode`, `accountFilter`, `trendStatus`, `languages`) take only the values listed in [Reference](#reference): Apify checks them before the run starts and refuses a misspelled value with the list of accepted ones, so a run never quietly answers a different question. Handles are forgiving - spaces, a leading `@`, capital letters and a pasted profile link (`https://bsky.app/profile/nytimes.com`) are all read as the handle. A handle that does not exist, a deleted post or an account that hides itself from logged-out viewers produces one row with `found: false` and the reason in `notFoundReason` - never a silent empty success.

`fields` is free text: letter case is corrected (`AuthorHandle` -> `authorHandle`), a name that is not a column of the mode is left out and named in the status message with the nearest column, and a list in which no name is a column of the mode stops the run with the list of valid columns instead of quietly writing every column. The identifying columns stay in every row whatever `fields` lists: `rowType`, `postUri`, `sourceRef`, `found` in the post modes; `rowType`, `topic` in `trendingTopics`; `rowType`, `handle`, `found` in `profiles`.

### Reference

#### Modes

| `mode` | What one row is | Source endpoint | Dataset view |
|---|---|---|---|
| `accountPosts` | A post of an account you named, newest first | author feed, paged by cursor | Posts |
| `threadReplies` | The post itself (`replyDepth: 0`) and every reply the source serves, plus quote posts when asked | post thread (+ quotes) | Posts |
| `customFeed` | A post of a public feed, in the feed's own order | feed generator | Posts |
| `trendingTopics` | A topic trending right now, with its post count and feed link | trends | Trending topics |
| `profiles` | An account with follower, following and post counts | profiles, 25 per request | Profiles |

#### Account filters

| Value | What the source returns |
|---|---|
| `posts_no_replies` | Only what the account published itself - the "what did this brand say" list (default) |
| `posts_with_replies` | The whole timeline, including the account's replies to other people |
| `posts_and_author_threads` | Posts plus the threads the account wrote to itself |
| `posts_with_media` | Only posts with an image or a video attached |

The filter is applied by the source, so posts outside it are never fetched and never charged.

#### Topic statuses

`trending` (still gaining posts) and `cooling` (past its peak) are the statuses you can filter by. The source also uses others - `saturating` was seen on 2026-10-01, `stale` on 2026-10-02 - and those rows are kept, untouched, when `trendStatus` is `any`. Most topics in a typical answer are not `trending`: of the 25 topics served on 2026-10-01 only three were, and at 06:00 UTC on 2026-10-02 none was (4 `cooling`, 21 `stale`). A `trending`-only run is therefore short by nature and can be empty in quiet hours - the status message then lists the statuses the source did serve.

#### Languages

`en` English, `pt` Portuguese, `ja` Japanese, `de` German, `es` Spanish, `fr` French, `it` Italian, `nl` Dutch, `tr` Turkish, `ru` Russian, `uk` Ukrainian, `pl` Polish, `ko` Korean, `zh` Chinese, `id` Indonesian, `fa` Persian, `th` Thai, `ar` Arabic.

Pick the two-letter codes from this list; any other value (a name like `English`, a three-letter code like `eng`) is refused by Apify before the run starts. The language is **declared by the app that published the post**, not detected here: a post of a news account often declares none, and such posts are dropped when `languages` is set. A tag like `en-US` is reported as `language: "en"`, with the full tag kept in `langs`.

#### Links and AT-URIs

| What you have | What it means |
|---|---|
| `bsky.app/profile/nytimes.com` | `handles: ["nytimes.com"]` |
| `bsky.app/profile/bsky.app/post/3mwolmfws5k2r` | A post: handle + record id -> `at://<did>/app.bsky.feed.post/3mwolmfws5k2r` |
| `bsky.app/profile/bsky.app/feed/whats-hot` | A feed: handle + feed id -> `at://<did>/app.bsky.feed.generator/whats-hot` |
| `/profile/did:plc:…/feed/b74101f118b8` (a topic's link) | The feed behind a trending topic - paste it into `feedUrls` |

Both shapes work in `postUrls` and `feedUrls`; the handle is resolved to its DID before the URI is built, which is what makes quote posts and third-party feeds answer at all.

### Examples

**What two newsrooms published, newest first**

```json
{ "mode": "accountPosts", "handles": ["nytimes.com", "theonion.com"], "accountFilter": "posts_no_replies", "includeReposts": false, "maxItems": 20 }
```

**The discussion under one post, three levels deep**

```json
{ "mode": "threadReplies", "postUrls": ["https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"], "threadDepth": 3, "maxItems": 25 }
```

**Who quote-posted an announcement (no reply tree)**

```json
{ "mode": "threadReplies", "postUrls": ["https://bsky.app/profile/aster.id/post/3mwqgsd4cgs2a"], "threadDepth": 0, "includeQuotes": true, "maxItems": 15 }
```

**English posts of a public feed**

```json
{ "mode": "customFeed", "feedUrls": ["https://bsky.app/profile/bsky.app/feed/whats-hot"], "languages": ["en"], "maxItems": 20 }
```

**What is trending right now**

```json
{ "mode": "trendingTopics", "trendStatus": "trending", "maxItems": 20 }
```

**Audience numbers for a handful of accounts**

```json
{ "mode": "profiles", "handles": ["bsky.app", "nytimes.com", "theonion.com", "atproto.com"], "maxItems": 10 }
```

**Monitoring run for a schedule: only new posts of the last day that travelled**

```json
{ "mode": "accountPosts", "handles": ["nytimes.com"], "sinceHours": 24, "minLikes": 10, "onlyNew": true, "fields": ["authorHandle", "text", "likeCount", "createdAt", "url"], "maxItems": 50 }
```

### Output

A real row from the cloud run `YDFsTLQ4jo3cXAXu5` (2026-10-01, `accountPosts`):

```json
{
  "rowType": "post",
  "postUri": "at://did:plc:eclio37ymobqex2ncko63h4r/app.bsky.feed.post/3mwtnkjsrpk2i",
  "postId": "3mwtnkjsrpk2i",
  "cid": "bafyreietviuynzhap4qo3ecpqf2k7ocxpmersfw4khjg5ukoe3jyesrihe",
  "url": "https://bsky.app/profile/nytimes.com/post/3mwtnkjsrpk2i",
  "authorHandle": "nytimes.com",
  "authorDisplayName": "The New York Times",
  "authorDid": "did:plc:eclio37ymobqex2ncko63h4r",
  "text": "Death row inmate Christa Pike is in critical condition after Tennessee's failed attempt to execute her, her lawyers said. Follow live updates:",
  "language": null,
  "langs": [],
  "createdAt": "2026-10-01T20:01:17.935Z",
  "indexedAt": "2026-10-01T20:01:18.568Z",
  "likeCount": 13,
  "repostCount": 1,
  "replyCount": 4,
  "quoteCount": 6,
  "bookmarkCount": 0,
  "isReply": false,
  "isRepost": false,
  "isQuotePost": false,
  "replyToUri": null,
  "replyToHandle": null,
  "threadRootUri": "at://did:plc:eclio37ymobqex2ncko63h4r/app.bsky.feed.post/3mwtnkjsrpk2i",
  "replyDepth": null,
  "quotedPostUri": null,
  "quotedAuthorHandle": null,
  "hashtags": [],
  "mentions": [],
  "links": [],
  "externalUrl": "https://nyti.ms/4jAP7nD",
  "externalTitle": "Live Updates: Christa Pike in Critical Condition After Botched Tennessee Execution",
  "embedType": "external",
  "imageCount": 0,
  "imageAlts": [],
  "labels": [],
  "sourceKind": "accountPosts",
  "sourceRef": "nytimes.com",
  "found": true,
  "notFoundReason": null,
  "fetchedAt": "2026-10-01T20:08:51.972Z"
}
```

#### Post row (`rowType: "post"`)

| Field | Type | Always filled | Meaning |
|---|---|---|---|
| `rowType` | string | yes | `post` |
| `postUri` | string | yes | AT-URI of the post |
| `postId` | string | yes | Record id - the last part of the post link |
| `cid` | string | yes | Content id of this version of the post |
| `url` | string | yes | Link to the post in the web app |
| `authorHandle` | string | yes | Who wrote it (for a repost: the original author) |
| `authorDisplayName` | string | no | Display name at fetch time |
| `authorDid` | string | yes | Stable account id |
| `text` | string | yes | Post text; empty for an image-only or video-only post |
| `language` | string | no | First declared language as a two-letter code; empty when the app declared none |
| `langs` | string\[] | yes | All declared language tags, as declared (`["en-US"]`) |
| `createdAt` | string | yes | When the author posted it, UTC |
| `indexedAt` | string | no | When the source indexed it, UTC |
| `likeCount`, `repostCount`, `replyCount`, `quoteCount`, `bookmarkCount` | number | yes | Engagement at `fetchedAt` |
| `isReply` | boolean | yes | The post answers another post |
| `isRepost` | boolean | yes | The row came into the feed as a repost |
| `isQuotePost` | boolean | yes | The post embeds another post |
| `replyToUri` | string | no | The post this one answers |
| `replyToHandle` | string | no | Author of that parent, when the source served it in the same answer |
| `threadRootUri` | string | yes | In `threadReplies`: the post the thread was read from; otherwise the root of the post's own thread |
| `replyDepth` | number | no | `threadReplies` only: 0 for the read post, 1 for a direct reply |
| `quotedPostUri` | string | no | The quoted post |
| `quotedAuthorHandle` | string | no | Author of the quoted post |
| `hashtags` | string\[] | yes | Tags, without the `#` |
| `mentions` | string\[] | yes | Mentioned handles |
| `links` | string\[] | yes | URLs in the text |
| `externalUrl`, `externalTitle` | string | no | The link card attached to the post |
| `embedType` | string | no | `images`, `video`, `external`, `quote`, `quoteWithMedia`, `feedCard`, `listCard`, `starterPackCard` |
| `imageCount` | number | yes | Attached images |
| `imageAlts` | string\[] | yes | Non-empty alt texts of those images |
| `labels` | string\[] | yes | Moderation labels on the post |
| `sourceKind` | string | yes | The mode that produced the row |
| `sourceRef` | string | yes | The exact input value that produced it (handle, post link or feed link) |
| `found` | boolean | yes | `false` on a row that explains why an input gave nothing |
| `notFoundReason` | string | no | Why, in words: deleted post, unknown handle, `optedOutOfLoggedOutVisibility` |
| `fetchedAt` | string | yes | When this run read it, UTC |

#### Trend row (`rowType: "trend"`)

`topic` (readable name), `topicId`, `description`, `category`, `status`, `postCount`, `startedAt` (UTC), `sampleAuthors` (up to 10 handles seen in the topic), `feedUri` (paste into `feedUrls`; `null` on the rare topic whose link is not a feed), `url`, `fetchedAt`. Example from run `SnSQV1ePRYRMdTQjK`: `"Celebrity Traitors series 2 premieres"`, category `entertainment`, status `trending`, `postCount: 784`, started `2026-10-01T17:54:23.570Z`.

#### Profile row (`rowType: "profile"`)

`handle`, `did`, `displayName`, `description`, `followersCount`, `followsCount`, `postsCount`, `createdAt` (UTC), `avatarUrl`, `bannerUrl`, `labels`, `verifiedStatus` (`valid`, `none`, …), `url`, `found`, `notFoundReason`, `fetchedAt`. Image URLs are reported as strings; no image is downloaded.

Dataset views: **Posts** (the three post modes), **Trending topics**, **Profiles**. Each run also writes a `SUMMARY` record into the default key-value store (linked from the run's output as `summary`) with the request count, rows read and written, rows per account/feed/post, what was skipped (reposts, languages, like floor, time window, already-seen posts, replies the source hides), every input the source would not serve and every input not read because the run stopped first.

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~bluesky-public-posts/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"accountPosts","handles":["nytimes.com"],"sinceHours":24,"maxItems":20}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/bluesky-public-posts').call({ mode: 'trendingTopics', trendStatus: 'trending', maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/bluesky-public-posts").call(run_input={
    "mode": "customFeed",
    "feedUrls": ["https://bsky.app/profile/bsky.app/feed/whats-hot"],
    "languages": ["en"],
    "fields": ["authorHandle", "text", "likeCount", "createdAt", "url"],
    "maxItems": 20,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

For an agent, `fields` is the cheap way to keep the context small: five columns instead of forty, in the order you name them. `apify call yadroo/bluesky-public-posts` works the same way from the CLI.

MCP: add `https://mcp.apify.com` to Claude / Cursor / any MCP client and call the `yadroo/bluesky-public-posts` tool with the same JSON input.

### Pricing

Pay per event: **$0.001 per run start** plus **$0.0015 per dataset row** (FREE tier). Every run is charged the start event, including a run that ends with no rows.

| Run | Rows | Cost |
|---|---|---|
| A thread or a short monitoring run | 20 | $0.001 + 20 × $0.0015 = **$0.031** |
| The default run | 50 | $0.001 + 50 × $0.0015 = **$0.076** |
| A feed harvest at the cap | 1000 | $0.001 + 1000 × $0.0015 = **$1.501** |

The row price follows your Apify plan (Bronze -10 %, Silver -20 %, Gold and above -30 %); the start event is the same on every plan, and platform usage is included in these prices. `maxItems` is a hard cap, so a run can never cost more than you planned, and `sinceHours`, `minLikes` and `onlyNew` cut rows **before** they are written - a schedule that finds nothing new costs the start event alone. The run also respects your **Maximum cost per run**: it stops writing at the last row that budget pays for and ends with "Stopped at your spending limit: N rows delivered"; with `onlyNew`, only the rows delivered are remembered, so the next run picks up the rest.

### Limits & FAQ

- **No keyword or hashtag search.** The source's post-search route answers HTTP 403 to callers without a session - checked from this runner and from an Apify run on 2026-10-01 - so this actor does not offer it, and nothing here pretends to. The closest replacement: take a topic from `trendingTopics` and read its `feedUri` with `customFeed`, or use a public feed that already collects your subject.
- **Only what is public without a login.** Accounts that ask not to be shown to logged-out viewers come back as one row with `found: false` and `notFoundReason: "optedOutOfLoggedOutVisibility"`; no other route is tried.
- **No follower or following lists, no liked-by lists** - by design, not by accident. This actor reads posts, public profile cards, topics and feeds.
- **Engagement counts are a snapshot.** The same post read tomorrow has different numbers, which is why every row carries `fetchedAt`.
- **`language` is declared, not detected.** The posting app writes it; accounts that leave it out get `language: null` and are dropped by a `languages` filter.
- **Trending topics are an unstable route.** They live in the source's own "unspecced" namespace and may change shape or disappear; the run then ends with a readable message instead of a crash, and the other four modes are untouched. At most 25 topics are served per call.
- **Third-party feeds can be empty or slow.** A feed is built by whoever published it; when it serves nothing, the run says so per feed in the status message and in `SUMMARY` instead of failing.
- **Deleted or unknown posts** give HTTP 400 at the source and become one `found: false` row with the reason - a typo in a list of ten links never disappears silently.
- **Rate limits.** The source asks callers to back off on HTTP 429 and to keep concurrency low; requests are therefore serial with a pause, and a 429 is retried after the delay the source names. A typical run makes 2-6 requests.
- **Hidden replies.** Replies removed by their author or by moderation are not served; they are counted in `SUMMARY` rather than guessed at.
- **Repeats are billed once.** A post an input serves twice (a feed repeating it, a reply that is also a quote post) is one row; the same post reached from two inputs (two accounts, two thread links) is one row per input, each with its own `sourceRef`. In `profiles`, a handle and a DID that name one account give one row.
- **Run timeout.** A run that gets close to its timeout stops starting new requests, saves what it has read and ends successfully with "Stopped before the run timeout: N rows saved". A typical run takes seconds; the default timeout is 15 minutes.
- **Thread depth costs rows, not requests**: the source returns the whole branch in one call, so `threadDepth: 10` and `threadDepth: 1` cost the same request and differ only in rows written.

***

Made by **Yadroo**. Sibling actors: [google-news-search](https://apify.com/yadroo/google-news-search), [hackernews-search](https://apify.com/yadroo/hackernews-search), [stackexchange-search](https://apify.com/yadroo/stackexchange-search), [rss-to-json](https://apify.com/yadroo/rss-to-json), [crypto-news](https://apify.com/yadroo/crypto-news).

# Actor input Schema

## `mode` (type: `string`):

Five questions, one per run. `accountPosts` is the timeline export: everything the accounts you name have posted, newest first. `threadReplies` turns one post link into its discussion - the post plus its replies, and the posts quoting it when you ask for them. `customFeed` reads any public feed by its link, including the topic feed behind a trending topic, which is the closest thing to a keyword search here. `trendingTopics` answers "what is Bluesky talking about right now" with a post count per topic. `profiles` resolves a list of handles to follower, following and post counts. Each mode fills the dataset view with its name; the other views stay empty.

## `handles` (type: `array`):

Handles as they appear after the @ in a profile URL (`bsky.app/profile/nytimes.com` -> `nytimes.com`), or raw DIDs (`did:plc:...`). Used by `accountPosts` and `profiles`, ignored by the other modes. In `accountPosts` the row limit is split evenly over the accounts, so three handles with a limit of 60 give roughly the last 20 posts of each. An account that does not exist, was deleted or hides its posts from logged-out viewers yields a single row with `found: false` and the reason in `notFoundReason` - a typo in a long list never disappears silently.

## `accountFilter` (type: `string`):

Passed to the source as the author-feed filter, so posts outside it are never fetched or charged. `posts_no_replies` is the default because that is what "what did this brand publish" means; switch to `posts_with_replies` for support or community work, where the replies of the account are the interesting half.

## `postUrls` (type: `array`):

Posts whose discussion you want, as web links (`bsky.app/profile/<handle>/post/<id>`) or as AT-URIs (`at://did:plc:.../app.bsky.feed.post/<id>`). Used by `threadReplies` only. Each post produces its own rows, all carrying `threadRootUri`, so several links can share one dataset. A deleted or unreachable post yields one row with `found: false`.

## `threadDepth` (type: `number`):

0 = the post alone (useful together with quote posts), 1 = direct replies, 3 = replies to replies to replies. Deeper trees cost more rows, not more requests: the source returns the whole branch in one call.

## `includeQuotes` (type: `boolean`):

Add the posts that quote-post the given link, marked `isQuotePost: true` with `quotedPostUri` pointing back at it. That is the half of a reaction a reply tree does not show, and what PR teams look for after an announcement. Combine with `threadDepth: 0` to get the quotes only.

## `feedUrls` (type: `array`):

Feeds to read, as web links (`bsky.app/profile/<handle-or-did>/feed/<id>`) or AT-URIs (`at://did:plc:.../app.bsky.feed.generator/<id>`). Used by `customFeed` only. The link of a trending topic from `trendingTopics` mode is such a feed link, so a two-step run (topics, then their feeds) gives you the posts behind a trend. Feeds built by third parties can be empty or slow for reasons outside this actor; the row count per feed is reported in the run summary.

## `trendStatus` (type: `string`):

Keep only topics the source currently marks with this status. `trending` is the live list for a newsroom; `cooling` shows what the network has just finished arguing about. Statuses other than these two are passed through untouched and kept when this is `any`.

## `languages` (type: `array`):

Keep only posts whose first declared language is one of these, compared against the `language` column. Empty = every language. The language is declared by the app that published the post, not detected by us, so a handful of posts carry none and are dropped by this filter when it is set. Applies to the three post modes.

## `minLikes` (type: `number`):

Keep only posts with at least this many likes at fetch time - the quick way to get the posts of an account that actually travelled. Empty = no floor. Engagement counts are a snapshot: the same post re-fetched tomorrow has different numbers, which is why every row carries `fetchedAt`.

## `sinceHours` (type: `number`):

Rolling window in hours, counted in UTC from the moment the run starts: 24 for a day, 168 for a week. This is the input to use on a schedule - it needs no date editing between runs. In `accountPosts` paging stops as soon as the account's feed reaches posts older than the window, so a narrow window is also a cheap run. In `threadReplies` and `customFeed` the window only filters the posts the source returns: a thread comes in one answer, and a custom feed is not ordered by time.

## `onlyNew` (type: `boolean`):

Remember the posts this run writes in a named key-value store of your account (`bluesky-seen`, or `bluesky-seen-<task id>` for a task) and, on the next run of the same actor or task, write only posts that were not written before. Made for monitoring: every run then holds just what is new, and an empty run means nothing was posted. The memory is kept per actor or task, so two tasks watching two accounts do not interfere. Only posts that were actually written are remembered: a run stopped by your maximum cost per run or by the timeout leaves the rest for the next run.

## `includeReposts` (type: `boolean`):

Author feeds mix in what the account reposted. They are dropped by default, so rows are the account's own words; switch this on to study what it amplifies. Kept rows are marked `isRepost: true` and keep the original author in `authorHandle`.

## `maxItems` (type: `number`):

Hard cap on the rows this run writes, and therefore on what it costs. Paging stops as soon as the cap is reached. In `accountPosts` the cap is divided over the handles you named, in `customFeed` over the feeds and in `threadReplies` over the post links; in `profiles` it is the number of accounts read. The run also stops at your "Maximum cost per run" and ends with a status that says so.

## `fields` (type: `array`):

Keep only the named columns, in the order given - for example `authorHandle, text, likeCount, createdAt, url`. Empty = every column of the mode. Letter case is corrected (`AuthorHandle` -> `authorHandle`); a name that is not a column of the mode is left out and reported in the status message with the nearest column; a list in which no name is a column of the mode stops the run with the list of valid columns. The identifying columns stay in every row: `rowType`, `postUri`, `sourceRef`, `found` in the post modes, `rowType`, `topic` in `trendingTopics`, `rowType`, `handle`, `found` in `profiles`.

## Actor input object example

```json
{
  "mode": "accountPosts",
  "handles": [
    "bsky.app",
    "nytimes.com"
  ],
  "accountFilter": "posts_no_replies",
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"
  ],
  "threadDepth": 3,
  "includeQuotes": false,
  "feedUrls": [
    "https://bsky.app/profile/bsky.app/feed/whats-hot"
  ],
  "trendStatus": "any",
  "onlyNew": false,
  "includeReposts": false,
  "maxItems": 50
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "handles": [
        "bsky.app",
        "nytimes.com"
    ],
    "postUrls": [
        "https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"
    ],
    "feedUrls": [
        "https://bsky.app/profile/bsky.app/feed/whats-hot"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/bluesky-public-posts").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "handles": [
        "bsky.app",
        "nytimes.com",
    ],
    "postUrls": ["https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"],
    "feedUrls": ["https://bsky.app/profile/bsky.app/feed/whats-hot"],
}

# Run the Actor and wait for it to finish
run = client.actor("yadroo/bluesky-public-posts").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "handles": [
    "bsky.app",
    "nytimes.com"
  ],
  "postUrls": [
    "https://bsky.app/profile/bsky.app/post/3mwolmfws5k2r"
  ],
  "feedUrls": [
    "https://bsky.app/profile/bsky.app/feed/whats-hot"
  ]
}' |
apify call yadroo/bluesky-public-posts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yadroo/bluesky-public-posts"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TyfMbKxiX7hAX0TeY/builds/2cx25AvDTH6zkOC5Z/openapi.json
