Kuvron
CronLinuxDevOpsAutomatización

Expresiones cron: sintaxis, campos y diferencias entre plataformas

13 de octubre de 2026 · 10 min de lectura

Leer */15 8-17 * * 1-5 a primera vista toma un momento. Saber que esa expresión significa "cada 15 minutos, entre las 8 y las 17, de lunes a viernes" requiere conocer la sintaxis. Pero hay otro problema: la misma expresión puede comportarse diferente según la plataforma donde la pegués. Linux crontab, GitHub Actions, Kubernetes, AWS EventBridge y Quartz Scheduler no interpretan las expresiones cron de la misma manera.

Este artículo cubre la sintaxis estándar de cinco campos, los operadores, errores frecuentes, y las diferencias concretas entre implementaciones.

Los cinco campos de una expresión cron

Una expresión cron estándar tiene cinco campos separados por espacios:

┌───────────── minuto       (0–59)
│ ┌───────────── hora        (0–23)
│ │ ┌───────────── día del mes (1–31)
│ │ │ ┌───────────── mes        (1–12)
│ │ │ │ ┌───────────── día semana (0–6, 0 = domingo)
│ │ │ │ │
* * * * *

La expresión * * * * * se ejecuta cada minuto. Cada campo puede contener un valor específico, un comodín o una combinación de operadores.

PosiciónCampoRangoNota
1Minuto0–59
2Hora0–23
3Día del mes1–31Días inexistentes (ej. 31 de abril) nunca ejecutan
4Mes1–121 = enero, 12 = diciembre
5Día semana0–60 = domingo (no lunes como ISO 8601)

Operadores: cómo construir expresiones precisas

La sintaxis acepta cuatro operadores que se pueden combinar entre sí.

* — cualquier valor

Sin restricción en ese campo. * * * * * ejecuta cada minuto de cada hora de cada día.

*/n — paso

Ejecuta cada n unidades, comenzando desde el valor mínimo del campo. */15 en minutos ejecuta en 0, 15, 30 y 45.

*/15 * * * *    → cada 15 minutos
0 */2 * * *     → cada 2 horas, al minuto 0

n-m — rango

Todos los valores de n a m inclusive.

0 9 * * 1-5     → a las 9:00, de lunes a viernes
0 8-18 * * *    → al minuto 0, de las 8 a las 18

n,m,... — lista

Valores específicos separados por coma.

0 9,18 * * *    → a las 9:00 y a las 18:00
0 9 1,15 * *    → a las 9:00 el día 1 y el 15 de cada mes

n-m/s — rango con paso

0-30/10 * * * *   → en los minutos 0, 10, 20 y 30 de cada hora
0 9-17/2 * * 1-5  → cada 2 horas entre las 9 y las 17, de lunes a viernes

Ejemplos prácticos

ExpresiónCuándo ejecuta
* * * * *Cada minuto
*/5 * * * *Cada 5 minutos
0 * * * *Al inicio de cada hora
0 9 * * 1-5A las 9:00, lunes a viernes
0 0 * * *Todos los días a medianoche
0 0 1 * *El primer día de cada mes, a medianoche
0 0 1 1 *El 1° de enero, a medianoche
30 23 * * 0Los domingos a las 23:30
*/15 9-17 * * 1-5Cada 15 minutos, en horario laboral
0 0,12 * * *A medianoche y al mediodía
0 4 * * 0Los domingos a las 4:00

Errores frecuentes

Asumir que el campo de mes es 0-indexed

El campo mes va de 1 a 12 (enero = 1). En JavaScript, Date.getMonth() devuelve 0–11 — el patrón opuesto. Si generás una expresión cron programáticamente desde JavaScript, ese desfase es fácil de introducir.

0 0 1 0 *    ← inválido: mes 0 no existe
0 0 1 1 *    ← correcto: el 1° de enero

Asumir que el domingo es 7

En la convención Unix estándar, 0 = domingo y 6 = sábado. Algunos sistemas como Vixie cron también aceptan 7 como domingo, pero no todos. Para compatibilidad máxima, usá 0.

*/60 en el campo de minutos

*/60 parte del mínimo (0) y avanza de 60 en 60: el único valor resultante es 0. Es equivalente a escribir 0 directamente, pero puede ser rechazado por algunos schedulers como inválido. Preferí siempre 0.

Días del mes que no existen en todos los meses

