How I Schedule Bluesky Posts Without SaaS
I schedule three Bluesky posts every day using GitHub Actions and a JSONL file. I do not use an external scheduling service.
The system works using a single file: content/bluesky-queue.jsonl.
Each line in this file is a JSON object.
- Unposted lines only contain text.
- Posted lines include a timestamp and a post URI.
The script reads the file from top to bottom. It finds the first line without a timestamp, posts it, and then updates that line.
Why I use JSONL instead of a database:
- It is easy to track changes in Git.
- Any CI job can append a new line to the file.
- It keeps the setup simple and free.
Handling Bluesky API Requirements Bluesky requires "facets" for links and hashtags. You cannot just send text. You must provide the exact byte positions for these elements.
I use a script to calculate these positions. I use TextEncoder to get the UTF-8 byte offsets. This prevents errors when you use emojis. Characters and bytes are not the same.
Optimizing GitHub Actions GitHub Actions often runs late if you schedule jobs at the top of the hour. To fix this, I use an off-minute offset. Instead of 00:00, I use 23:37. This reduces delays.
I also add a random delay between 0 and 5 minutes before posting. This makes the posting pattern look more human. It avoids the exact machine timing that some algorithms de-emphasize.
Preventing Infinite Loops When the script updates the queue, it commits the change back to the repository. This could trigger the workflow again.
I solve this with a commit message guard:
- The script adds [skip bluesky-queue] to the commit message.
- The workflow checks for this tag.
- If the tag exists, the workflow does not run.
This system is part of a long-term experiment with AI-curated sites. It stays lean, cheap, and reliable.
