Cómo leer una expresión cron (y escribir una que se ejecute cuando crees)
Una expresión cron son cinco campos en posiciones fijas y cuatro símbolos. Aprende a leerla de un vistazo y esquiva las dos trampas que la ejecutan a la hora equivocada.
Un pull request añade una línea, */15 9-17 * * 1-5, y la revisión se atasca en una pregunta que nadie sabe responder en voz alta: ¿cuándo se ejecuta esto? Alguien aventura «¿cada 15 minutos?». Otro añade «durante el día, entre semana, creo». Los dos se acercan, y ninguno está seguro.
Las expresiones cron parecen un jeroglífico, pero casi no hay nada que memorizar. Son cinco campos en posiciones fijas y cuatro símbolos. Una vez que sabes leer las posiciones, */15 9-17 * * 1-5 dice una sola cosa, dentro de un mismo dialecto de cron, y la dice siempre. Lo que hace tropezar a la gente no es la sintaxis, sino dos trampas en las que una programación que se lee bien acaba ejecutándose a la hora equivocada. Veremos las dos.
Cinco campos, y la posición lo es todo
Una línea cron estándar son cinco campos separados por espacios. Su significado depende por completo del lugar que ocupan: no hay etiquetas, así que 30 6 y 6 30 son horas distintas, no la misma escrita de dos formas.
De izquierda a derecha:
| Posición | Campo | Rango |
|---|---|---|
| 1 | Minuto | 0–59 |
| 2 | Hora | 0–23 |
| 3 | Día del mes | 1–31 |
| 4 | Mes | 1–12 |
| 5 | Día de la semana | 0–7 (0 y 7 son domingo) |
Así que 30 6 * * * es «a las 6:30»: minuto 30, hora 6, y los tres asteriscos significan cualquier día del mes, cualquier mes y cualquier día de la semana. Para leer una línea cron, recorre esas cinco casillas y di cada una en voz alta: en el minuto __, la hora __, el día __, el mes __, el día de la semana __.
Dos de los campos se solapan de una forma que sorprende, y ahí nace la primera gran trampa. Guárdalo para la sección de «día del mes frente a día de la semana», más abajo.
En la mayoría de los analizadores, el mes y el día de la semana también aceptan nombres de tres letras: JAN–DEC y SUN–SAT. 0 9 * * MON se lee mejor que 0 9 * * 1, y usar el nombre evita las diferencias de numeración entre plataformas (más sobre esto abajo).
Cuatro símbolos hacen todo el trabajo
Todos los campos usan los mismos cuatro símbolos. Apréndelos una vez y podrás leer las formas de cinco campos más comunes.
*(asterisco): cualquier valor. Un asterisco en el campo de la hora significa cada hora; cinco asteriscos, cada minuto de cada día.,(coma): una lista.0,30en el campo del minuto es «en el :00 y el :30».MON,WED,FRIson esos tres días.-(guion): un rango.9-17en el campo de la hora es «de las 9 a las 17, ambas incluidas».MON-FRIes la semana laboral./(barra): un intervalo.*/15en el campo del minuto es «cada 15 minutos»: 0, 15, 30, 45. También puedes usar intervalos dentro de un rango:9-17/2es cada dos horas, de las 9 a las 17.
Ese último es el que más confunde, así que conviene fijarlo:
5 * * * * # en el minuto 5 de cada hora: una vez por hora
*/5 * * * * # cada 5 minutos: doce veces por hora
5 es un valor único: el minuto 5, y solo el minuto 5. */5 es un intervalo sobre todos los valores: 0, 5, 10… Uno es un punto; el otro, un ritmo. Al confundirlos, «cada cinco minutos» puede convertirse silenciosamente en «una vez por hora».
Hay una versión más sutil del mismo error. El intervalo cuenta desde el inicio del rango del campo, no desde «ahora», y ese rango se reinicia cada hora. Por eso */35 en el campo del minuto no es «cada 35 minutos». Se ejecuta en el minuto 0 y en el 35; luego entra la hora siguiente y vuelve a empezar en 0, así que te deja un hueco de 35 minutos seguido de otro de 25, para siempre. Cualquier intervalo que no divida su rango de forma exacta hace lo mismo. Si necesitas una cadencia real de 35 minutos, un solo campo cron no puede expresarla.
Ahora el ejemplo del principio se lee sin esfuerzo. */15 9-17 * * 1-5: cada 15 minutos, entre las horas 9 y 17, cualquier día del mes, cualquier mes, en los días de la semana del 1 al 5, que en cron estándar es de lunes a viernes. Fíjate en que la hora 17 está incluida, así que sigue disparándose hasta las 17:45: va de las 09:00 a las 17:45, no hasta las 17:00. Si prefieres no traducirlo en la cabeza, pégalo en la herramienta de expresiones cron: imprime la versión en lenguaje claro, desglosa cada campo con su rango válido y lista las próximas ejecuciones (5 por defecto, hasta 50).
Los números de día de la semana no son portables. El cron estándar y GitHub Actions cuentan desde
0, con0= domingo. Cloudflare Workers cuenta desde1, con1= domingo hasta7= sábado, y Kubernetes trata0= domingo. Así que1-5es de lunes a viernes en un crontab de Linux, pero de domingo a jueves en Cloudflare. Escribe los días con nombre cuando tu plataforma lo acepte, comoMON-FRIoFRI, y la numeración deja de importar.
La trampa de los campos de día: día del mes y día de la semana
Esta ha quemado a casi todo el mundo. ¿Qué ejecuta esto?
0 0 13 * FRI
Minuto 0, hora 0, día del mes 13, cualquier mes, día de la semana viernes. Parece «medianoche del viernes 13». No lo es. En el cron estándar de Unix (la línea Vixie/ISC que usan la mayoría de los crontab de Linux), cuando ambos campos, el del día del mes y el del día de la semana, están restringidos, ninguno es *, cron ejecuta la tarea cuando coincide cualquiera de los dos. Así que 0 0 13 * FRI se ejecuta a medianoche el día 13 de cada mes, y también todos los viernes. Es un O, no un Y.
La regla en una frase: si uno de los campos de día es *, el otro se aplica sin más; pero en cuanto los dos llevan un valor, se combinan con O, no con Y. Por eso no existe una línea cron sencilla para «viernes 13»: no puedes pedir las dos condiciones a la vez.
Este par de campos es también donde más difieren las implementaciones, así que toma el comportamiento de O como el valor habitual, no como una garantía universal, y consulta la documentación de tu plataforma cuando los dos campos de día lleven valor. La herramienta avisa de este caso en cuanto ve los dos campos restringidos, para que lo veas venir.
La otra trampa: ¿en qué zona horaria se ejecuta?
Una expresión cron no lleva zona horaria dentro. 0 9 * * * son «las 9:00», pero ¿las 9:00 en qué zona horaria? La respuesta se decide fuera de la expresión, y equivocarse es como una tarea de «las 9 de la mañana» acaba despertando a alguien a las 2 de la madrugada.
El crontab clásico se ejecuta en la zona horaria local del servidor. En los crontab compatibles con Cronie puedes cambiarla con un CRON_TZ al principio del archivo; pero no des por hecho que asignar una variable de entorno TZ cambie la zona de la programación, y no metas ninguno de los dos prefijos dentro de un spec.schedule de Kubernetes: Kubernetes lo rechaza y usa un campo aparte, spec.timeZone. Cambia el servidor de sitio, o deja que el centro de datos use UTC por defecto, y la misma línea se dispara a otra hora de reloj. La mayoría de los planificadores gestionados esquivan la ambigüedad fijándolo todo a UTC: tanto los disparadores schedule: de GitHub Actions como los cron triggers de Cloudflare Workers interpretan tu expresión en UTC, sin excepción.
El horario de verano añade una capa más, pero solo donde cron compara contra la hora de reloj local: los crontab de estilo Vixie/Cronie. Ahí, una tarea puesta a las 2:30 no se ejecuta el día del cambio de primavera (esa hora de reloj no existe) y puede ejecutarse dos veces el día del cambio de otoño. Las plataformas basadas en UTC no tienen ningún salto local de horario de verano; lo único que cambia es cómo se muestra la hora de una ejecución al convertirla a tu zona. Si algo de esto te afecta, no lo calcules de cabeza: la herramienta te deja elegir una zona y muestra cada próxima ejecución en esa zona y en UTC, de modo que los desfases del horario de verano se ven a simple vista. Y para la diferencia entre UTC, un desfase y una zona, mira UTC, GMT, ISO 8601 y timestamp Unix: ¿cuál es la diferencia?.
Cinco campos, seis campos y los atajos @daily
El cron tradicional de Unix tiene cinco campos. Algunos planificadores añaden un campo de segundos al principio: Spring y muchas librerías cron de lenguajes de programación usan una forma de seis campos, mientras que Quartz usa seis campos obligatorios más un séptimo opcional para el año. Así que */30 * * * * * (seis campos) es «cada 30 segundos», y */30 * * * * (cinco campos) es «cada 30 minutos». Cuenta los campos antes de leer nada más: un campo de más al principio cambia el significado de todos los demás.
Los crontab de estilo Cronie y Vixie también entienden un conjunto de atajos con nombre que sustituyen los cinco campos por completo:
@yearly # 0 0 1 1 * una vez al año, medianoche del 1 de enero
@monthly # 0 0 1 * * medianoche del día 1
@weekly # 0 0 * * 0 medianoche del domingo
@daily # 0 0 * * * cada medianoche (@midnight es lo mismo)
@hourly # 0 * * * * al inicio de cada hora
@reboot # una vez, al arrancar
Son cómodos, pero no universales: son una extensión que solo algunas implementaciones de crontab admiten. @reboot, en concreto, necesita una máquina con un ciclo de arranque, así que los planificadores gestionados que no lo tienen casi nunca lo aceptan (puede ser rechazado o simplemente no compatible). Cuando quieras inspeccionar un atajo, expándelo primero a su forma de cinco campos: la herramienta analiza la sintaxis numérica de cinco y seis campos, así que 0 0 * * * te da el desglose de campos y la lista de próximas ejecuciones que @daily por sí solo no da.
? L W # y otras extensiones de dialecto
Si ves ?, L, W o #, estás ante una extensión de dialecto, no ante cron genérico (? para «sin valor concreto», L para «último», W para «día laborable más cercano», # para «enésimo día de la semana del mes»). Se asocian sobre todo a Quartz, pero no son exclusivas de Quartz: Cloudflare Workers, por ejemplo, admite una parte de L, W y #. Y el mismo carácter puede significar cosas distintas: Kubernetes acepta ?, pero simplemente lo trata como *, no como el «sin valor concreto» de Quartz. Así que no des por sentado un único significado: lee la documentación de la plataforma a la que apuntas.
Esta es la verdadera lección de todo el artículo: la sintaxis es fácil, pero lo que decide su significado es el dialecto. Antes de desplegar una línea, contrástala con el entorno en el que se ejecutará.
| Destino | Campos | Zona horaria | Números de día de la semana | Extensiones |
|---|---|---|---|---|
| Linux / Cronie | 5 | servidor, o CRON_TZ | 0–7, 0 y 7 = domingo | atajos @ |
| GitHub Actions | 5 | UTC | 0–6, 0 = domingo | sin segundos |
| Cloudflare Workers | 5 | UTC | 1 = domingo … 7 = sábado | parte de L W # |
| Kubernetes CronJob | 5 | .spec.timeZone | 0 = domingo | ? actúa como * |
| Quartz | 6–7 | configuración del planificador | según el dialecto | ? L W # |
Dos notas prácticas que la tabla no recoge: GitHub Actions ejecuta las programaciones «en la medida de lo posible» y puede retrasarlas cuando hay carga, así que no lo uses como temporizador preciso; y Windows no tiene cron: recurre al Programador de tareas o a schtasks.
Lista de comprobación para leer una expresión cron
Cuando te llegue una línea cron:
- Cuenta los campos. Cinco es lo estándar; seis significa que el primero son segundos; un atajo como
@dailyes otra cosa. - Lee las cinco posiciones en orden (minuto, hora, día del mes, mes, día de la semana) y di cada una en voz alta.
- Interpreta los símbolos:
*significa cualquier valor,,es una lista,-es un rango,/es un intervalo. Recuerda que5(un punto) no es*/5(un ritmo). - Revisa los dos campos de día. Si el día del mes y el día de la semana llevan valor, el cron estándar lo trata como un O: la tarea se ejecuta con que coincida cualquiera (confírmalo en tu plataforma).
- Pregunta qué zona horaria usa el entorno (local del servidor o UTC), porque la expresión no lo dice.
Con eso, cualquier línea cron se vuelve legible a simple vista. Cuando prefieras confirmar en vez de traducir, sobre todo las trampas del campo de día y de la zona horaria, suelta la expresión en la herramienta de expresiones cron: descripción en lenguaje claro, rangos por campo y las horas exactas de las próximas ejecuciones en la zona que elijas.
Y si una línea que ya confirmaste como correcta sigue sin hacer nada, ese es otro problema: no la sintaxis, sino el entorno, dónde está el crontab o el propio host. La guía de campo para esa otra mitad es ¿Por qué no se ejecutó mi tarea cron?.