Skip to main content
Nivel: Avanzado · Etiquetas: Scheduler, Broadcast, Zonas horarias, SQLite, WhatsApp Funcionalidades: Scheduler durable (bot.schedule con id, missed, run.occurrenceKey, run.signal) · Zonas horarias (scheduler.timeZone) · Difusiones (bot.broadcast con lotes, pausas y reintentos) · Datos por usuario (ctx.user, bot.user()) · Store persistente (SQLiteStore, store.schedules()) · Rate limiting

Requisitos previos

Node.js 22.12 o superior y un proyecto ESM ("type": "module"), como en Inicio rápido.

El bot completo

Cómo funciona

1. El mismo texto si se repite

El resumen se construye a partir de run.scheduledAt, el hueco que cubre la ejecución, no de la hora del reloj. Si la tarea vuelve a ejecutarse para el mismo hueco (mira el paso 6), produce el mismo mensaje. Las fechas se formatean en TIME_ZONE con toLocaleDateString, así que el día de la semana es el de esa zona, no el del servidor. Hay una sola zona para todo el bot (DIGEST_TZ): cada suscriptor recibe el resumen a las 08:00 de esa zona. Para zonas por usuario, guarda la zona de cada uno en ctx.user y registra una tarea por zona.

2. Un scheduler durable en SQLite

scheduler.store: store.schedules() guarda la próxima ejecución de cada tarea durable en el mismo archivo SQLite que el resto del bot. scheduler.timeZone hace que todas las tareas cron lean sus campos en esa zona IANA: 0 8 * * * son las 08:00 en Bogotá, corra donde corra el servidor, y los cambios de horario siguen la política documentada.

3. Altas y bajas

suscribir y baja guardan la elección del usuario con ctx.user, el perfil por usuario que vive en el store. Nadie recibe el resumen sin pedirlo: los envíos masivos no solicitados son la forma más rápida de que bloqueen un número de WhatsApp.

4. Una tarea cron durable

El id hace la tarea durable: su próxima ejecución queda guardada, un reinicio conserva el calendario y, con varias réplicas, solo una ejecuta cada ocurrencia (la reclama con un lease con fencing). missed: 'run-once' envía un resumen si el bot estaba caído a las 08:00 en vez de saltarse el día. Registra las tareas durables al arrancar, como aquí: solo se guarda el calendario, nunca el handler.

5. Un broadcast pausado

bot.broadcast() envía un contenido a muchos chats sin bloquear la cola de entrada de nadie: 20 chats por lote, una pausa de 2 segundos entre lotes y hasta 3 reintentos por chat tras un RateLimitedError (que pausa a todos los workers). Un chat que falla nunca detiene al resto; el resultado cuenta sent, failed y los chats aún pending si run.signal lo abortó (el bot se está parando).

6. Al menos una vez, sin duplicados

Una tarea durable se ejecuta al menos una vez por ocurrencia: si el proceso muere a mitad del broadcast, la ocurrencia se repite. run.occurrenceKey (daily-digest@2026-03-02T13:00:00.000Z) es estable por hueco. onProgress se ejecuta en cuanto termina cada chat, así que el bot guarda la clave como digest.last justo después de enviarle el resumen, y una repetición se salta a quien ya lo recibió ese día. Solo un chat cuyo envío salió justo antes de la caída, sin que se llegara a escribir su digest.last, puede recibirlo dos veces. En un chat privado el id del chat es el del usuario, así que bot.user(chat.id) lee el perfil correcto; los grupos se omiten.

7. Parar durante el broadcast

Una caída hace que la ocurrencia se repita, pero una parada ordenada no: bot.stop() aborta run.signal, el broadcast devuelve en pending los chats a los que no llegó, el handler termina y el scheduler confirma el hueco y pasa al día siguiente. Esos chats se quedan sin el resumen de ese día, y el bot lo registra como daily digest cut short. Despliega fuera de la franja de las 08:00, o envía tú mismo a los chats pending si perder un día importa.

Transcripción de ejemplo

Capturada con npx botmesh dev bot.js, con el reloj unos segundos antes de las 08:00 en Bogotá el lunes 2 de marzo de 2026. El resumen llega solo a las 08:00:

Ejecutarlo en WhatsApp o Telegram

node bot.js se conecta a WhatsApp: escanea el QR con el teléfono que hará de bot. Define DIGEST_TZ (por ejemplo Europe/Madrid) para cambiar la zona. Para Telegram, instala @botmesh/telegram y pasa new TelegramProvider({ token: process.env.TELEGRAM_TOKEN }) en su lugar.

Siguientes pasos