Jobs
HTTP jobs (call your endpoint) and heartbeat checks (monitor your own cron) — both in one manifest.
kind: http | heartbeat Cron as Code
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 One manifest, your whole account
Not just jobs. Channels, rules, tags, and variables — your entire SteadyCron configuration in one version-controlled file.
HTTP jobs (call your endpoint) and heartbeat checks (monitor your own cron) — both in one manifest.
kind: http | heartbeat Declare Slack, Discord, email, Telegram, and webhook channels once — reference them by id from any job.
kind: slack | discord | email Define when to page — threshold, events (failure, missed, recovery), escalation — as code, not click-ops.
on: [failure, missed] Group jobs by team or environment with tags. Store environment-specific values in variables — substituted at apply time, never committed.
tags: [env, team] Rename-safe stable ids
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.
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 Namespaces
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 docsSecrets model
Two mechanisms, each the right tool for the job.
${ENV} — CLI substitutionThe 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.
Plan before you push
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 HTTP jobs that call your endpoints and heartbeat checks that watch your own cron — declared together, managed together.
Runs on Hetzner in Germany, governed by German law. No US sub-processors for cron execution or job data.
YAML manifest, dedicated CLI, GitHub Action, plan diffs in PRs. The same workflow as your infrastructure — applied to cron.
FAQ
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.
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.
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.
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.
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.
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 Sign up, export your current account, and commit your first manifest in minutes.