Agendar publicações
Um bot não precisa de estar sempre a correr. A configuração fiável mais simples é um script que corre de poucos em poucos minutos, publica o que está na hora e termina.
Publicar só o que está na hora
Dê ao script um calendário e um intervalo de execução fixo. Cada execução publica os itens cuja hora chegou desde a execução anterior: nada é publicado duas vezes e nada falha, desde que as execuções cheguem a horas.
// scheduled.ts: execute a cada 15 minutos (cron, GitHub Actions...).
// Cada execução publica o que ficou na hora desde a anterior,
// por isso nada é publicado duas vezes.
import { AtpAgent } from "@atproto/api";
const RUN_EVERY_MS = 15 * 60 * 1000;
const SCHEDULE = [
{ at: "2026-11-02T08:00:00Z", text: "Good morning! Today's forecast is up." },
{ at: "2026-11-02T18:00:00Z", text: "Evening update: clear skies tonight." },
];
const now = Date.now();
const due = SCHEDULE.filter((post) => {
const at = Date.parse(post.at);
return at <= now && at > now - RUN_EVERY_MS;
});
if (due.length > 0) {
const agent = new AtpAgent({ service: "https://bsky.social" });
await agent.login({
identifier: process.env.BLUESKY_HANDLE ?? "",
password: process.env.BLUESKY_APP_PASSWORD ?? "",
});
for (const post of due) {
await agent.post({ text: post.text, createdAt: new Date().toISOString() });
console.log(`Posted: ${post.text}`);
}
} else {
console.log("Nothing due.");
}O mesmo em Python:
# scheduled.py: execute a cada 15 minutos (cron, GitHub Actions...).
# Cada execução publica o que ficou na hora desde a anterior,
# por isso nada é publicado duas vezes.
import os
from datetime import datetime, timedelta, timezone
from atproto import Client
RUN_EVERY = timedelta(minutes=15)
SCHEDULE = [
("2026-11-02T08:00:00+00:00", "Good morning! Today's forecast is up."),
("2026-11-02T18:00:00+00:00", "Evening update: clear skies tonight."),
]
now = datetime.now(timezone.utc)
due = [
text
for at, text in SCHEDULE
if now - RUN_EVERY < datetime.fromisoformat(at) <= now
]
if due:
client = Client()
client.login(os.environ["BLUESKY_HANDLE"], os.environ["BLUESKY_APP_PASSWORD"])
for text in due:
client.send_post(text)
print(f"Posted: {text}")
else:
print("Nothing due.")Para um calendário que muda, leia-o de um ficheiro ou de uma base de dados em vez do código, e registe o que foi publicado, para que uma execução atrasada ou repetida não possa publicar duas vezes.
Executá-lo com cron
Num servidor que controla, execute crontab -e e acrescente uma linha (a
cada 15 minutos):
*/15 * * * * cd /home/bot/my-bot && BLUESKY_HANDLE=weather.bluesky.bot BLUESKY_APP_PASSWORD=$(cat ~/.bluesky-app-password) npx tsx scheduled.ts >> bot.log 2>&1Executá-lo com GitHub Actions
No repositório do seu bot, acrescente .github/workflows/post.yml e guarde
as credenciais como segredos do repositório (Settings → Secrets and
variables → Actions):
name: Post
on:
schedule:
- cron: "*/15 * * * *"
workflow_dispatch:
jobs:
post:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx tsx scheduled.ts
env:
BLUESKY_HANDLE: ${{ secrets.BLUESKY_HANDLE }}
BLUESKY_APP_PASSWORD: ${{ secrets.BLUESKY_APP_PASSWORD }}O GitHub executa os workflows agendados sem garantias: uma execução pode começar tarde ou, com muita carga, ser saltada. Mantenha no script um intervalo igual ao do calendário, e não conte com o minuto exato.
Respeitar os limites
- Publique a um ritmo humano. Um bot que inunda os feeds é silenciado, denunciado ou limitado.
- Entre uma vez por execução, não uma vez por publicação: os inícios de sessão têm o seu próprio limite, mais apertado.
- Leia os limites de utilização do Bluesky antes de pôr a correr um bot muito ativo.