epochkit

Cron expression for every year

The cron expression 0 0 1 1 * runs at 12:00 AM, on day 1 of the month, only in January. The restricted fields are minute 0, hour 0, day of month 1, and month 1; the remaining asterisks match anything. That works out to one run per year.

0 0 1 1 *

Field breakdown

Field-by-field meaning of the cron expression 0 0 1 1 *
Field Value Meaning
Minute 0 The top of the hour (:00)
Hour 0 00:00 (midnight)
Day of month 1 Day 1 of the month
Month 1 January
Day of week * Every day of the week (0–6)

Next runs

Next 5 runs Timezone: UTC

Run times are calculated in your browser timezone. The scheduler that executes the job uses its own — always UTC on Vercel Cron, UTC by default on GitHub Actions, the host clock on crontab.

How this schedule works

Cron evaluates the five fields of 0 0 1 1 * — minute, hour, day of month, month, day of week — against the clock once a minute and starts the job on any minute where all five match, which cronstrue reads as "At 12:00 AM, on day 1 of the month, only in January": one run per year. Day of month is pinned to 1 and the month field selects January, while day of week stays an asterisk — so the run lands on whatever weekday that date happens to be, and never drifts to a neighbouring day.

When to use every year

An annual schedule is for archive and retention sweeps, rotating a yearly certificate or key, and resetting counters at the turn of the year. It is also the schedule most likely to be broken by the time it finally runs, because a year is long enough for the surrounding code to change, for credentials to expire, and for everyone who wrote it to have moved on. Treat the expression as a statement of intent, and make the job runnable on demand so that it can be exercised long before January 1 arrives.

Common mistakes with every year

Cron has no year field, so an annual expression means "every January 1, forever" and there is no way to scope it to a single year. For a genuine one-off, use the at command or guard the job with a date check. The real hazard, though, is decay. Twelve months is long enough for the runtime to be upgraded, the credentials to be rotated, the bucket to be renamed and the author to have left, and the job's one annual attempt is not the moment to discover any of it. Rehearse it in a non-production environment on a schedule you can actually observe.

Platform snippets

The same expression, ready to paste into the four schedulers that read five-field cron. The notes below each snippet are the ones that follow from this schedule; the cron expression generator lists every caveat for every platform.

GitHub Actions

yaml
name: Scheduled workflow
on:
  schedule:
    - cron: '0 0 1 1 *'
  workflow_dispatch:

jobs:
  scheduled:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run job
        run: echo "Running scheduled job"
  • • Workflows scheduled at the top of the hour may be delayed during high-load periods. Consider offsetting the minute field (e.g. "5 0 1 1 *") for more reliable timing.

Kubernetes CronJob

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-cron-job
spec:
  schedule: "0 0 1 1 *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: job
              image: my-image:latest
              command: ["/bin/sh", "-c", "echo Running"]
          restartPolicy: OnFailure
  • • Do NOT use CRON_TZ or TZ inside the schedule string. Kubernetes 1.29+ rejects this at creation with a validation error; earlier versions issue a warning. Existing CronJobs using TZ/CRON_TZ will continue to warn on update. Use the timeZone field instead.
  • • Kubernetes supports shorthand macros: @yearly, @monthly, @weekly, @daily, @hourly.

Vercel Cron

json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "crons": [
    {
      "path": "/api/cron",
      "schedule": "0 0 1 1 *"
    }
  ]
}
  • • Hobby plan: invocations occur within the specified hour (e.g. "0 8 * * *" runs between 08:00:00 and 08:59:59).

crontab

bash
# Edit your crontab: crontab -e
0 0 1 1 * /path/to/command

# With logging:
0 0 1 1 * /path/to/command >> /var/log/cron.log 2>&1
  • • Most modern implementations (vixie-cron, cronie) support shorthand macros: @yearly, @monthly, @weekly, @daily, @hourly, @reboot. These are not defined by POSIX — check your platform.

Variations

Frequently asked questions

How do I read the cron expression 0 0 1 1 *?

Read the five space-separated fields left to right as minute, hour, day of month, month, and day of week. In 0 0 1 1 * the minute is 0, hour is 0, day of month is 1, month is 1, day of week is *. Of the operators cron defines, this expression uses only the ones that matter here: an asterisk matches every value in its field. Put together, cronstrue reads the whole thing as "At 12:00 AM, on day 1 of the month, only in January", and a scheduler that checks all five fields once a minute will start the job one run per year.

What timezone does 0 0 1 1 * run in?

The expression carries no timezone of its own: it names hour 0, and the scheduler decides which hour 0 that is. Read as UTC, the run lands at 00:00 — 19:00 the previous day in New York, 00:00 in London, and 09:00 in Tokyo on a January date, with daylight saving moving the first two later in the year, so the same UTC instant is 20:00 the previous day in New York in July. That offset crosses a calendar day, and this schedule is pinned to the 1st of the month, so the date moves with it: the run starts at 00:00 UTC on the 1st, which is 19:00 on the last day of the previous month in New York. A job that archives "the period that just closed" is therefore running while that period is, locally, still open. Set the zone rather than converting by hand where you can: GitHub Actions defaults to UTC but takes an optional timezone key beside cron, and a Kubernetes CronJob has spec.timeZone, stable since 1.27. Where you cannot, convert deliberately: Vercel Cron is UTC-only, and a Linux crontab follows the host clock unless CRON_TZ overrides it.

Can a cron expression target a specific year?

Standard five-field cron has no year field: the fields stop at day of week, so every schedule repeats indefinitely. The expression 0 0 1 1 * runs at 00:00 on January 1 every year, and there is no way to say "only in 2027." Quartz adds an optional sixth or seventh field for seconds and year, but that syntax is rejected by crontab, Kubernetes, and GitHub Actions. For a genuine one-off, use the at command on Linux, a scheduled workflow guarded by a date check in the job, or a queue with a delayed message.

Related cron schedules

Build a custom schedule in the cron expression generator, or browse every epochkit tool.