0 9 31 * * ejecutará únicamente en los meses que tengan 31 días. Nunca ejecuta en febrero, abril, junio, septiembre o noviembre. 0 9 29 2 * — el 29 de febrero — solo ejecuta en años bisiestos. No se produce error: la condición simplemente no se cumple.

DOM y DOW ambos activos: OR, no AND

Cuando tanto el campo de día del mes como el de día de semana tienen un valor distinto de *, el comportamiento en crontab Unix es ejecutar cuando se cumpla cualquiera de las dos condiciones (OR):

0 9 15 * 1    → ejecuta el día 15 de cualquier mes
              → Y también cada lunes
              → (no "solo los lunes que caen en el día 15")

Este comportamiento OR es el de Vixie cron, adoptado por la mayoría de los sistemas Linux. AWS EventBridge y Quartz resuelven la ambigüedad con el operador ? — ver sección siguiente.

Zonas horarias: el cron corre en la zona del servidor

El cron estándar no tiene campo de timezone. La hora corresponde a la zona horaria del servidor. En servidores en UTC, 0 9 * * * es a las 9:00 UTC — que puede ser las 6:00 u otra hora local según el destino.

Cambios de hora (DST)

Si el servidor corre en una zona con horario de verano, las tareas programadas cerca de la transición pueden saltearse o duplicarse. Los schedulers que aceptan timezone explícita tienen comportamiento propio en estas situaciones — ver la sección de diferencias entre plataformas.

Diferencias entre plataformas

No todas las implementaciones cron son iguales. La sintaxis de 5 campos es el denominador común, pero hay variaciones importantes.

Linux/macOS crontab — 5 campos, timezone del sistema

Es la implementación de referencia. Se configura con crontab -e. La zona horaria es la del sistema (/etc/timezone o variable TZ). Vixie cron acepta 7 como alias de domingo. Los atajos @reboot, @hourly, @daily, @weekly, @monthly y @yearly son no estándar y no funcionan en plataformas cloud.

0 3 * * *   /usr/local/bin/backup.sh

GitHub Actions — 5 campos, UTC fijo, ejecución no garantizada puntual

El trigger on.schedule acepta expresiones de 5 campos. La zona horaria es siempre UTC — no se puede cambiar. La documentación oficial de GitHub advierte que los workflows programados pueden ejecutarse con retraso durante períodos de alta carga en la infraestructura, especialmente en los minutos :00 de cada hora. Para cargas de trabajo que requieren precisión temporal, un scheduler externo dedicado es una alternativa más predecible. GitHub deshabilita automáticamente el schedule en repositorios públicos sin actividad en los últimos 60 días.

on:
  schedule:
    - cron: "0 9 * * 1-5"   # lunes a viernes a las 9:00 UTC

Kubernetes CronJobs — 5 campos, timezone configurable desde v1.25

Usa la misma sintaxis de 5 campos. Cuando .spec.timeZone no se especifica, el controlador evalúa la expresión usando la zona horaria del kube-controller-manager. En clústeres gestionados (GKE, EKS, AKS) esto suele ser UTC, pero no está garantizado en clústeres autoalojados. Desde Kubernetes 1.25 (GA en 1.27) se puede especificar explícitamente:

spec:
  schedule: "0 9 * * 1-5"
  timeZone: "America/Lima"

Para comportamiento predecible en cualquier entorno, especificá siempre .spec.timeZone.

AWS EventBridge Rules (CloudWatch Events) — 6 campos, UTC fijo

El mecanismo tradicional de scheduling de AWS. Usa un formato propio de 6 campos:

cron(Minutos Horas Día-mes Mes Día-semana Año)

Siempre en UTC. No hay forma de cambiar la timezone.

El operador ? resuelve la ambigüedad DOM/DOW: si uno de los dos campos lleva un valor concreto o *, el otro debe llevar ?. No se puede usar * simultáneamente en ambos:

cron(0 9 * * ? *)     → 9:00 UTC todos los días   (DOM=*, DOW=?)
cron(0 9 ? * 2 *)     → 9:00 UTC todos los lunes  (DOM=?, DOW=2)
cron(0 9 15 * ? *)    → 9:00 UTC el día 15         (DOM=15, DOW=?)

AWS EventBridge Scheduler — 6 campos, timezone configurable, DST documentado

El servicio de scheduling de nueva generación de AWS (recomendado sobre EventBridge Rules). Usa el mismo formato de 6 campos y el mismo operador ?, pero permite configurar la zona horaria al crear el schedule:

