Bluesky.bot

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
// 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
# 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):

bash
*/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>&1

Ejecú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):

yaml
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.