{"id":51412788,"url":"https://github.com/steadycron/cli","last_synced_at":"2026-07-04T16:00:41.308Z","repository":{"id":363884999,"uuid":"1234408580","full_name":"steadycron/cli","owner":"steadycron","description":"Official CLI for SteadyCron — manage cron jobs and heartbeat monitors as code from a YAML manifest.","archived":false,"fork":false,"pushed_at":"2026-07-01T18:27:01.000Z","size":357,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-01T19:23:44.743Z","etag":null,"topics":["cronjob","cronjob-scheduler","devops","dotnet-tool","gitops","heartbeat","monitoring"],"latest_commit_sha":null,"homepage":"https://steadycron.com","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/steadycron.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-10T06:26:55.000Z","updated_at":"2026-07-01T18:27:04.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/steadycron/cli","commit_stats":null,"previous_names":["steadycron/cli"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/steadycron/cli","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steadycron%2Fcli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steadycron%2Fcli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steadycron%2Fcli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steadycron%2Fcli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/steadycron","download_url":"https://codeload.github.com/steadycron/cli/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/steadycron%2Fcli/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35127443,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cronjob","cronjob-scheduler","devops","dotnet-tool","gitops","heartbeat","monitoring"],"created_at":"2026-07-04T16:00:27.010Z","updated_at":"2026-07-04T16:00:41.301Z","avatar_url":"https://github.com/steadycron.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SteadyCron CLI\n\n[![NuGet](https://img.shields.io/nuget/v/steadycron.svg?logo=nuget)](https://www.nuget.org/packages/steadycron)\n[![CI](https://github.com/steadycron/cli/actions/workflows/ci.yml/badge.svg)](https://github.com/steadycron/cli/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/steadycron/cli/blob/main/LICENSE)\n\nThe official command-line interface for [SteadyCron](https://steadycron.com) — schedule, run, and\nmonitor cron jobs as code. Everything works from the terminal: account creation, your first\nmonitored job, day-to-day management, and a full manifest-as-code workflow. No dashboard required.\n\n```bash\ndotnet tool install -g steadycron\nsteadycron signup\nsteadycron init\n```\n\n## Contents\n\n- [Quickstart](#quickstart)\n- [Install](#install)\n- [Authenticate](#authenticate)\n- [Your first job: `init`](#your-first-job-init)\n- [Day-to-day commands](#day-to-day-commands)\n- [Cron as code](#cron-as-code)\n  - [The v2 manifest](#the-v2-manifest)\n  - [Interpolation, secrets, and `.env` files](#two-interpolation-mechanisms)\n  - [Workflow: validate → plan → apply](#workflow-validate--plan--apply)\n  - [`export` and restoring to another account](#export--pull-the-current-account-state-as-a-manifest)\n  - [`manifest scaffold`](#manifest-scaffold--boilerplate-manifest-generator)\n  - [`manifest add`](#manifest-add--append-a-resource-to-an-existing-manifest)\n  - [Importing existing schedules](#importing-existing-schedules)\n  - [CI: plan on PRs, apply on merge](#cron-and-monitoring-as-code-in-ci)\n- [Activity reports](#activity-reports)\n- [Logbook](#logbook)\n- [Exit codes](#exit-codes)\n- [Development](#development)\n\n## Quickstart\n\nFrom nothing to a protected cron job, entirely in your terminal:\n\n```\n$ dotnet tool install -g steadycron\n\n$ steadycron signup\nEmail: dan@example.com\nPassword: ********\n✓ Account created. Verification code sent to dan@example.com.\nEnter 6-digit code: 482913\n✓ Email verified.\n✓ API key created (cli-dans-laptop, scope: full) → saved to ~/.config/steadycron/config.json\n✓ Default email alert channel set up for dan@example.com.\n\nNext: steadycron init — create your first monitored job.\n\n$ steadycron init\n? What do you want to do?\n  › Monitor an existing cron job (heartbeat)\n    Schedule a new HTTP job (we call your URL)\n    Skip — just set up the manifest workflow\n\n? Job name: nightly-backup\n? Expected schedule (cron) [*/15 * * * *]: 0 2 * * *\n? Timezone:\n  › UTC\n    Local (Europe/Berlin)\n    Other…\n  Next fires: 2026-07-05 02:00, 2026-07-06 02:00, 2026-07-07 02:00 (UTC)\n? Grace period seconds [3600]:\n\n✓ Created heartbeat monitor nightly-backup.\n✓ If a ping doesn't arrive on time, we'll email dan@example.com.\n\n┌────────────────┬───────────┬──────────────────┬────────────────┬──────────┐\n│ Name           │ Kind      │ Status           │ Schedule       │ Next run │\n├────────────────┼───────────┼──────────────────┼────────────────┼──────────┤\n│ nightly-backup │ heartbeat │ waiting for ping │ 0 2 * * * UTC  │ —        │\n└────────────────┴───────────┴──────────────────┴────────────────┴──────────┘\n1 job. Check status anytime: steadycron jobs get nightly-backup\n\nAdd this to the end of your cron command:\n\n    \u0026\u0026 curl -fsS https://ping.steadycron.com/\u003ctoken\u003e\n\nExample crontab line:\n    0 2 * * *  /usr/local/bin/backup.sh \u0026\u0026 curl -fsS https://ping.steadycron.com/\u003ctoken\u003e\nOther schedulers (systemd, Docker, Kubernetes, Windows): https://steadycron.com/docs/ping-recipes\n\n✓ Wrote steadycron.yaml — your account as code (1 job, 1 channel, 1 rule)\n✓ Wrote steadycron_example.yaml — reference for every manifest feature\n\nManage everything as code:\n  1. Add or edit jobs in steadycron.yaml\n  2. steadycron validate steadycron.yaml    check locally, no API\n  3. steadycron plan steadycron.yaml        preview changes\n  4. steadycron apply steadycron.yaml       sync to your account\n```\n\nThat's the whole product in one session: a monitored job, alerting to your inbox, and your\naccount exported as a version-controllable manifest.\n\nAlready have an account? `steadycron login` mints a fresh key for this machine without touching\nany other machine's key — see [Authenticate](#authenticate).\n\nFrom here, either keep managing resources one-off ([Day-to-day commands](#day-to-day-commands))\nor commit `steadycron.yaml` and manage everything through `plan`/`apply`\n([Cron as code](#cron-as-code)). Both operate on the same account — mix them freely.\n\n## Install\n\n### As a .NET global tool (recommended)\n\nRequires the [.NET 10 runtime](https://dotnet.microsoft.com/download).\n\n```bash\ndotnet tool install -g steadycron\nsteadycron --version\n```\n\nUpdate with `dotnet tool update -g steadycron`.\n\n### Self-contained binary\n\nDownload the single-file binary for your platform from the\n[Releases](https://github.com/steadycron/cli/releases) page — no .NET runtime required:\n\n```bash\n# example: Linux x64\ncurl -Lo steadycron https://github.com/steadycron/cli/releases/latest/download/steadycron-linux-x64\nchmod +x steadycron \u0026\u0026 sudo mv steadycron /usr/local/bin/\n```\n\nBinaries are available for Linux (x64, arm64), macOS (x64, arm64), and Windows (x64).\n\n## Authenticate\n\nNew to SteadyCron? Create an account, verify your email, and provision an API key — entirely\nin-terminal:\n\n```bash\nsteadycron signup\n```\n\nThe full transcript is in the [Quickstart](#quickstart). What it does: creates the account,\nverifies your email with a 6-digit code, mints a full-scope API key named `cli-\u003chostname\u003e`, saves\nit to the config file, and sets up a default email alert channel pointing at your address.\n\n**Config file location:** `~/.config/steadycron/config.json` on Linux/macOS,\n`%APPDATA%\\steadycron\\config.json` on Windows.\n\nAlready have an account? Sign in on a new machine:\n\n```bash\nsteadycron login\n```\n\n`login` mints a fresh key for this machine and never touches or reveals any other machine's key.\nStale `cli-*` keys can be revoked from the dashboard if you ever want to clean up.\n\nPrefer to create a key from the dashboard instead? Create one under **Settings → API keys**, then\nprovide it via an environment variable:\n\n```bash\nexport STEADYCRON_API_KEY=sc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nConfiguration is resolved in this order (first wins):\n\n1. `--api-key` / `--api-url` flags\n2. `STEADYCRON_API_KEY` / `STEADYCRON_API_URL` environment variables\n3. The config file (`steadycron config set --api-key sc_...`)\n4. Built-in defaults (`--api-url` defaults to `https://api.steadycron.com`)\n\n```bash\nsteadycron config show --check   # verify connectivity\n```\n\n\u003e A **read-only** key can run `export`, `validate`, and read-only sub-commands.\n\u003e Mutating commands (`apply`, `sync`, `jobs create/pause/delete`, etc.) require a **full** key.\n\n## Your first job: `init`\n\n`steadycron init` is an interactive wizard that creates your first monitored job — a heartbeat\nmonitor for an existing cron job, or a new HTTP job SteadyCron calls on a schedule — plus a\ndefault alert rule. The full transcript is in the [Quickstart](#quickstart).\n\nWhat the wizard gives you:\n\n- **Sensible defaults everywhere** — schedule prefills `*/15 * * * *`, the grace period is derived\n  from your schedule, and timezone is a selector (UTC / your local timezone / manual entry).\n  Enter-through works for a quick test job.\n- **A next-fires preview** in your chosen timezone before anything is created.\n- **Plain-language alerting** — the default rule emails your verified address on a missed ping\n  (heartbeat) or failed run (HTTP). Add more channels later with `channels create` + `rules add`.\n- **The ping snippet** for heartbeats, with a platform-matched scheduler example and a pointer to\n  [ping recipes](https://steadycron.com/docs/ping-recipes) for systemd, Docker, Kubernetes, and\n  Windows Task Scheduler.\n- **Two manifest files** written to the current directory (never overwriting existing files):\n  - `steadycron.yaml` — a live export of your account, immediately usable with `plan`/`apply`\n  - `steadycron_example.yaml` — a fully commented reference covering every manifest feature\n- **A README badge snippet** for the job — markdown you can paste straight into your repo's README.\n- **A `.gitignore` guard**, when the current directory is a git repo: appends `secrets.env` and\n  `*.env` so a stray secrets file can never be committed by accident.\n- **A GitHub Actions opt-in**, when the current directory looks like a GitHub-hosted repo (a\n  `.github/` folder, or a `github.com` remote): offers to write\n  `.github/workflows/steadycron.yml` — the same plan-on-PR/apply-on-merge workflow documented\n  [below](#cron-and-monitoring-as-code-in-ci), pre-wired to `steadycron.yaml`.\n\nThe third wizard option, **Skip**, creates nothing but still writes both manifest files (and offers\nthe `.gitignore`/CI setup) — the fastest way to start manifest-first on an empty account.\n\n`init` requires a configured API key (`signup`/`login`/`config set` first) and an interactive\nterminal. Run it again anytime to add another job, or use\n[`steadycron manifest add job`](#manifest-add--append-a-resource-to-an-existing-manifest) to add\nthe next one manifest-first, without touching the API at all.\n\n## Day-to-day commands\n\nCommands that accept a `\u003cJOB\u003e` argument resolve it in order: GUID → job key → exact name. Job\nkeys are shown in `jobs list` (Key column) and are stable across renames — prefer them over\nnames in scripts.\n\n```bash\nsteadycron jobs list                          # table of all jobs with their job keys\nsteadycron jobs list --kind heartbeat --status missed\nsteadycron jobs get my-job-key                # by job key (preferred), name, or id\nsteadycron jobs logs my-job-key -n 20\nsteadycron jobs pause my-job-key\nsteadycron jobs resume my-job-key\nsteadycron jobs run my-job-key\nsteadycron jobs delete old-job-key --yes\n\nsteadycron jobs create --name warm-cache --url https://api.myapp.com/warm \\\n  --method GET --interval 900 --skip-if-running\n\nsteadycron cron preview \"*/15 9-17 * * 1-5\" --timezone Europe/Berlin\n\nsteadycron tags list\nsteadycron tags create env prod --color green\n\nsteadycron vars list\nsteadycron vars set digest_token \"sk_live_…\"\n\nsteadycron channels list\nsteadycron channels create --name \"Ops email\" --kind email --to ops@example.com\n\nsteadycron rules list nightly-db-backup        # shows trigger, severity, channel kind and target\nsteadycron rules add nightly-db-backup \\\n  --channel \"Ops email\" --trigger missed_heartbeat --severity p1\nsteadycron rules test nightly-db-backup        # fires a test alert on every channel for the job\nsteadycron rules delete \u003crule-id\u003e\n```\n\n`rules test` sends one notification per unique channel (even if multiple triggers point at the\nsame channel) and exits non-zero if any delivery fails — useful for verifying a new channel\nconfiguration from CI or a script.\n\nAdd `--json` to any command for machine-readable output.\n\n## Cron as code\n\nOne-off commands are fine for getting started; the manifest is how you run SteadyCron for real.\nDeclare your entire account — jobs, heartbeat monitors, alert channels, tags, variables — in a\nYAML file, commit it, and reconcile with a single command:\n\n```bash\nsteadycron sync steadycron.yaml --namespace prod\n```\n\nYou already have a starting manifest: `init` wrote `steadycron.yaml` (your live account) and\n`steadycron_example.yaml` (annotated reference). Or generate either from scratch — see\n[`export`](#export--pull-the-current-account-state-as-a-manifest) and\n[`manifest scaffold`](#manifest-scaffold--boilerplate-manifest-generator).\n\n### The v2 manifest\n\nA manifest declares your whole account as code. The server reconciles from this single source of\ntruth — every schedule, alert rule, and monitoring configuration is version-controlled and\nreviewable in a pull request.\n\n```yaml\n# examples/steadycron.yaml\nversion: 2\nnamespace: prod   # required for --prune\n\nchannels:\n  - id: slack-oncall\n    name: Slack #oncall\n    kind: slack\n    config:\n      webhook_url: ${SLACK_WEBHOOK_URL}   # CLI env-var substitution\n\ntags:\n  - id: env-prod\n    key: env\n    value: prod\n\nvariables:\n  - id: digest-token\n    name: digest_token        # used in HTTP fields as {{digest_token}}\n    value: ${DIGEST_TOKEN}    # value resolved at load time, never committed\n\njobs:\n  - id: weekly-digest          # stable id — rename the job without re-creating it\n    name: weekly-digest-email\n    kind: http\n    method: POST\n    url: ${API_BASE_URL}/jobs/digest\n    schedule: \"0 9 * * 1\"     # Mondays at 09:00\n    timezone: Europe/Berlin\n    timeout: 120\n    retries: 3\n    headers:\n      Authorization: \"Bearer {{digest_token}}\"   # server-side template substitution\n    tags: [\"env:prod\"]\n    rules:\n      - channel: slack-oncall\n        trigger: on_failure\n        severity: p1\n\n  - id: nightly-db-backup\n    name: nightly-db-backup\n    kind: heartbeat\n    schedule: \"0 2 * * *\"\n    grace: 1800\n```\n\nSee [`examples/steadycron.yaml`](examples/steadycron.yaml) for the complete field reference, or\nrun `steadycron manifest scaffold` for a fully commented boilerplate.\n\n### Two interpolation mechanisms\n\n| Syntax | Where it runs | Scope |\n|---|---|---|\n| `${ENV_VAR}` and `${ENV_VAR:-default}` | **CLI, at load time** | Any manifest field |\n| `{{template_var}}` | **Server, at execution time** | HTTP job URL / headers / body only |\n\nThe CLI resolves `${...}` before sending the manifest to the API. `{{...}}` is passed through\nuntouched and substituted by the server when the job fires.\n\n### Secrets and `.env` files\n\nSecret fields never leave the server in plaintext: on `export` they come back as `${SC_…}`\nplaceholders (alert-channel credentials such as `webhook_url`/`bot_token`/`secret`/webhook headers,\nand **template-variable values**). To `apply` such a manifest you must supply those values.\n\n`steadycron init` writes `steadycron_secrets.env` for you — a scaffold listing every secret your\naccount's manifest actually references (empty until you add a secret-bearing channel or variable),\nand adds it to `.gitignore` automatically. `apply`/`sync`/`plan`/`validate` pick it up with **no\nflag needed**, as long as it's in the current directory:\n\n```bash\nsteadycron init                    # writes steadycron.yaml + steadycron_secrets.env (gitignored)\n#  ... fill in steadycron_secrets.env with real values ...\nsteadycron apply                   # uses steadycron.yaml + steadycron_secrets.env automatically\n```\n\nFor anything else — a different filename, multiple files, restoring to a different account — pass\none or more `--env-file` flags explicitly (repeatable; an explicit flag always fully overrides the\ndefault file; values take precedence over the process environment):\n\n```bash\nsteadycron apply production.yaml --namespace prod --env-file secrets.env\n```\n\nWhen a manifest references any required `${...}` placeholder and neither the default file nor an\nexplicit `--env-file` resolves it, `apply`/`sync`/`plan` **refuse to run** — pass `--env-file`, or\n`--allow-process-env` to source the values from the current environment instead (e.g. CI that\ninjects secrets as env vars). `validate` supports the same `--env-file`/auto-detection but never\nenforces this (it's a local, read-only lint).\n\n### Restoring to another account\n\n`export` + `apply` move a whole account — jobs, channels, tags, **variable values**, rules — to a\nfresh one over the CLI, no UI required:\n\n```bash\n# 1. On the source account: export the manifest and a scaffold of the secrets it needs.\nsteadycron export -o production.yaml --write-env secrets.env\n\n# 2. Fill in secrets.env with the real values (it lists every ${SC_…} the manifest references).\n\n# 3. On the target account (different API key): apply, sourcing the secrets from the file.\nsteadycron apply production.yaml --namespace prod --prune --env-file secrets.env\n```\n\nThe server recreates every resource and sets channel credentials and variable values from the\nplaceholders your `.env` resolves. (Variable-value round-trip requires a server that supports it;\nolder servers export variable names only.)\n\n### v1 manifests (deprecated)\n\nVersion 1 (jobs-only, name-keyed, no namespace/channels/tags/variables) is still accepted. The CLI\nprints a deprecation warning and recommends upgrading:\n\n```\n⚠ Manifest version 1 is deprecated. Run 'steadycron export' to upgrade to v2.\n```\n\nSupport will be removed no earlier than two minor releases after this notice.\n\n## Workflow: validate → plan → apply\n\n### `validate` — lint locally, no API call\n\n```bash\nsteadycron validate steadycron.yaml\nsteadycron validate ./manifests/\n```\n\nChecks schema, cron syntax, cross-references (job tags → declared tags, rule channels → declared\nchannels), duplicate IDs, and kind-specific field constraints. Fast CI gate — runs in milliseconds.\nExits **0** on success, **2** on errors.\n\n### `plan` — preview what would change\n\n```bash\nsteadycron plan steadycron.yaml --namespace prod\nsteadycron plan ./manifests/ --namespace prod --output json   # machine-readable server plan\nsteadycron plan steadycron.yaml --namespace prod --detailed-exitcode\n```\n\nCalls the server's `/api/reconcile` dry-run and renders its authoritative plan. The server is the\nsingle source of truth — the CLI never computes its own diff.\n\n`--detailed-exitcode` exits **2** when drift is detected (Terraform-style), **0** when clean.\nWithout the flag, any plan exits **0** unless there are errors.\n\n### `sync` — plan + apply\n\n```bash\n# Interactive: shows plan, prompts to confirm, then applies\nsteadycron sync steadycron.yaml --namespace prod\n\n# Non-interactive (CI): applies without prompt\nsteadycron sync steadycron.yaml --namespace prod --yes\n\n# Include --prune to delete server resources removed from the manifest\nsteadycron sync ./manifests/ --namespace prod --prune --yes\n```\n\n`sync` is declarative: it **creates** new resources, **updates** changed ones, and (with `--prune`)\n**deletes** ones missing from the manifest. Without `--prune`, orphaned server resources are reported\nbut not deleted.\n\n### `apply` — alias for `sync --yes`\n\n```bash\nsteadycron apply ./manifests/ --namespace prod --prune\n```\n\nApplies immediately without prompting. Typical use: CI pipelines on merge to the default branch.\n\n### `export` — pull the current account state as a manifest\n\n```bash\nsteadycron export -o steadycron.yaml                     # whole account → file\nsteadycron export -o steadycron.yaml --write-env         # + steadycron_secrets.env scaffold for its secrets\nsteadycron export --scope jobs -o jobs.yaml              # jobs only\nsteadycron export --scope job weekly-digest-email        # single job → stdout\nsteadycron export --format json                          # JSON instead of YAML\n```\n\nWrites the manifest verbatim from the server. Secret fields — alert-channel credentials\n(`webhook_url`, `bot_token`, `secret`, webhook headers) **and template-variable values** — are\nreplaced with `${SC_…}` placeholders, never plaintext. The CLI prints a summary of the required\nenvironment variables to stderr (so piping with `-o` stays clean), and the manifest itself carries a\n`# required env vars:` header block.\n\n`--write-env [path]` writes a ready-to-fill `.env` scaffold listing every referenced secret\n(refuses to overwrite an existing file) — defaults to `steadycron_secrets.env` when no path is\ngiven, which `apply`/`sync`/`plan`/`validate` then pick up with no flag needed. Pass an explicit\npath (e.g. `--write-env secrets.env`) to use a different name.\n\nUseful for bootstrapping: export your current account, commit the result, and manage it as code\ngoing forward. To move an account to a new one, see [Restoring to another account](#restoring-to-another-account).\n\n### Multi-file manifests\n\nSeparate concerns across files or directories:\n\n```bash\nsteadycron validate ./manifests/\nsteadycron plan ./manifests/ --namespace prod\nsteadycron apply manifests/channels.yaml manifests/jobs.yaml --namespace prod\n```\n\nWhen multiple files are used, they must agree on `version` and `namespace`. Duplicate resource `id`\nvalues across files are an error.\n\n### `manifest scaffold` — boilerplate manifest generator\n\n`steadycron manifest scaffold` generates a fully documented boilerplate manifest (or Terraform\nHCL) that covers every SteadyCron feature. Use it to get started without reading the docs or\ncreating resources in the dashboard first.\n\n```bash\nsteadycron manifest scaffold                          # print documented YAML to stdout\nsteadycron manifest scaffold -o steadycron.yaml       # write to file (refuses to overwrite)\nsteadycron manifest scaffold --terraform              # Terraform HCL instead of YAML\nsteadycron manifest scaffold --terraform -o main.tf   # write Terraform boilerplate to file\n```\n\nThe generated manifest includes a namespace, template variables with `${ENV_VAR}` explanation,\ntags, all five alert channel kinds (email, Slack, webhook, Discord, Telegram), a fully annotated\nHTTP job (every field), a heartbeat monitor, and inline alert rules. Every field has a comment\nexplaining what it does and listing valid values. Delete the sections you don't need, then run\n`steadycron plan steadycron.yaml` to preview.\n\nThe Terraform boilerplate uses the exact resource and attribute names from the live\n`steadycron/steadycron` provider — `steadycron_http_job`, `steadycron_heartbeat_monitor`,\n`steadycron_alert_channel`, `steadycron_alert_rule`, `steadycron_tag`, and\n`steadycron_template_variable` — so it applies out of the box with `terraform init \u0026\u0026 terraform apply`.\n\n`manifest scaffold` requires no API key and no configuration — run it any time.\n\n### `manifest add` — append a resource to an existing manifest\n\n`steadycron manifest add \u003cresource\u003e` appends a single job, channel, tag, or template variable to\nan existing manifest — the manifest-first way to grow `steadycron.yaml` over time, instead of\nhand-editing YAML or creating resources imperatively with `jobs create`/`channels create`. Like\n`manifest scaffold`, it never calls the API and never requires a key: it edits the file directly,\nadditively, and only after validating the result.\n\n```bash\n# Fully via flags — no prompts, safe for scripts\nsteadycron manifest add job --kind heartbeat --name nightly-backup --schedule \"0 2 * * *\"\nsteadycron manifest add channel --kind slack --name ops-slack\nsteadycron manifest add tag env staging --color yellow\nsteadycron manifest add variable api_token\n```\n\nMissing values are prompted for interactively — schedule prefills `*/15 * * * *`, timezone is the\nsame UTC/local/manual selector `init` uses, and the grace period default is derived from the\nschedule, all computed locally (no API call):\n\n```\n$ steadycron manifest add job\n? Kind:\n  › heartbeat\n    http\nJob name: nightly-backup\nSchedule (cron) [*/15 * * * *]: ⏎\n? Timezone:\n  › UTC\n    Local (Europe/Berlin)\n    Other…\nGrace period seconds [1800]: ⏎\n✓ Added job nightly-backup to steadycron.yaml\nPreview: steadycron plan steadycron.yaml\n```\n\nComments and formatting outside the inserted block are never touched — `add` locates the right\nsection (creating it if absent, or the file itself if it doesn't exist yet) and appends after the\nlast existing item. The candidate result is validated before anything is written; a failure (or a\nduplicate `id`/`name`) leaves the original file byte-identical and exits non-zero.\n\nEvery secret-bearing field — channel webhook URLs, bot tokens, template variable values — is\nalways emitted as an `${ENV_VAR}` placeholder with an explanatory comment. `add` never prompts for\na secret value, so nothing you type ever ends up committed to the manifest in plaintext.\n\n`--dry-run` prints the generated block without writing anything; `-f/--file \u003cpath\u003e` targets a\nmanifest other than `steadycron.yaml`; `steadycron manifest g` is the short alias. `--terraform` is\nreserved for a future release and currently exits with an error.\n\n## Importing existing schedules\n\n`import` generates a v2 manifest from a crontab or `vercel.json` file — client-side only, no API\ncalls. Review the result, then `sync` it.\n\n### `import crontab` — migrate from a crontab\n\n```bash\n# Import a crontab file\nsteadycron import crontab /etc/cron.d/myjobs -o steadycron.yaml\n\n# Import from stdin (e.g. from `crontab -l`)\ncrontab -l | steadycron import crontab -o steadycron.yaml\n\n# Preview what would be imported without writing anything\nsteadycron import crontab mycron.txt --dry-run\n\n# System crontab (extra username column): auto-detected, or force with --system\nsteadycron import crontab /etc/cron.d/myjobs --system -o steadycron.yaml\n\n# Force all entries to a specific kind\nsteadycron import crontab mycron.txt --as heartbeat -o monitors.yaml\n```\n\n**Job kind mapping (`--as auto` default):**\n\n| Command type | Becomes |\n|---|---|\n| `curl`/`wget`/bare `https://…` URL | `http` job — URL, method, headers, body extracted |\n| Anything else | `heartbeat` monitor |\n\n`--as http` forces http for every entry (skips with a warning if no URL can be extracted).\n`--as heartbeat` forces heartbeat regardless of the command.\n\n**crontab conventions handled:**\n\n- `MAILTO=`, `PATH=`, and other env-assignment lines are ignored.\n- A `# comment` immediately above an entry becomes the job `name` (blank line clears it).\n- Macros: `@hourly`, `@daily`/`@midnight`, `@weekly`, `@monthly`, `@yearly`/`@annually` are\n  expanded to their 5-field equivalents.\n- `@reboot` is skipped with an actionable warning (no equivalent schedule exists).\n- System crontab / `cron.d` format (6th field is a username) is auto-detected or set with `--system`.\n\n**Heartbeat monitors — post-sync step:**\n\nWhen a command is imported as a heartbeat, the CLI prints the ping snippet you must append to that\ncron command (the ping token only exists after `sync` creates the monitor):\n\n```\n! Heartbeat nightly-backup (id: nightly-backup)\n   After sync, append this to your cron command:\n   \u0026\u0026 curl -fsS 'https://ping.steadycron.com/\u003cTOKEN\u003e'\n   (\u003cTOKEN\u003e available after: steadycron jobs get nightly-backup)\n```\n\n**Note on timezone:** crontab entries run in the server's local timezone. All imported entries get\n`timezone: UTC` as a safe default. Edit the `timezone` field in the manifest before `sync` if your\ncron server runs in a different timezone.\n\n### `import vercel` — migrate from Vercel cron jobs\n\nVercel's hobby plan limits crons to once per day in UTC. Migrating to SteadyCron removes both\nrestrictions.\n\n```bash\n# Basic import (--base-url required)\nsteadycron import vercel --base-url https://app.example.com -o steadycron.yaml\n\n# With a cron secret (added as Authorization header; never inlined)\nsteadycron import vercel \\\n  --base-url https://app.example.com \\\n  --cron-secret-env VERCEL_CRON_SECRET \\\n  -o steadycron.yaml\n\n# Preview without writing\nsteadycron import vercel --base-url https://app.example.com --dry-run\n```\n\nThe importer reads `vercel.json` in the current directory (or pass an explicit path as the first\nargument). For each cron entry, it emits an `http GET` job with the full URL (`--base-url` + path)\nand `timezone: UTC`.\n\n**`--cron-secret-env NAME`** — instead of inlining the secret value, the manifest emits:\n\n```yaml\nheaders:\n  Authorization: Bearer ${VERCEL_CRON_SECRET}\n```\n\nThe `${…}` placeholder is resolved at `sync` time via `--env-file` or the process environment.\nThe manifest itself never contains the secret.\n\n**End-to-end flow:**\n\n```bash\n# 1. Generate the manifest\nsteadycron import vercel \\\n  --base-url https://app.example.com \\\n  --cron-secret-env VERCEL_CRON_SECRET \\\n  -o steadycron.yaml\n\n# 2. Review the manifest\nsteadycron validate steadycron.yaml\n\n# 3. Set the secret and sync\necho \"VERCEL_CRON_SECRET=your-secret-here\" \u003e secrets.env\nsteadycron apply steadycron.yaml --env-file secrets.env --namespace prod\n```\n\nAfter migrating, you can tighten schedules and set per-job timezones — capabilities Vercel doesn't\nexpose.\n\n## Cron and monitoring as code in CI\n\nAdd the SteadyCron GitHub Action to plan on pull requests and apply on merge:\n\n```yaml\n# .github/workflows/steadycron.yml\nname: SteadyCron\n\non:\n  pull_request:\n    paths: [\"steadycron/**\"]\n  push:\n    branches: [main]\n    paths: [\"steadycron/**\"]\n\npermissions:\n  contents: read\n  pull-requests: write\n\njobs:\n  steadycron:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: steadycron/action@v1\n        with:\n          # plan on PRs, apply on push to main\n          command: ${{ github.event_name == 'pull_request' \u0026\u0026 'plan' || 'apply' }}\n          manifest: steadycron/\n          namespace: prod\n          prune: \"true\"\n          comment-on-pr: \"true\"\n        env:\n          STEADYCRON_API_KEY: ${{ secrets.STEADYCRON_API_KEY }}\n```\n\nThe API key is read from the `STEADYCRON_API_KEY` environment variable (a repository secret), not a\n`with:` input — this keeps it out of the action's logged inputs.\n\nThe action:\n1. Installs the CLI (pinned version).\n2. On `pull_request` — runs `steadycron plan --output json`, formats the plan as Markdown, and\n   **posts/updates a sticky PR comment** (find-and-replace so re-runs update in place). Fails the\n   check if the plan has errors (limit violations, conflicts, etc.).\n3. On `push` to the default branch — runs `steadycron apply --yes`.\n\nSee [`examples/ci/`](examples/ci/) for standalone `pull_request` and `push` workflow files.\n\n## Activity reports\n\n```bash\nsteadycron report                  # Overview of the last 24 h\nsteadycron report --hours 6        # shorter window\nsteadycron report --hours 168      # last 7 d (Developer plan and above)\nsteadycron report --hours 720      # last 30 d (Team plan only)\nsteadycron report --verbose        # adds HTTP response bodies and full alert delivery list\nsteadycron report --json           # machine-readable (CI alerting, dashboards)\n```\n\nThe report mirrors the web Overview dashboard — same calculations, same terminology, so the\nnumbers always agree for an identical time window. It calls `/api/reports/summary` and shows:\n\n| Section | What you see |\n|---|---|\n| **KPI row** | Total checks (HTTP + heartbeat), Successful + success-rate %, Incidents (failed checks), Alerts delivered/failed/suppressed, Jobs reporting |\n| **Active issues** | Jobs with failures or missed check-ins, ranked by attention severity: missed → abandoned → failure → late |\n| **Silent monitors** | Jobs with zero activity in the window — possible schedule drift or misconfiguration |\n| **Footer** | One-line health verdict; exits non-zero when failures or undelivered alerts are detected |\n\nPlan limits cap how far back you can query (Free: 1 day, Developer: 7 days, Team: 30 days).\nThe server returns a `range_exceeds_plan` error with a clear message if the requested `--hours` exceeds your limit.\n\n## Logbook\n\n```bash\nsteadycron logbook                                       # last 24 h of all event types\nsteadycron logbook --hours 168                           # last 7 days\nsteadycron logbook --domain executions                   # filter by category\nsteadycron logbook --domain executions --domain alerts   # multiple categories\nsteadycron logbook --severity critical                   # critical events only\nsteadycron logbook --job my-job-key                      # events for a specific job\nsteadycron logbook --page 2 --page-size 100             # paginate\nsteadycron logbook --all                                 # fetch all pages\nsteadycron logbook --verbose                             # full metadata per event\nsteadycron logbook --all --json                          # machine-readable output\n```\n\nMirrors the web Logbook page. Available `--domain` values:\n\n| Domain | Event types included |\n|---|---|\n| `executions` | HTTP execution succeeded / failed |\n| `heartbeats` | Heartbeat missed / recovered, ping received, run started / abandoned |\n| `alerts` | Alert delivered / failed / suppressed / pending |\n| `jobs` | Job created / deleted / paused / resumed |\n| `keys` | API key created / revoked |\n| `rules` | Alert rule created / deleted |\n| `channels` | Alert channel created / updated / deleted |\n| `subscription` | Plan upgrade / downgrade / cancellation / past-due / paused |\n\n`--severity` accepts `info`, `warning`, or `critical` (repeatable). `--job` accepts a job key,\nname, or id. `--all` pages through all results; omitting it returns the first `--page-size`\nevents (default 50, max 100). `--verbose` shows every metadata field per event (HTTP status,\nerror type, response excerpt, source IP, etc.).\n\n## Exit codes\n\n| Code | Meaning |\n|---|---|\n| `0` | Success (or `plan` with no drift when `--detailed-exitcode` is not set) |\n| `1` | Unexpected error |\n| `2` | Manifest load/validation error; also `plan --detailed-exitcode` when drift is detected |\n| `3` | API error |\n| `4` | Missing/invalid credentials (`401`/`403`) — run `steadycron signup` or `steadycron login` |\n| `5` | Plan/apply has `errors[]` (limit violations, conflicts) or per-resource failures |\n| `130` | Cancelled (Ctrl+C) |\n\n\u003e `--detailed-exitcode` overloads code `2` for `plan`: exit `2` = drift detected,\n\u003e exit `0` = clean. This matches `terraform plan` behaviour and is documented as an opt-in\n\u003e so that CI scripts branching on exit code `2` for validation errors are unaffected.\n\n## Development\n\n```bash\ndotnet build\ndotnet test\ndotnet run --project src/SteadyCron.Cli -- jobs list\n```\n\nBuilt on .NET 10 with [Spectre.Console](https://spectreconsole.net/) and\n[YamlDotNet](https://github.com/aaubry/YamlDotNet).\n\n## License\n\n[MIT](https://github.com/steadycron/cli/blob/main/LICENSE) © SteadyCron.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteadycron%2Fcli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsteadycron%2Fcli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsteadycron%2Fcli/lists"}