Bluesky API Scheduling for Developers: Build Your Own Solution
Learn how to schedule Bluesky posts using the AT Protocol API. A developer guide to building custom scheduling solutions with code examples and best practices.
If you’re a developer looking to build custom scheduling for Bluesky, the AT Protocol provides the APIs needed to create, schedule, and manage posts programmatically. This guide explains the technical foundations for Bluesky API scheduling and how to build your own solution.
Understanding the AT Protocol
Bluesky runs on the Authenticated Transfer Protocol (AT Protocol or atproto), an open standard for decentralized social networking. Unlike proprietary APIs from traditional platforms, atproto is designed for third-party development.
Key Protocol Concepts
Repositories: Each user has a data repository containing their content. Posts, profiles, follows, and other data live in repos.
Lexicons: Schema definitions that describe data types and API methods. Bluesky’s lexicons define what a post looks like, how to create one, etc.
Records: Individual data items (like a post). Creating a post means creating a record in a user’s repository.
PDSes (Personal Data Servers): Servers that host user repositories. Most users are on Bluesky’s main PDS, but the protocol allows self-hosting.
Official Documentation
The Bluesky API documentation provides comprehensive reference. Start there for official guides, endpoint documentation, and SDK information.
Authentication
Before making API calls, authenticate to establish a session.
Getting Credentials
For scheduling applications, use app passwords rather than the user’s main password:
- User goes to Bluesky Settings → App Passwords
- Creates a new app password with descriptive name
- Uses this password for API authentication
App passwords can be revoked individually without affecting the main account.
Creating a Session
Sessions are created via the com.atproto.server.createSession endpoint. The official SDKs handle this:
TypeScript example concept:
1
2
3
4
5
6
7
8
9
10
import { BskyAgent } from '@atproto/api'
const agent = new BskyAgent({
service: 'https://bsky.social'
})
await agent.login({
identifier: 'handle.bsky.social',
password: 'app-password-here'
})
Python example concept:
1
2
3
4
from atproto import Client
client = Client()
client.login('handle.bsky.social', 'app-password-here')
Sessions return access tokens for subsequent requests. The SDKs manage token refresh automatically.
Security Considerations
For scheduling applications:
- Store credentials securely (never in code)
- Use environment variables or secrets management
- Implement token refresh for long-running processes
- Design for credential rotation
Creating Posts via API
The core operation: publishing a post.
Post Structure
Posts are records of type app.bsky.feed.post. Key fields:
- text: The post content (up to 300 characters)
- createdAt: ISO timestamp (represents when the content was created)
- facets: Rich text features (links, mentions)
- embed: Attached content (images, links, quotes)
- reply: Reference to parent post (for threading)
- langs: Language tags
Basic Post Creation
Conceptually, creating a post:
1
2
3
4
await agent.post({
text: 'Hello from the API!',
createdAt: new Date().toISOString()
})
The SDK wraps the underlying com.atproto.repo.createRecord call for posts.
Rich Text and Facets
Links and mentions require facets—annotations indicating where rich text elements appear:
1
2
3
4
5
6
7
8
9
10
11
12
13
// Conceptual structure for a post with a link
{
text: 'Check out this article!',
facets: [
{
index: { byteStart: 15, byteEnd: 27 },
features: [{
$type: 'app.bsky.richtext.facet#link',
uri: 'https://example.com/article'
}]
}
]
}
Facet indices use byte positions, not character positions—important for non-ASCII text. The official SDKs include utilities for calculating facets correctly.
Adding Images
Posts can include up to four images:
- Upload image blob via
com.atproto.repo.uploadBlob - Reference the returned blob in the post’s embed field
1
2
3
4
5
6
7
8
9
10
11
12
13
// Conceptual flow
const imageBlob = await agent.uploadBlob(imageData, { encoding: 'image/jpeg' })
await agent.post({
text: 'Check out this image',
embed: {
$type: 'app.bsky.embed.images',
images: [{
alt: 'Description of the image',
image: imageBlob.blob
}]
}
})
Include alt text for accessibility—the Bluesky community values it.
Building Scheduling Logic
The Bluesky API handles immediate posting. Scheduling means building infrastructure to trigger API calls at specified times.
Approach 1: Cron Jobs
Simple and reliable for single-server setups:
- Store scheduled posts with target publication times in a database
- Run a cron job periodically (e.g., every minute)
- Query for posts where scheduled_time <= now AND not yet published
- For each, make the API call to publish
- Mark as published or handle failures
Advantages: Simple, battle-tested approach
Considerations: Requires server running continuously, timing granularity limited by cron frequency
Approach 2: Task Queues
More robust for production systems:
- Store scheduled posts with target times
- Use a task queue (Celery, Bull, etc.) to schedule delayed tasks
- When task executes, make the API call
- Queue handles retries, failures, distribution
Advantages: Better failure handling, scalable
Considerations: More infrastructure complexity
Approach 3: Serverless Scheduled Functions
Cloud functions with scheduled triggers:
- Database stores scheduled post data
- Cloud function runs every N minutes
- Queries for due posts and publishes them
- Cloud provider manages execution
Advantages: No server management, pay-per-use
Considerations: Cold start times, execution limits
Handling Failures
API calls can fail. Design for resilience:
- Implement retry logic with backoff
- Handle rate limiting (wait and retry)
- Log failures for debugging
- Consider notification for persistent failures
- Decide policy for posts that couldn’t be published (retry later? alert user? abandon?)
Threading via API
Creating threads means creating posts where each replies to the previous.
Thread Structure
Replies reference their parent via the reply field:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Conceptual thread creation
// First post
const firstPost = await agent.post({
text: 'Thread: Here is my first point (1/3)'
})
// Second post replies to first
const secondPost = await agent.post({
text: 'Second point continues the thought (2/3)',
reply: {
root: { uri: firstPost.uri, cid: firstPost.cid },
parent: { uri: firstPost.uri, cid: firstPost.cid }
}
})
// Third post replies to second
await agent.post({
text: 'Final conclusion (3/3)',
reply: {
root: { uri: firstPost.uri, cid: firstPost.cid },
parent: { uri: secondPost.uri, cid: secondPost.cid }
}
})
Note: root always references the first post; parent references the immediate previous post.
Threading in Scheduled Context
For scheduled threads:
- Store all thread posts together with scheduled time
- At publication time, create posts sequentially
- Capture URI/CID of each to correctly link subsequent posts
- Handle partial failures (what if post 3/5 fails?)
Thread Failure Handling
Threads complicate failure handling:
- If early post fails, remaining posts can’t be correctly threaded
- May need to retry the entire thread or alert for manual intervention
- Consider “all or nothing” approach for thread atomicity
Rate Limiting Considerations
Bluesky has rate limits to prevent abuse.
Current Approach
Rate limits exist on various operations. The specific limits may change, so:
- Consult current documentation for precise limits
- Implement exponential backoff on rate limit responses
- Space operations appropriately for bulk actions
- Design scheduling to avoid bursts (e.g., don’t schedule 100 posts for the same minute)
Practical Scheduling Implications
For a scheduling service:
- Limit how many posts can publish simultaneously
- Space multiple users’ posts to avoid concurrent bursts
- Implement queue management that respects rate limits
- Monitor for rate limit errors and adapt
Security and Credential Management
Building scheduling for others requires careful credential handling.
Storing User Credentials
If your application schedules on behalf of users:
- Encrypt stored credentials at rest
- Use app passwords exclusively (never main passwords)
- Implement credential rotation handling
- Design for user credential revocation
OAuth and Future Auth
The AT Protocol has OAuth support. For production applications, review current authentication recommendations in official documentation—patterns may evolve.
Audit and Logging
For a multi-user scheduling service:
- Log all scheduling operations
- Audit access to credentials
- Monitor for unusual patterns
- Implement proper access controls
Building a Simple Scheduler
Here’s a conceptual architecture for a basic scheduler:
Components
1. Web interface: User composes posts, sets schedule times, manages queue
2. Database: Stores scheduled posts with user reference, content, scheduled time, status
3. Worker process: Runs continuously or periodic job, finds due posts, publishes via API
4. Credential store: Encrypted storage for user app passwords
Database Schema Concept
1
2
3
4
5
6
7
8
9
10
11
scheduled_posts:
- id (primary key)
- user_id (reference to users)
- content (text)
- scheduled_at (datetime)
- media_refs (optional, array)
- reply_to (optional, for threads)
- status (pending/published/failed)
- published_at (datetime, nullable)
- bluesky_uri (post URI after publish)
- error_message (for failed posts)
Worker Logic Concept
1
2
3
4
5
6
7
8
9
every minute:
posts = query(status=pending, scheduled_at <= now)
for post in posts:
try:
result = post_to_bluesky(post)
update(post, status=published, bluesky_uri=result.uri)
catch error:
update(post, status=failed, error_message=error)
schedule_retry_if_appropriate(post)
This is a starting point—production systems add substantial complexity for reliability, scale, and user experience.
SDK Options
Official and community SDKs accelerate development:
Official SDKs
@atproto/api (TypeScript/JavaScript): Official SDK from Bluesky. Covers all API operations.
atproto (Python): Official Python SDK. Comparable functionality.
Community SDKs
Community members have built SDKs for other languages (Go, Rust, Ruby, etc.). Check the Bluesky developer community for current options.
Using SDKs vs. Raw HTTP
SDKs are almost always preferable:
- Handle authentication complexity
- Provide typed interfaces
- Manage token refresh
- Abstract protocol details
- Reduce error-prone boilerplate
Raw HTTP is feasible but significantly more work with more room for errors.
Testing Your Implementation
Before deploying scheduling:
Test Account
Create a test account specifically for development. Don’t test on real accounts where errors could embarrass users.
Staging Environment
If building a service:
- Set up staging environment connected to Bluesky
- Test all flows before production
- Include failure scenarios in testing
Edge Cases to Test
- Posts at exactly character limit
- Posts with various media types
- Threads of various lengths
- Timezone edge cases
- Rate limiting behavior
- Credential expiration
- Network failures and retries
Ongoing Maintenance
Running a scheduling service requires ongoing attention:
API Changes
The AT Protocol and Bluesky’s implementation may evolve:
- Monitor official channels for changes
- Keep SDKs updated
- Test after updates before production deployment
Monitoring
Implement monitoring for:
- Publication success rates
- API errors and types
- Queue depths (posts waiting to publish)
- Credential issues
User Communication
For multi-user services:
- Notify users of publication failures
- Provide visibility into scheduled queue
- Allow users to edit/cancel pending posts
Frequently Asked Questions
Can I use the free API for a commercial scheduling product?
Review current terms of service for commercial use. The AT Protocol is open, but Bluesky may have specific policies for high-volume commercial applications.
How accurate is scheduled timing?
Depends on your implementation. Cron-based approaches check periodically (granularity of your cron interval). Queue-based approaches can be more precise.
Is there an official scheduling API?
No. Bluesky's API handles immediate posting. Building your own trigger mechanism is required.
What SDKs are recommended for production?
The official TypeScript and Python SDKs are well-maintained. For other languages, evaluate community SDKs carefully for activity and completeness.
How should I handle image scheduling?
Upload images at scheduling time (storing blob references) or at publication time (requires storing image files). Consider storage costs and reliability tradeoffs.
What are the rate limits for posting?
Specific limits may change. Consult current documentation and implement dynamic rate limit handling based on API responses.
