Skip to content

Agendamento cron

Cron no último dia do mês

O cron padrão de 5 campos não tem sintaxe para o último dia do mês. Agende 0 0 28-31 * * e deixe o comando rodar só quando amanhã for dia 1; Quartz, Spring, node-cron e Cloudflare aceitam L direto, como em 0 0 L * *.

Último dia do mês
Expressão0 0 28-31 * *
Execuções12 vezes em 2027
OnCalendar do systemd*-*~01 00:00:00

Como funciona

Os meses terminam no dia 28, 29, 30 ou 31, e o campo de dia do mês só aceita números fixos. 0 0 31 * * perde cinco meses; 0 0 28-31 * * dispara até quatro vezes no fim de todo mês. O POSIX e o Vixie cron nunca ganharam uma palavra para “último”.

A solução portável é disparar em todo dia candidato e deixar um teste de uma linha decidir: se o dia de amanhã é 01, hoje é o último dia. Numa linha do crontab, o % de date +%d precisa ser escrito \%, senão o cron corta o comando ali.

O Quartz criou o L no campo de dia do mês, e a ideia se espalhou: o CronExpression do Spring, o node-cron 4.x e os Cron Triggers da Cloudflare aceitam, e o systemd tem a própria forma, *-*~01. A tabela de próximas execuções abaixo avalia a forma com L. Para fechamento contábil ou folha de pagamento, lembre que o último dia útil costuma importar mais que o último dia do calendário.

Explicação campo a campo
CampoValorSignificado
Minuto0minuto 0 (hora cheia)
Hora0hora 0 (0h)
Dia do mês28-31dias 28 a 31
Mês*todo mês
Dia da semana*todo dia da semana

A tabela descreve a parte portável, 0 0 28-31 * *. As próximas execuções abaixo usam 0 0 L * *, a forma no estilo Quartz, que dispara só no último dia.

Próximas execuções

Relógio do agendador

Como no GitHub Actions sem timezone, na Vercel, na Cloudflare e no Kubernetes com timeZone "Etc/UTC".

Próximas execuções, calculadas no seu navegador a partir de agora
ExecuçãoSeu horárioUTC
1Calculando… 
2  
3  
4  
5  

Versões prontas para colar

O mesmo agendamento em cada agendador, com o que muda em cada um.

crontab (Linux, macOS, BSD)

crontab -e
# Days 28-31, but only act when tomorrow is the 1st (GNU date):
0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/job.sh

# BSD / macOS date:
0 0 28-31 * * [ "$(date -v+1d +\%d)" = "01" ] && /usr/local/bin/job.sh
  • Dentro de uma linha do crontab o % precisa ser escapado como \%; um % sem escape encerra o comando e o resto vai para a entrada padrão dele. O teste roda no máximo quatro vezes por mês e não faz nada em três delas.

GitHub Actions

.github/workflows/scheduled.yml
on:
  schedule:
    - cron: '0 0 28-31 * *'
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - name: Only on the last day of the month
        id: last
        run: echo "is_last=$([ "$(date -u -d tomorrow +%d)" = 01 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
      - if: steps.last.outputs.is_last == 'true'
        run: ./job.sh
  • O GitHub usa a sintaxe POSIX do cron, sem L, então o workflow acorda nos dias 28 a 31 e um passo verifica a data. Aqui não precisa escapar %: essa regra é só do crontab.

CronJob do Kubernetes

cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: month-end
spec:
  schedule: "0 0 28-31 * *"
  timeZone: "Etc/UTC"
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: job
              image: alpine:3.20
              command: ["/bin/sh", "-c"]
              args:
                - |
                  [ "$(date -d @$(( $(date +%s) + 86400 )) +%d)" = "01" ] || exit 0
                  exec /app/job.sh
  • A sintaxe de agendamento do Kubernetes não tem L. O contêiner verifica se amanhã é dia 1 e sai com 0 caso contrário; a forma aritmética date -d @… funciona no BusyBox (Alpine) e no GNU date.

