# Bluesky Posts Search Scraper (`maximedupre/bluesky-search`) Actor

Search public Bluesky posts by keyword, author, or post URL, or find profiles by text. Save structured post and profile records with public links, dates, media, and engagement counts.

- **URL**: https://apify.com/maximedupre/bluesky-search.md
- **Developed by:** [Maxime Dupré](https://apify.com/maximedupre) (community)
- **Categories:** Social media, Marketing, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.90 / 1,000 post returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

### 🔎 Find public Bluesky posts and profiles

Bluesky Posts Search Scraper is for researchers, marketers, developers, and anyone who needs public Bluesky data. It returns structured post and profile records with text, public links, author details, dates, media metadata, counts, and optional sentiment and topic annotations. This helps you review public conversations in a dataset.

- Collect public post and profile records with **[Bluesky Data Scraper](https://apify.com/maximedupre/bluesky-search/examples/bluesky-data-scraper)** for research.
- Find public profiles by text with **[Bluesky Profile Search](https://apify.com/maximedupre/bluesky-search/examples/bluesky-profile-search)**.
- Find public posts by keyword phrase with **[Bluesky Post Search](https://apify.com/maximedupre/bluesky-search/examples/bluesky-post-search)**.
- Collect an author's public post feed with **[Bluesky Scraper](https://apify.com/maximedupre/bluesky-search/examples/bluesky-scraper)**.
- Open a public post and collect its replies with **[Blue Sky Search](https://apify.com/maximedupre/bluesky-search/examples/blue-sky-search)**.

#### 📦 Get structured Bluesky post and profile rows

Each run writes public rows to the default dataset. A row is either a post or a profile. Fields that the public source does not provide may be absent. When several keyword phrases find the same post, the Actor saves the first eligible match once and ignores later matches. The `keyword` field records the phrase that first found that saved post.

Post rows can include text, stable public links, author identity, publication time, engagement counts, languages, images, link previews, video metadata, reply context, and lightweight sentiment and topic annotations. Profile rows can include a handle, public URL, DID, bio, avatar, and activity counts.

#### ▶️ Choose one Bluesky search path

**How to run**

1. Choose `Posts` or `Profiles` as the result type.
2. For `Posts`, choose `Keywords`, `Author feed`, or `Post URL`. For `Profiles`, enter a profile search query.
3. Set the fields for the path you chose. Other fields are ignored. Keyword searches use one shared set of filters for every phrase in the run.
4. Set `maxItems` to cap the number of saved records. If you leave it empty, the Actor returns all available matching records until the public source is exhausted.
5. Start the run and open the dataset link in the output.

Only public Bluesky content that the source makes available can be returned. Different criteria need separate runs because one shared set of filters applies to all keyword phrases.

#### ⚙️ Input

**Input fields**

| Field | Type | What it does |
|---|---|---|
| `resultType` | string | Required. Chooses `posts` for public post rows or `profiles` for public profile rows. |
| `postDiscoveryMethod` | string | For `posts`, chooses `keywords`, `authorFeed`, or `postUrl`. Only fields for the chosen path are used. |
| `keywords` | array of strings | Searches public Bluesky posts for one or more phrases. The same filters apply to every phrase. |
| `startDate` | date string | Optional UTC start date. Keeps posts published on or after this `YYYY-MM-DD` date. |
| `endDate` | date string | Optional UTC end date. Keeps posts published on or before this `YYYY-MM-DD` date. |
| `languages` | array of strings | Optional language codes for keyword searches, such as `en` or `es`. A post must match one listed language. |
| `authorFilter` | string | Optional public Bluesky handle or DID for keyword searches. Keeps posts from that author only. |
| `mentionFilter` | string | Optional public Bluesky handle or DID for keyword searches. Keeps posts that mention that account. |
| `hashtags` | array of strings | Optional hashtag text for keyword searches, with or without `#`. Keeps posts that use at least one listed hashtag. |
| `domains` | array of strings | Optional domains for keyword searches, such as `example.com`. Keeps posts that link to at least one listed domain. |
| `exactUrl` | string | Optional full URL for keyword searches. Keeps posts that share this exact URL. |
| `minLikes` | integer | Optional minimum like count for keyword searches. |
| `minReposts` | integer | Optional minimum repost count for keyword searches. |
| `minReplies` | integer | Optional minimum reply count for keyword searches. |
| `includeReplies` | boolean | For keyword searches, includes matching reply posts when `true`. When `false`, it keeps matching original posts only. |
| `sortBy` | string | For keyword searches, chooses `latest` for newer posts first or `top` for more popular posts first. |
| `authorHandle` | string | A public Bluesky handle or DID for the `authorFeed` path. |
| `postUrl` | URL string | A public Bluesky post URL for the `postUrl` path. |
| `expandReplies` | boolean | For the `postUrl` path, also collects replies to the selected post when `true`. |
| `profileQuery` | string | Text used to find public Bluesky profiles when `resultType` is `profiles`. |
| `maxItems` | integer | Optional work limit. Stops after this many records. If you leave it empty, the Actor returns all available matching records until the public source is exhausted. |

**Example input**

This example is copied from the public input of a successful current-beta default-input run:

```json
{
  "resultType": "posts",
  "postDiscoveryMethod": "keywords",
  "keywords": [
    "climate"
  ],
  "includeReplies": false,
  "sortBy": "latest",
  "expandReplies": false,
  "maxItems": 20
}
```

#### 🧾 Output

**Dataset link**

The `dataset` output is a string link to the default dataset overview. Every saved item uses one of the two row shapes below. Fields not marked as required may be absent when the public source does not provide them.

**Post rows**

| Field | Type | What it does |
|---|---|---|
| `recordType` | required string | Always `post` for this shape. |
| `text` | required string | The text written in the public Bluesky post. |
| `postUrl` | required URL string | A direct public link to the post. |
| `postUri` | string | The stable Bluesky URI for the post. |
| `publishedAt` | required date-time string | The date and time when the post was published. |
| `keyword` | string | The keyword phrase that first found the post in a keyword search. |
| `author` | object | Public identity details for the post author. |
| `author.did` | string | The author's stable decentralized identifier. |
| `author.handle` | string | The author's public Bluesky handle. |
| `author.displayName` | string | The author's public display name, when available. |
| `author.avatarUrl` | URL string | A direct link to the author's public avatar, when available. |
| `likeCount` | integer | The number of likes on the post, when available. |
| `repostCount` | integer | The number of reposts of the post, when available. |
| `replyCount` | integer | The number of replies to the post, when available. |
| `quoteCount` | integer | The number of quote posts for the post, when available. |
| `images` | array of objects | Images attached to the post, when available. The array can be empty. |
| `images[].url` | URL string | A direct link to an attached image. |
| `images[].altText` | string | Text that describes an attached image, when available. |
| `links` | array of objects | Web links attached to or shown in the post, when available. The array can be empty. |
| `links[].url` | URL string | The web address shared in the post. |
| `links[].title` | string | The title shown for the linked page, when available. |
| `links[].description` | string | The description shown for the linked page, when available. |
| `links[].imageUrl` | URL string | A direct link to the preview image for the linked page, when available. |
| `video` | object | Metadata for a video attached to the post, when available. |
| `video.playlistUrl` | URL string | The direct playlist link for the video, when available. |
| `video.thumbnailUrl` | URL string | A direct link to the video thumbnail, when available. |
| `video.altText` | string | Text that describes the video, when available. |
| `video.width` | integer | The video width in pixels, when available. |
| `video.height` | integer | The video height in pixels, when available. |
| `languages` | array of strings | Language codes found on the post, when available. The array can be empty. |
| `replyContext` | object | Links a collected reply to the post selected for reply collection, when available. |
| `replyContext.sourcePostUri` | string | The stable URI of the selected source post. |
| `replyContext.sourcePostUrl` | URL string | A direct public link to the selected source post. |
| `sentiment` | object | A lightweight sentiment result for the post or reply, when available. |
| `sentiment.label` | string | The sentiment label: `positive`, `neutral`, or `negative`. |
| `sentiment.score` | number | The sentiment score, from `-1` for most negative to `1` for most positive. |
| `topics` | array of strings | Content categories assigned to the post, when available. |

**Example post row**

This unshortened row comes from a successful current-beta post URL run:

```json
{
  "recordType": "post",
  "text": "Our final updates have been posted to the Climate.gov website, and this will be our final post to our social media channels.",
  "postUrl": "https://bsky.app/profile/climate.noaa.gov/post/3lsmjv6ncvc2e",
  "postUri": "at://did:plc:ybhjcsleqxtr2oyxiq7jijr7/app.bsky.feed.post/3lsmjv6ncvc2e",
  "publishedAt": "2025-06-27T21:00:03.630Z",
  "author": {
    "did": "did:plc:ybhjcsleqxtr2oyxiq7jijr7",
    "handle": "climate.noaa.gov",
    "displayName": "NOAA Climate.gov",
    "avatarUrl": "https://cdn.bsky.app/img/avatar/plain/did:plc:ybhjcsleqxtr2oyxiq7jijr7/bafkreigqphneccrf63pygu2ug5tjxws65loq5dwoc6gffc3fsk2e3tb5l4"
  },
  "likeCount": 963,
  "repostCount": 413,
  "replyCount": 115,
  "quoteCount": 161,
  "images": [
    {
      "url": "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:ybhjcsleqxtr2oyxiq7jijr7/bafkreid44tl6gdwtb3sh4j7kypi5feyo3fqk7dhbk4euffq4rmpfzvha54"
    }
  ],
  "links": [
    {
      "url": "https://climate.gov/"
    }
  ],
  "languages": [
    "en"
  ],
  "sentiment": {
    "label": "neutral",
    "score": 0
  }
}
```

**Profile rows**

| Field | Type | What it does |
|---|---|---|
| `recordType` | required string | Always `profile` for this shape. |
| `did` | string | The profile's stable decentralized identifier, when available. |
| `handle` | required string | The profile's public Bluesky handle. |
| `displayName` | string | The profile's public display name, when available. |
| `description` | string | The public profile bio, when available. |
| `avatarUrl` | URL string | A direct link to the profile avatar, when available. |
| `profileUrl` | required URL string | A direct public link to the profile. |
| `followersCount` | integer | The number of public followers, when available. |
| `followingCount` | integer | The number of public accounts this profile follows, when available. |
| `postsCount` | integer | The number of public posts from this profile, when available. |

**Example profile row**

This unshortened row comes from a successful current-beta profile search run:

```json
{
  "recordType": "profile",
  "handle": "metoffice.gov.uk",
  "profileUrl": "https://bsky.app/profile/metoffice.gov.uk",
  "did": "did:plc:pzn3wkuiqw224zyfwvdhpn6j",
  "displayName": "Met Office - weather and climate",
  "description": "Official UK Met Office account. Get the weather in your hand with our app: http://linktr.ee/MOContent",
  "avatarUrl": "https://cdn.bsky.app/img/avatar/plain/did:plc:pzn3wkuiqw224zyfwvdhpn6j/bafkreicdxw5q2mxirjgtgze7y6pfplghg55drhstxum6w236x45moryeyy",
  "followersCount": 42365,
  "followingCount": 35,
  "postsCount": 2876
}
```

#### 💳 Pricing

**How charges work**

This Actor uses pay per event pricing. For keyword searches, the `Post returned` event is charged for each public post found by a keyword phrase and saved to the dataset. The current event price appears in the pricing tab. If no post is saved, no `Post returned` event occurs. For keyword searches, `maxItems` can cap saved rows and the matching post charges.

#### 🔌 Integrations

**Use the data**

Open the `dataset` link in Apify Console or read its items through the Apify API. Download the rows as JSON, CSV, Excel, XML, RSS, or HTML.

https://www.youtube.com/watch?v=bNACk1\_S\_6w\&list=PLObrtcm1Kw6MUrlLNDbK9QRg8VDJg0gOW\&index=4

#### ❓ FAQ

##### Can I search posts and profiles in one run?

No. Choose `Posts` or `Profiles` for a run. Start another run for the other type.

##### Can I use different filters for each keyword?

No. One shared set of post-search filters applies to every phrase in the run. Use separate runs for different filter sets.

##### What happens when several keywords find the same post?

The Actor saves the first eligible match once and ignores later matches. The `keyword` field shows the phrase that found the saved row first.

##### Can I collect an author's feed?

Yes. Choose `Posts`, choose `Author feed`, and enter a public handle or DID. The feed returns post rows.

##### Can I collect replies to a known post?

Yes. Choose `Posts`, choose `Post URL`, turn on `Collect replies`, and set `maxItems`. The public source may not expose every reply.

##### What if I leave Maximum results empty?

The Actor returns all available matching records until the public source is exhausted. The source can still limit what it indexes or returns.

##### Why is a field missing?

Some fields are optional. They appear only when the public source provides them.

##### Does it access private or deleted content?

No. It reads public content that the source makes available. Private, deleted, blocked, or inaccessible content is not returned.

##### Are the sentiment and topic fields expert analysis?

No. They are lightweight annotations when available, not expert or machine-learning-grounded interpretation.

### 📝 Changelog

**v0.0** (01-09-2026)

- Initial release.

### 🆘 Support

For issues, questions, or feature requests, [file a ticket](https://console.apify.com/actors/maximedupre~bluesky-search/issues) and I'll fix or implement it in less than 24h 🫡

### 🔗 Related Actors

- **[Twitter (X) Scraper](https://apify.com/maximedupre/twitter-scraper)** collects public X posts, profiles, network members, and regional trends.
- **[Reddit Comments Search Scraper](https://apify.com/maximedupre/reddit-comments-search-scraper)** searches public Reddit comments for topic or brand mentions.
- **[Threads Replies Scraper](https://apify.com/maximedupre/scrape-threads-replies)** collects public replies and conversation context from Threads posts or profiles.
- **[Facebook User Posts Scraper](https://apify.com/maximedupre/facebook-user-posts-scraper)** collects public posts from Facebook profiles, Pages, or known post URLs.
- **[Bluesky Starter Pack Scraper](https://apify.com/maximedupre/bluesky-starter-packs)** finds public Starter Packs by topic, creator, link, or AT Protocol identifier.

**Made with ❤️ by Maxime Dupré**

# Actor input Schema

## `resultType` (type: `string`):

Choose Posts for public post records or Profiles for public profile records.

## `postDiscoveryMethod` (type: `string`):

For Posts, choose Keywords, Author feed, or Post URL. The fields for the selected source are used.

## `keywords` (type: `array`):

Enter one or more keyword phrases. The Actor searches public Bluesky posts that match each phrase. The same filters apply to every phrase.

## `startDate` (type: `string`):

Optional first date for the publication window. The date uses UTC, a standard time used around the world. Include posts published on or after this date. Use YYYY-MM-DD.

## `endDate` (type: `string`):

Optional last date for the publication window. The date uses UTC, a standard time used around the world. Include posts published on or before this date. Use YYYY-MM-DD.

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

For Keywords, enter optional language codes, such as en or es. A post must match one of the listed languages. Other choices ignore this field.

## `authorFilter` (type: `string`):

Optional for Keywords. Enter a public Bluesky handle or DID to keep posts from this author only. Other choices ignore this field.

## `mentionFilter` (type: `string`):

Optional for Keywords. Enter a public Bluesky handle or DID to keep posts that mention this account. Other choices ignore this field.

## `hashtags` (type: `array`):

Optional for Keywords. Enter hashtag text with or without #. Keep posts that use at least one listed hashtag. Other choices ignore this field.

## `domains` (type: `array`):

Optional for Keywords. Enter domains such as example.com. Keep posts that link to at least one listed domain. Other choices ignore this field.

## `exactUrl` (type: `string`):

Optional for Keywords. Enter a full URL. Keep posts that share this exact URL. Other choices ignore this field.

## `minLikes` (type: `integer`):

Optional for Keywords. Keep posts with at least this many likes. Other choices ignore this field.

## `minReposts` (type: `integer`):

Optional for Keywords. Keep posts with at least this many reposts. Other choices ignore this field.

## `minReplies` (type: `integer`):

Optional for Keywords. Keep posts with at least this many replies. Other choices ignore this field.

## `includeReplies` (type: `boolean`):

For Keywords, include matching reply posts in the results. Leave it off to return matching original posts only. Other choices ignore this field.

## `sortBy` (type: `string`):

For Keywords, choose Latest or Top. Latest puts newer posts first. Top puts more popular posts first. Other choices ignore this field.

## `authorHandle` (type: `string`):

Enter one public Bluesky handle or DID. Use this with Author feed.

## `postUrl` (type: `string`):

Enter one public Bluesky post URL. Use this with Post URL.

## `expandReplies` (type: `boolean`):

For Post URL, also collect replies to the selected post. Leave it off to return the selected post only. Other choices ignore this field.

## `profileQuery` (type: `string`):

Enter text to find public Bluesky profiles. Use this with Profiles.

## `maxItems` (type: `integer`):

Optional work limit. Stop after this many records. If you leave it empty, the Actor returns all available matching records until the public source is exhausted.

## Actor input object example

```json
{
  "resultType": "posts",
  "postDiscoveryMethod": "keywords",
  "keywords": [
    "climate"
  ],
  "includeReplies": false,
  "sortBy": "latest",
  "expandReplies": false,
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Link to the dataset with the search results.

# 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 = {
    "resultType": "posts",
    "postDiscoveryMethod": "keywords",
    "keywords": [
        "climate"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("maximedupre/bluesky-search").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 = {
    "resultType": "posts",
    "postDiscoveryMethod": "keywords",
    "keywords": ["climate"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("maximedupre/bluesky-search").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 '{
  "resultType": "posts",
  "postDiscoveryMethod": "keywords",
  "keywords": [
    "climate"
  ],
  "maxItems": 20
}' |
apify call maximedupre/bluesky-search --silent --output-dataset

```

## MCP server setup

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

```

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/eizJdu2Y20t1xENTk/builds/HN5qrKHtk4Kbx3tL5/openapi.json
