Crons
Scheduled jobs with tracked runs, logs, and metrics.
A cron is a scheduled job: like a service, but instead of running continuously it runs a command on a recurring schedule. Each execution is a tracked run with its own logs and metrics.
build "worker" {
base = "node"
command = "npm run build"
}
cron "daily-cleanup" {
build = build.worker
command = "npm run cleanup"
schedule = "0 2 * * *" # 02:00 UTC every day
env = {
DATABASE_URL = postgres.main.url
}
}
postgres "main" {}
A cron is shaped like a service: it has its own build (or image), command, env, and root. To share code between a long-running service and a cron, point both at the same build block.
Configuration
schedule(required) - when to run. Either a standard 5-field cron expression (minute hour day-of-month month day-of-week, interpreted in UTC) or a macro:@hourly,@daily,@weekly,@monthly,@yearly.build- reference to abuildblock. Mutually exclusive withimage.image- a pre-existing container image. Mutually exclusive withbuild.command- the command to run on each tick. Required when usingbuild.root- working directory for the command, relative tospecific.hcl. Defaults to the referenced build’sroot.env- environment variables, using the same references as services (postgres.*,secret.*,config.*,service.<name>.private_url, interpolated strings, and so on).dev- a{ command, env }block to override the command and env when running the cron locally.
Crons don’t serve network traffic, so they cannot declare endpoint, volume, or health_check blocks.
Schedule examples
schedule = "0 2 * * *" # 02:00 every day
schedule = "*/15 * * * *" # every 15 minutes
schedule = "0 9 * * 1" # 09:00 every Monday
schedule = "@hourly" # top of every hour
schedule = "@daily" # midnight every day
Image-based crons
A cron can run a pre-existing image instead of a build:
cron "hourly-sync" {
image = "python:3.12"
command = "python /app/sync.py"
schedule = "@hourly"
}
Local development
Crons do not fire on their schedule during specific dev, since that would be surprising while iterating. Instead, trigger a cron manually, exactly like a one-off command:
specific exec daily-cleanup # runs the cron's own command
specific exec daily-cleanup -- npm run cleanup:dry-run # run a different command
The command runs with the cron’s resolved environment variables and working directory, with databases and other resources started, the same way specific exec <service> works. Crons are also listed in the local dashboard (specific dev prints its URL) under Crons.
Production
When deployed, each cron runs on its schedule in the cloud. Every execution is a tracked run:
- Runs are visible in the dashboard under Crons → (cron) → runs.
- Each run has its own logs and CPU/memory metrics, collected the same way as services.
- Overlapping runs are allowed - if a run is still going when the next is due, the next one starts anyway.
- You can trigger a run on demand with the Run now button.