Bluesky Scheduling API
Schedule Bluesky posts for a future time via API.
The AT Protocol has no native scheduled publishing — createdAt is just metadata inside the record. PostGate layers scheduling on top: add an ISO 8601 scheduled_at to the same POST /api/posts envelope (or scheduled_at: 'queue' to take the next slot on a recurring schedule), and PostGate holds the post and calls com.atproto.repo.createRecord on the account's behalf at that instant.
The AT Protocol itself has no concept of scheduled publishing — a record is created the moment com.atproto.repo.createRecord is called, with a createdAt timestamp that's just metadata inside the record, not an instruction to a server to wait. Scheduling is something PostGate layers on top: it holds the post, waits for the target time, and only then makes that call on the account's behalf.
The request shape is identical to scheduling any other platform through PostGate.
Schedule for a fixed time
scheduled_at to the same envelope you'd use to publish immediately. PostGate stores the post as scheduled and materializes it at that instant.curl -X POST "https://postgate.studio/api/posts?profile_group_id=grp_acme123" \
-H "Authorization: Bearer $POSTGATE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bsky-morning-001" \
-d '{
"post": { "body": "Good morning. New post is live." },
"profiles": ["pro_bluesky123"],
"scheduled_at": "2026-09-01T13:00:00Z"
}'Or use a recurring queue
kind: "slots" and comma-separated HH:mm times, then create posts with scheduled_at: "queue" to drop into the next open slot instead of picking a time for every single post.curl -X POST https://postgate.studio/api/schedules \
-H "Authorization: Bearer $POSTGATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Bluesky daily slots",
"kind": "slots",
"expression": "09:00, 13:30, 18:00",
"timezone": "America/Sao_Paulo",
"profile_group_id": "grp_acme123",
"template": {
"post": { "body": "" },
"profiles": ["pro_bluesky123"]
}
}'Draft first, publish later
draft: true instead of a schedule to save the post for manual approval. Publish it on demand with POST /api/posts/{id}/publish, or edit it beforehand with PATCH /api/posts/{id} — both work on drafts and still-pending scheduled posts.What if the app password is revoked between now and then?
reconnect_required and the scheduled post fails at materialization time rather than at creation time. Check GET /api/profiles/{id}/health before relying on a long lead time for anything important.Frequently asked questions
Does Bluesky natively support scheduled posts?
No. The AT Protocol has no concept of scheduled publishing — a record is created the moment com.atproto.repo.createRecord is called, and its createdAt timestamp is just metadata inside the record, not an instruction to a server to wait. Scheduling is something PostGate layers on top: it holds the post, waits for the target time, and only then makes the call on the account’s behalf.
How do I schedule a Bluesky post for a specific time?
Add an ISO 8601 scheduled_at to the same POST /api/posts envelope you would use to publish immediately. PostGate stores the post as scheduled and materializes it at that instant. The request shape is identical to scheduling any other platform through PostGate.
How does queue scheduling work for Bluesky posts?
Define a queue schedule once with kind: "slots", comma-separated HH:mm times, and a timezone, then create posts with scheduled_at: "queue" to drop each one into the next open slot instead of picking a time for every single post.
Can I edit or approve a Bluesky post before it publishes?
Yes. Set draft: true instead of a schedule to save the post for manual approval, then publish it on demand with POST /api/posts/{id}/publish, or edit it beforehand with PATCH /api/posts/{id}. Both operations work on drafts and on still-pending scheduled posts.
Why did my scheduled Bluesky post fail after being accepted?
A scheduled post can sit for days or weeks before it fires. If the account holder revokes the app password on Bluesky’s side in the meantime, the profile flips to reconnect_required and the post fails at materialization time rather than at creation time. Check GET /api/profiles/{id}/health before relying on a long lead time for anything important.
