Skip to main content
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.
specific.hcl
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 a build block. Mutually exclusive with image.
  • image - a pre-existing container image. Mutually exclusive with build.
  • command - the command to run on each tick. Required when using build.
  • root - working directory for the command, relative to specific.hcl. Defaults to the referenced build’s root.
  • 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:
specific.hcl
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.