Cron Jobs da Vercel

vercel.json
{
  "crons": [
    { "path": "/api/month-end", "schedule": "0 0 28-31 * *" }
  ]
}
  • A Vercel não tem L. Agende os dias 28 a 31 e faça a função retornar cedo a menos que amanhã (em UTC) seja dia 1. São até quatro execuções por mês, uma por dia, o que o plano Hobby permite (lá com precisão por hora).

Cloudflare Workers

wrangler.toml
[triggers]
crons = ["0 0 L * *"]
  • A Cloudflare aceita o L no estilo Quartz no campo de dia do mês, então não precisa de gambiarra. LW seria o último dia útil do mês. Os horários são em UTC.

node-cron (Node.js)

scheduler.js
import cron from 'node-cron';

// L = last day of the month (node-cron 4.x)
cron.schedule('0 0 0 L * *', async () => {
  await runMonthEnd();
}, { timezone: 'UTC' });
  • O node-cron 4.x aceita L (e L-3, LW) no campo de dia do mês.

Timer do systemd

job.timer + job.service
# /etc/systemd/system/job.timer
[Unit]
Description=Run job.service on the last day of each month

[Timer]
OnCalendar=*-*~01 00:00:00
Persistent=true

[Install]
WantedBy=timers.target

# /etc/systemd/system/job.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/job.sh

# systemctl daemon-reload && systemctl enable --now job.timer
  • Confira com systemd-analyze calendar '*-*~01 00:00:00', que mostra a forma normalizada e a próxima execução. AccuracySec padrão é 1min, então o disparo pode variar até um minuto.
  • Persistent=true roda a tarefa assim que a máquina liga se o horário passou com ela desligada, algo que o cron não faz.
  • O ~ significa “contando do fim do mês”: *-*~01 é o último dia, *-*~02 o penúltimo.

Spring @Scheduled e Quartz

Java
@Scheduled(cron = "0 0 0 L * *", zone = "UTC")
public void monthEnd() { }

// Quartz CronTrigger:
CronScheduleBuilder.cronSchedule("0 0 0 L * ?")
  • Spring (5.3+) e Quartz aceitam L no campo de dia do mês; L-2 é dois dias antes do último e LW o último dia útil.

Armadilhas

0 0 31 * * roda sete vezes por ano

Só janeiro, março, maio, julho, agosto, outubro e dezembro têm dia 31. O cron não empurra a execução para o dia mais próximo; simplesmente não dispara.

O % sem escape

date +%d funciona no terminal e quebra em silêncio no crontab, onde % significa quebra de linha. Escreva date +\%d na linha do crontab, ou leve a lógica para um arquivo de script.

Considere o dia 1

Muitas tarefas de fim de mês são, na verdade, tarefas “depois que o mês fecha”. Rodar 0 0 1 * * processando o mês anterior evita o problema do último dia e ainda enxerga o último dia completo.

Perguntas frequentes

Como rodar um cron no último dia do mês?
Agende 0 0 28-31 * * e coloque [ "$(date -d tomorrow +\%d)" = "01" ] && antes do comando. Onde o L é aceito (Quartz, Spring, node-cron, Cloudflare), use L no campo de dia do mês.
O crontab aceita L para o último dia?
Não. cronie, o cron do Debian e os outros derivados do Vixie cron não aceitam L. Ele vem do Quartz.
Como fica o último dia do mês no systemd?
OnCalendar=*-*~01 00:00:00. O til conta os dias a partir do fim do mês.
Como rodar no último dia útil do mês?
Use LW no Quartz, no Spring, no node-cron ou na Cloudflare. No cron puro, rode nos dias 26 a 31 em dias de semana e verifique no script que não sobra outro dia útil no mês; feriados exigem uma lista à parte.

Revisado em por Arielton Oberek.