aws scheduler create-schedule \
  --schedule-expression "cron(0 9 ? * 2 *)" \
  --schedule-expression-timezone "America/Lima" \
  ...

Si no se configura ScheduleExpressionTimezone, la expresión se interpreta en UTC.

AWS documenta explícitamente el comportamiento en transiciones DST:

  • Primavera (hora no existe): si la hora programada cae en el intervalo que se salta al adelantar el reloj, EventBridge Scheduler omite esa ejecución ese día y retoma el schedule normalmente al día siguiente.
  • Otoño (hora se repite): si el reloj retrocede y la hora programada ocurre dos veces, EventBridge Scheduler ejecuta el schedule una sola vez — en la primera ocurrencia, antes del cambio de hora.

Quartz CronTrigger (Java) — 6 campos + Año opcional, DOW 1 = domingo

Quartz usa una sintaxis de 6 campos donde el primero son los segundos (0–59). El séptimo campo (Año) es opcional:

Segundos Minutos Horas Día-mes Mes Día-semana [Año]

El campo Día-semana va de 1 a 7, donde 1 = domingo y 7 = sábado. También acepta nombres: SUN MON TUE WED THU FRI SAT.

En Quartz, cuando DOW tiene un valor concreto, DOM debe ser ? (y viceversa). No se puede especificar valores simultáneos en ambos campos:

0 0 9 ? * MON-FRI      → 9:00:00 lunes a viernes (DOM=?, DOW=MON-FRI)
0 0 9 L * ?            → 9:00:00 el último día de cada mes
0 0 9 ? * 6#3          → 9:00:00 el tercer viernes del mes

Spring @Scheduled — 6 campos fijos, DOW 0 = domingo, sin campo Año

Spring Framework implementa su propio parser (clase CronExpression, estable desde 5.3). Comparte la posición de los segundos con Quartz, pero tiene diferencias importantes:

  • No soporta el campo Año — siempre son exactamente 6 campos.
  • El DOW va de 0 a 7, donde 0 y 7 = domingo y 1 = lunes — la misma convención que Unix, no la de Quartz.
  • No requiere ? — a diferencia de Quartz, Spring permite * en ambos DOM y DOW simultáneamente. El ? es aceptado como alias de * para compatibilidad, pero no es obligatorio.
  • Soporta el parámetro zone para configurar timezone directamente en la anotación.
// Spring @Scheduled — 6 campos, DOW: 0=domingo, 1=lunes
@Scheduled(cron = "0 0 9 * * MON-FRI")                          // 9:00:00, lunes a viernes
@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "America/Lima")   // con timezone explícita

El riesgo al copiar una expresión entre Quartz y Spring es el DOW numérico: el mismo número representa días distintos en cada framework. 1 es lunes en Spring y domingo en Quartz; 2 es martes en Spring y lunes en Quartz. La expresión es sintácticamente válida en ambos — no hay error en tiempo de compilación ni al deployar — pero el día resultante es diferente.

// ⚠️ Válido en ambos frameworks, pero con días distintos
// Spring: 2 = martes | Quartz: 2 = lunes
"0 0 9 ? * 2"

// ✅ Portable — los nombres son iguales en ambos
"0 0 9 ? * MON"    // Quartz: válido, DOM=? obligatorio
"0 0 9 * * MON"    // Spring: válido, DOM=* sin requerir ?

Vercel Cron — 5 campos, UTC fijo

Usa la sintaxis estándar de 5 campos, siempre en UTC. Se configura en vercel.json:

{
  "crons": [
    { "path": "/api/sync", "schedule": "0 4 * * *" }
  ]
}

Tabla comparativa

PlataformaCamposSegundosAñoOp. ?Timezone por defecto
Unix crontab5NoNoNoDel sistema
GitHub Actions5NoNoNoUTC fijo
Kubernetes CronJob5NoNoNokube-controller-manager (normalmente UTC)
Vercel Cron5NoNoNoUTC fijo
EventBridge Rules6NoSíObligatorioUTC fijo
EventBridge Scheduler6NoSíObligatorioUTC por defecto; configurable
Quartz CronTrigger6–7Sí, pos. 1 / DOW: 1=DOMOpcionalObligatorioConfigurable
Spring @Scheduled6Sí, pos. 1 / DOW: 0=DOMNoNo requeridoConfigurable

Cómo validar una expresión antes de deployar

