Biblioteca estándar
Cron
Un scheduler de background integrado. Cada job corre en su propio thread, sin bloquear, y ejecuta su task de verdad — los contadores de cron_list() reflejan ejecuciones reales.
-- Doc example: cron scheduler. Jobs run on background threads and EXECUTE their
-- task for real — the doctest asserts the observable effect, not just registration.
intent: "doc example: cron"
require time
require file("_doctest_cron.txt")
task write_marker()
write_file("_doctest_cron.txt", "cron ran")
print("scheduled jobs: " + text(length(cron_list())))
test "a scheduled job executes its task and the counters tell the truth"
cron_after(0.2, write_marker)
sleep(0.8)
assert_eq(read_file("_doctest_cron.txt"), "cron ran")
assert_eq(length(cron_list()), 1)
each j in cron_list()
assert_eq(j["name"], "write_marker")
assert_eq(j["run_count"], 1)
assert_eq(j["errors"], 0)Programar§
task sync_inventory()
let data be http_get("https://api.warehouse.com/stock")
share data as "inventory"
cron_every(300, sync_inventory) -- cada 5 minutos (intervalo: fin de una corrida → próximo inicio)
cron_after(3600, send_reminder) -- una vez, después de 1 hora
-- De pared: una expresión cron (5 campos) o un alias
cron_every("0 9 * * *", daily_report) -- todos los días a las 09:00 UTC
cron_every("30 8 * * mon-fri", standup, {"tz": "-03:00"}) -- lunes a viernes 08:30, offset fijo -03:00
cron_every("*/15 * * * *", sync_inventory) -- :00 :15 :30 :45, alineado al reloj
cron_every("@hourly", rotate_logs) -- @hourly @daily @weekly @monthly @yearly
El task debe ser de 0 parámetros y estar definido en el top-level (el job lo ejecuta por nombre). Un task con parámetros obligatorios falla en la registración con un error claro — envolvelo en un task sin argumentos. cron_every exige un intervalo positivo; cron_after acepta delay 0 (ejecuta ya mismo). El argumento task es la referencia (sync_inventory) o su nombre como texto ("sync_inventory"); ambos builtins devuelven el nombre del job (texto), que es lo que toma cron_cancel(nombre).
Gestionar§
cron_cancel("sync_inventory") -- detener un job
let jobs be cron_list() -- listar todos los jobs
print(cron_status()) -- estado formateado
Cada entrada de cron_list() trae name, schedule ("every 300.0s", "after 60.0s" o la expresión cron), interval (segundos, o nothing en jobs por expresión), repeating, active, run_count (ejecuciones completadas), errors (ticks que terminaron en error), next_run (timestamp unix del próximo disparo) y tz ("UTC"/"+HH:MM", o nothing en jobs por intervalo). cron_status() imprime lo mismo como texto: [active] daily_report: at '0 9 *' (UTC), next 2026-08-30T09:00:00Z, runs: 3, errors: 0. Registrar un job con el mismo nombre reemplaza al anterior (los contadores arrancan de cero).
Semántica§
- Dos tipos de programación. Un número es un intervalo: un delay fijo entre el fin de una ejecución y el inicio de la siguiente (deriva lo que dure la corrida — perfecto para "cada 6 horas"). Un texto es una expresión cron:
minuto hora día mes día-de-semanacon, rangosa-b, pasos/n, listas, nombres de mes/día (jan..dec,sun..sat;0y7son domingo), o un alias (@hourly,@daily,@weekly,@monthly,@yearly). Si día-del-mes y día-de-semana están ambos restringidos, dispara cuando cualquiera matchea (la regla clásica de Vixie). El job dispara en el próximo minuto que matchea después de que termina la corrida anterior — las ocurrencias que caen mientras una corrida sigue en curso se saltean, nunca se encolan. - UTC por defecto. Como todo builtin de
time.{"tz": "-03:00"}(o"+05:30") desplaza la expresión un offset fijo. Las zonas IANA con horario de verano (America/Sao_Paulo) no están soportadas y fallan con un error claro — escribí el offset, o programá en UTC. Una expresión inválida, una que nunca matchea (0 0 31 2 *), una opción desconocida u opciones con un intervalo numérico fallan en la registración; no queda nada programado. - Sin solapamiento. Un job jamás corre dos ticks a la vez: si el task tarda más que el intervalo, los ticks se serializan.
- Errores: el job sigue. Un error de runtime en el task incrementa
errors, se loguea por el log del server ([serve] [cron] job 'x' failed: …) y el job queda programado para el próximo tick. El proceso jamás se cae por un tick fallido. - Estado in-memory, sin catch-up. Un reinicio re-registra los jobs cuando el top-level vuelve a correr;
run_countarranca de 0 y las corridas perdidas mientras el proceso estaba caído no existen — también con expresiones. Es a propósito: el scheduler no guarda ningún estado que pueda mentir. Si necesitás "corrió atrasada", esa decisión es del programa — guardálast_runvos (un archivo o una tabla), compará connow()/next_runal arrancar, ycron_after(0, task)si está vencida.
Bajo serve: estado compartido§
Bajo synsema serve, los jobs corren con el mismo estado compartido y las mismas capabilities que las rutas: db, state_*, memoria, blackboard. Los jobs del top-level arrancan recién cuando el server ya está sirviendo, y un cron_every registrado desde una ruta funciona y es visible globalmente.
task tick()
state_incr("latidos", 1)
cron_every(60, tick)
serve on 8080
route "GET /salud"
give state_get("latidos")
Mantener los jobs vivos§
Bajo run, los jobs ejecutan mientras el programa viva y se detienen cuando termina. Los jobs comparten estado entre ellos (un job puede leer lo que otro escribió con state_* o remember). Para intercambiar datos con el resto del programa, usá efectos externos: un archivo (write_file/read_file), una base en disco, o el blackboard (share/observe). Bajo serve no hace falta nada de esto: jobs y rutas ya ven el mismo estado compartido.
Usá synsema serve para mantener el proceso — y la programación — corriendo, incluso sin rutas:
synsema serve scheduler.syn -- "Serving N cron job(s). Press Ctrl+C to stop."
Los threads esperan estacionados (cero CPU entre ticks); el intérprete de cada job se construye una vez y se reusa.