Your cron jobs
belong in Git

Define every job, channel, and alerting rule in a YAML manifest. Review changes in PRs. The CLI reconciles your account to match — no click-ops, no drift, no surprises.

Free tier — no credit card required.

The workflow

# 1. Adopt existing jobs into a manifest
$ steadycron export --namespace prod > manifests/prod.yaml

# 2. Edit, add jobs, review
$ git diff manifests/prod.yaml

# 3. Preview — like terraform plan
$ steadycron plan manifests/prod.yaml
  ~ weekly-digest   update  retries: 2 → 3
  + invoice-cron    create

# 4. Apply — transactional
$ steadycron sync manifests/prod.yaml

manifests/prod.yaml

namespace: production

channels:
  - id: ops-slack
    kind: slack
    webhook: ${SLACK_WEBHOOK_URL}

jobs:
  - id: weekly-digest
    name: Weekly digest email
    kind: http
    method: POST
    url: https://api.myapp.com/jobs/digest
    schedule: "0 9 * * 1"
    timezone: Europe/Berlin
    timeout: 120
    retries: 3
    channel: ops-slack

  - id: nightly-backup
    name: Nightly DB backup
    kind: heartbeat
    schedule: "0 2 * * *"
    grace: 1800

Declare everything as code

Not just jobs. Channels, rules, tags, and variables — your entire SteadyCron configuration in one version-controlled file.

Jobs

HTTP jobs (call your endpoint) and heartbeat checks (monitor your own cron) — both in one manifest.

kind: http | heartbeat

Alert channels

Declare Slack, Discord, email, Telegram, and webhook channels once — reference them by id from any job.

kind: slack | discord | email

Alerting rules

Define when to page — threshold, events (failure, missed, recovery), escalation — as code, not click-ops.

on: [failure, missed]

Tags & variables

Group jobs by team or environment with tags. Store environment-specific values in variables — substituted at apply time, never committed.

tags: [env, team]

Rename a job. Keep the ping URL.

Each job has a stable id that never changes. Rename the display name freely — the underlying identity is preserved, so heartbeat ping URLs stay intact and your scripts don't break.

The id is also how plan knows an update is an update — not a delete + create.

Learn more about stable IDs

Safe rename — id stays the same

- id: nightly-backup              # ← stable forever
- name: Nightly DB backup         # ← old name
+ name: Nightly database backup   # ← renamed freely
  kind: heartbeat
  schedule: "0 2 * * *"
  grace: 1800

# Ping URL unchanged:
https://ping.steadycron.com/{nightly-backup-token}

Multi-environment manifests

manifests/
  production.yaml   # namespace: production
  staging.yaml      # namespace: staging
  workers.yaml      # namespace: workers-team

# Apply each independently
steadycron apply --prune manifests/production.yaml
steadycron apply --prune manifests/staging.yaml

IaC and dashboard coexist

A namespace scopes what the CLI manages. With --prune, only resources inside that namespace are removed — dashboard-created jobs in the default namespace are never touched.

Use one manifest per environment or team. Apply them independently from CI. Each namespace is an isolated blast radius.

Namespaces & ownership docs

Secrets never land in git

Two mechanisms, each the right tool for the job.

$

${ENV} — CLI substitution

The CLI reads environment variables at apply time and substitutes them before calling the API. Works in any field. Use for API keys, webhook URLs, and environment-specific values.

channels:
  - id: ops-slack
    kind: slack
    webhook: ${SLACK_WEBHOOK_URL}
{{

{{var}} — server-side template

Resolved by SteadyCron at execution time, not apply time. Works in HTTP job url, headers, and body. Use for dynamic per-execution values.

jobs:
  - id: data-export
    kind: http
    url: https://api.myapp.com/export/{{date}}
    body: '{"run_id": "{{uuid}}"}'

steadycron export emits both kinds of placeholder automatically — the exported file is safe to commit.

Diff your cron changes in every PR

steadycron plan shows exactly what would change before anything is written — like terraform plan.

Wire it to your GitHub Actions PR pipeline and the plan diff appears as a PR comment on every change to your manifests. Reviewers see exactly which jobs will be created, updated, or removed.

Set up CI/CD

$ steadycron plan manifests/prod.yaml

  ~ weekly-digest    update  retries: 2  3
  + invoice-reminder create  http  0 17 * * 5
  - old-report       (not in manifest — use --prune)

  1 to create  ·  1 to update  ·  1 pending deletion

Why SteadyCron for cron as code

Execution and monitoring in one manifest

HTTP jobs that call your endpoints and heartbeat checks that watch your own cron — declared together, managed together.

EU-hosted, GDPR-native

Runs on Hetzner in Germany, governed by German law. No US sub-processors for cron execution or job data.

Standard developer workflow

YAML manifest, dedicated CLI, GitHub Action, plan diffs in PRs. The same workflow as your infrastructure — applied to cron.

Common questions

Does managing jobs as code replace the dashboard?

No. Namespaces let IaC-managed jobs and dashboard-created jobs coexist in the same account. Only resources inside your manifest's namespace are touched by the CLI — everything else is left alone.

What happens to my heartbeat ping URL when I rename a job?

Nothing changes. Each job's ping URL is tied to its stable id field, not its display name. You can rename a job freely without updating any of your scripts or monitoring integrations.

Can I use this in my CI/CD pipeline?

Yes — that's the recommended workflow. Run steadycron plan on pull requests to post the diff as a comment, and steadycron apply --prune on merge. See the CI/CD setup guide for a ready-to-copy GitHub Actions workflow.

Are my API keys or secrets ever stored in the manifest?

No. Use ${ENV_VAR_NAME} placeholders anywhere in the manifest. The CLI substitutes them from your process environment at apply time — the file itself never contains secrets. steadycron export emits placeholders automatically.

Can I manage multiple environments from one repo?

Yes. Use a separate manifest file per environment, each with its own namespace. Apply them independently from CI — only the resources inside that namespace are affected.

Start with this manifest

Copy it, edit the URLs, and run steadycron sync.

namespace: my-app

channels:
  - id: alerts
    kind: slack
    webhook: ${SLACK_WEBHOOK_URL}

jobs:
  - id: my-scheduled-job
    name: My scheduled job
    kind: http
    method: POST
    url: https://api.myapp.com/jobs/my-task
    schedule: "0 9 * * 1-5"
    timezone: UTC
    timeout: 60
    retries: 2
    channel: alerts

  - id: my-heartbeat
    name: My heartbeat check
    kind: heartbeat
    schedule: "*/5 * * * *"
    grace: 300

Cron jobs that live in your repo

Sign up, export your current account, and commit your first manifest in minutes.

  • Free tier — no credit card
  • EU-hosted, GDPR-native
  • Execution + monitoring in one manifest