---
slug: 34-cron
title: Cron
description: Programá tareas en background en Synsema con cron_every y cron_after, gestionalas con cron_cancel/cron_list, y mantenelas vivas con serve.
example_ids: [cron]
---

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

```synsema
-- 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

```synsema
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

```synsema
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-semana` con `*`, rangos `a-b`, pasos `*/n`, listas, nombres de mes/día (`jan..dec`, `sun..sat`; `0` y `7` son 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_count` arranca 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_run` vos (un archivo o una tabla), compará con `now()` / `next_run` al arrancar, y `cron_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.

```synsema
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:

```sh
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.