Ejecutar una expresión incorrecta en producción no produce un error inmediato: simplemente no hace lo que esperabas, o no ejecuta nunca. Antes de deployar:

  1. Parsear la expresión localmente. El Cron Expression Parser de Kuvron analiza la expresión en el navegador, muestra una descripción en lenguaje natural y calcula las próximas 5 ejecuciones con fecha y hora exactas. Sin servidor, sin que la expresión salga de tu máquina.
  2. Verificar las próximas ejecuciones calculadas. Si el parser dice "No se encontraron ejecuciones en el próximo año" o las fechas no coinciden con lo esperado, el error está en la expresión, no en el scheduler.
  3. Revisar la timezone de la plataforma. Confirmá si la plataforma usa UTC, la timezone del servidor, o una configurable. Una diferencia de zona horaria puede hacer que la tarea ejecute horas antes o después de lo previsto.
  4. Adaptar la expresión al formato de la plataforma. La sintaxis estándar de 5 campos no aplica directamente en EventBridge ni en Quartz/Spring. Convertí la expresión al formato correcto antes de deployarla.

Preguntas frecuentes

¿Qué pasa si escribo 0 9 31 2 *?

En cron tradicional, la condición nunca se cumple — el 31 de febrero no existe en ningún año — y el scheduler simplemente no ejecuta la tarea sin lanzar error. El mismo comportamiento aplica a combinaciones como el 31 de abril, junio, septiembre o noviembre. Dicho esto, el tratamiento de fechas imposibles puede variar según la implementación: algunos schedulers modernos o entornos cloud pueden rechazar la expresión en el momento de registrarla. Verificá el comportamiento concreto de la plataforma que usás.

¿La sintaxis @daily, @hourly, @weekly es estándar?

No. Son atajos no estándar introducidos por Vixie cron y adoptados por muchas implementaciones en Linux/macOS: @daily = 0 0 * * *, @hourly = 0 * * * *, @weekly = 0 0 * * 0, @monthly = 0 0 1 * *. No funcionan en GitHub Actions, Kubernetes, Vercel ni EventBridge. Si necesitás portabilidad, usá la sintaxis de 5 campos explícita.

¿Se pueden usar nombres de meses o días (MON, JAN) en lugar de números?

Algunas implementaciones — Vixie cron, Quartz, Spring — aceptan abreviaturas en inglés (JAN-DEC, SUN-SAT). Para garantizar compatibilidad entre plataformas, confirmá primero que la plataforma los soporta. En caso de duda, usá números.

¿Por qué GitHub Actions a veces no ejecuta el workflow en el horario exacto?

La documentación oficial de GitHub advierte que los workflows con on.schedule pueden ejecutarse con retraso durante períodos de alta carga en la infraestructura, especialmente en los minutos :00 de cada hora. Si la precisión temporal es crítica para tu caso de uso, un scheduler externo dedicado es una alternativa más predecible. Adicionalmente, los workflows programados se deshabilitan automáticamente en repositorios públicos sin actividad (commits, PRs, issues) en 60 días.

¿Cómo especifico "el último viernes del mes"?

La sintaxis estándar de 5 campos no tiene este operador. Quartz lo soporta con L y #: 0 0 9 ? * 6L (último viernes del mes a las 9:00:00). En EventBridge: cron(0 9 ? * 6L *). En crontab Unix, la aproximación habitual es programar todos los viernes (0 9 * * 5) y dentro del script verificar si es el último del mes.

¿Cómo sé si dos expresiones producen las mismas ejecuciones?

Calculando las próximas N ejecuciones de ambas y comparándolas. 0 */2 * * * y 0 0,2,4,6,8,10,12,14,16,18,20,22 * * * son equivalentes. El Cron Expression Parser de Kuvron muestra las próximas 5 ejecuciones de cualquier expresión para verificarlo directamente.

¿Qué ocurre con las ejecuciones programadas durante un cambio de hora (DST)?

Depende del scheduler y de si tiene timezone configurada. En AWS EventBridge Scheduler — el único de los servicios cubiertos que documenta este comportamiento explícitamente — si la hora programada cae en el intervalo que se salta al adelantar el reloj (primavera), esa ejecución se omite. Si la hora se repite al atrasar el reloj (otoño), el scheduler ejecuta una sola vez, en la primera ocurrencia. Para schedulers que operan exclusivamente en UTC (GitHub Actions, Vercel, EventBridge Rules), este problema no aplica siempre que el requisito del sistema sea una hora UTC fija.

← Volver al blog