Programar posts
Un bot no necesita estar en marcha todo el tiempo. La configuración fiable más sencilla es un script que se ejecuta cada pocos minutos, publica lo que toca y termina.
Publica solo lo que toca
Dale al script un calendario y un intervalo de ejecución fijo. Cada ejecución publica los elementos cuya hora llegó desde la ejecución anterior: nada se publica dos veces y nada se pierde, siempre que las ejecuciones lleguen a tiempo.
// scheduled.ts: ejecútalo cada 15 minutos (cron, GitHub Actions...).
// Cada ejecución publica lo que tocaba desde la anterior,
// así que nada se publica dos veces.
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.");
}Lo mismo en Python:
# scheduled.py: ejecútalo cada 15 minutos (cron, GitHub Actions...).
# Cada ejecución publica lo que tocaba desde la anterior,
# así que nada se publica dos veces.
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 un calendario que cambia, léelo de un archivo o de una base de datos en vez de dejarlo en el código, y registra lo que se ha publicado para que una ejecución tardía o repetida no pueda publicar dos veces.
Ejecútalo con cron
En un servidor que controles, ejecuta crontab -e y añade una línea (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>&1Ejecútalo con GitHub Actions
En el repositorio de tu bot, añade .github/workflows/post.yml y guarda las
credenciales como secretos del repositorio (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 }}GitHub ejecuta los workflows programados sin garantías: una ejecución puede empezar tarde o, con mucha carga, saltarse. Mantén en el script un intervalo igual al del calendario y no cuentes con el minuto exacto.
Respeta los límites
- Publica a un ritmo humano. Un bot que inunda los timelines acaba silenciado, denunciado o limitado.
- Inicia sesión una vez por ejecución, no una vez por post: los inicios de sesión tienen su propio límite, más estricto.
- Lee los límites de uso de Bluesky antes de poner en marcha un bot muy activo.