{"id":48755277,"url":"https://github.com/nuetzliches/croniq","last_synced_at":"2026-04-27T02:00:51.852Z","repository":{"id":350773113,"uuid":"1108755354","full_name":"nuetzliches/croniq","owner":"nuetzliches","description":"⚙️ Better Cron — distributed job scheduling platform built in Rust","archived":false,"fork":false,"pushed_at":"2026-04-22T16:05:40.000Z","size":42015,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-22T18:08:43.375Z","etag":null,"topics":["cron","devops","distributed-systems","job-scheduler","rust","scheduler"],"latest_commit_sha":null,"homepage":"https://nuetzliches.github.io/croniq/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nuetzliches.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-12-02T21:55:07.000Z","updated_at":"2026-04-22T16:04:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nuetzliches/croniq","commit_stats":null,"previous_names":["nuetzliches/croniq"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/nuetzliches/croniq","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nuetzliches%2Fcroniq","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nuetzliches%2Fcroniq/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nuetzliches%2Fcroniq/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nuetzliches%2Fcroniq/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nuetzliches","download_url":"https://codeload.github.com/nuetzliches/croniq/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nuetzliches%2Fcroniq/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32319560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"online","status_checked_at":"2026-04-27T02:00:06.769Z","response_time":128,"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":["cron","devops","distributed-systems","job-scheduler","rust","scheduler"],"created_at":"2026-04-13T01:23:26.289Z","updated_at":"2026-04-27T02:00:51.839Z","avatar_url":"https://github.com/nuetzliches.png","language":"Rust","readme":"# Croniq\n\n[![CI](https://github.com/nuetzliches/croniq/actions/workflows/ci.yml/badge.svg)](https://github.com/nuetzliches/croniq/actions/workflows/ci.yml)\n[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT)\n[![Container](https://img.shields.io/badge/container-ghcr.io-blue?logo=docker)](https://ghcr.io/nuetzliches/croniq)\n[![OpenAPI](https://img.shields.io/badge/OpenAPI-3.1-green)](openapi.yaml)\n\n**Distributed cron that just works.** Single binary. SQLite default. Production-ready retries.\n\nReliable distributed job scheduling with built-in retries, calendar-aware scheduling, a React dashboard, and an AI-native MCP server. Deploy as a single binary or Docker container — no cluster required.\n\nFull API documentation: [`openapi.yaml`](openapi.yaml)\n\n---\n\n## Why Croniq?\n\n| Problem | Croniq |\n|---|---|\n| Cron jobs fail silently — nobody notices for days | Dead letter queue, execution logs, Prometheus metrics, failure notification hooks |\n| Single server = single point of failure | Pull-based runner protocol — scale runners independently |\n| No retries, no backoff, no timeout | Exponential, linear, fixed retry with jitter. Per-job timeout enforcement |\n| Most schedulers need a cluster just to get started | Single binary, SQLite by default, Docker one-liner |\n| Timezone and DST edge cases break everything | Per-job timezone, calendar system with business day rules |\n| Teams can't self-service their own schedules | Hybrid model: Croniqfile DSL for ops, REST API + Runner SDK for developers |\n\n## Who Is It For?\n\n- **Small-to-mid engineering teams** running 20–200 scheduled jobs without a platform team\n- **DevOps/SRE teams** replacing fragile crontabs with something observable\n- **Self-hosters** who want a single Docker container with a dashboard\n\n---\n\n## Key Features\n\n**Croniqfile DSL** — human-readable scheduling configuration. Includes parser, compiler, formatter, validator, and crontab migration tool.\n\n**Hybrid job registration** — define jobs in the Croniqfile (infrastructure-as-code) *or* register them dynamically via REST API and Runner SDK. Both coexist; Croniqfile takes precedence on conflicts.\n\n**Pull-based runner protocol** — runners poll for work via HTTP long-poll. Scale runners independently. Built-in capability routing, instance guard, and lease renewal.\n\n**Calendar system** — include/exclude rules for weekdays, holidays, annual dates, and time windows. Jobs fire only when the calendar allows.\n\n**Retry + dead letter** — exponential, linear, or fixed backoff with jitter. Failed executions go to a dead letter queue for inspection and one-click replay.\n\n**Execution modes** — `queued` (default) persists every execution with full retry and restart recovery. `ephemeral` skips persistence for high-frequency fire-and-forget jobs. Configurable per-job or globally in `defaults {}`. Catch-up policies (`all` / `latest` / `none`) control missed-fire behaviour on restart. Queue TTL and per-job depth limits prevent runaway backlogs.\n\n**Auth** — JWT tokens, API keys, and password authentication. Per-scope authorization is enforced on every endpoint: a token must carry the matching scope (e.g. `jobs:write`, `dead-letters:write`, `runners:read`) or the wildcard `admin` scope. See [Scopes](#scopes) below.\n\n**React dashboard** — login, jobs CRUD with live scheduling, runners with status badges, executions with log viewer, dead letter detail panel.\n\n**MCP server** — 12 tools for AI assistant integration. Observe queue status, list runners, trigger jobs, manage dead letters — all from Claude, Cursor, or any MCP client.\n\n**Failure notifications** — `CRONIQ_ON_FAILURE_CMD` runs a shell command when executions fail. Pipe to Slack, PagerDuty, or any webhook endpoint.\n\n---\n\n## Quick Start\n\nPick the install method that fits your environment. All produce the same `croniq-server` / `croniq` binaries.\n\n### Docker Compose (recommended for trying it out)\n\nFull stack — server + two demo runners executing live jobs — in one command:\n\n```sh\ngit clone https://github.com/nuetzliches/croniq \u0026\u0026 cd croniq\ndocker compose up\n```\n\nOpen **http://localhost:4000**. The demo runners register against [`Croniqfile.demo`](Croniqfile.demo), so you'll see executions, retries, and occasional dead letters streaming in immediately. Tune with `RUNNER_REPLICAS` and `RUNNER_FAIL_RATE` env vars.\n\n### Docker (server only)\n\n```sh\ndocker run -p 4000:4000 ghcr.io/nuetzliches/croniq:latest\n```\n\nOn first start a random admin password is generated and printed to the container logs. Set `CRONIQ_ADMIN_PASSWORD` to use a fixed one.\n\n### curl | sh\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/nuetzliches/croniq/main/install.sh | sh\n```\n\nDetects your OS/arch (Linux/macOS, x64/ARM64), downloads the latest release, installs to `/usr/local/bin`. Override with `INSTALL_DIR` or `CRONIQ_VERSION`.\n\n### Homebrew (macOS / Linux)\n\n```sh\nbrew install nuetzliches/tap/croniq\n```\n\n### From source\n\n```sh\n# Zero-to-running in one command (generates a random admin password)\ncroniq quickstart\n\n# Or step by step (prompts for password):\ncroniq init --data-dir .data --username admin\ncroniq-server --config Croniqfile --data-dir .data --ui-dir ui/dist\n```\n\nOpen **http://localhost:4000** and log in as `admin` with the password shown during init.\n\n### Migrate from crontab\n\n```sh\ncroniq migrate /etc/crontab -o Croniqfile\n```\n\n---\n\n## Configuration\n\nJobs can be defined in a **Croniqfile** (declarative DSL), via the **REST API**, or through the **Runner SDK**.\n\n### Croniqfile\n\n```\nserver {\n  listen :4000\n  data_dir /var/lib/croniq\n}\n\ndefaults {\n  timezone Europe/Vienna\n  retry exponential { max_attempts 3; base 2s; cap 30s }\n  timeout 5m\n\n  # Execution mode: \"queued\" (default) persists every execution to DB,\n  # enabling retries, dead-letter, and restart recovery.\n  # \"ephemeral\" skips persistence — ideal for high-frequency heartbeat jobs.\n  execution_mode queued\n\n  # What to do with missed fires on server restart:\n  # \"all\" (default) — replay everything, \"latest\" — run once, \"none\" — skip\n  catch_up all\n\n  # Cancel queued executions that have been waiting too long (optional)\n  # queue_ttl 1h\n\n  # Max queued executions per job before new fires are skipped (default: 10)\n  # max_queue_depth 10\n}\n\ncalendar business-days {\n  include weekly monday tuesday wednesday thursday friday\n  exclude annual 01-01 12-25 12-26\n}\n\njob billing:invoice {\n  every weekday at 02:00 { calendar business-days }\n  runner { require billing }\n  timeout 15m\n}\n\njob etl:sync {\n  every 15 minutes\n}\n\n# High-frequency monitoring job — fire-and-forget, no DB overhead\njob infra:heartbeat {\n  ephemeral every 5 seconds\n}\n```\n\n### REST API\n\n```sh\n# Register a job + schedule via API (immediately live in scheduler)\ncurl -X POST http://localhost:4000/v1/jobs/register \\\n  -H \"Authorization: ApiKey croniq_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"job_key\": \"etl:sync\", \"schedule\": \"5m\", \"timeout\": \"10m\"}'\n```\n\n### Runner SDK\n\n```rust\nuse croniq_runner_sdk::{CroniqRunner, ExecutionContext};\n\n#[tokio::main]\nasync fn main() {\n    let runner = CroniqRunner::builder(\"http://localhost:4000\", \"my-runner\")\n        .api_key(\"croniq_abc123\")\n        .capabilities(vec![\"billing\".into()])\n        .max_inflight(5)\n        .build();\n\n    // Register handler + schedule — auto-registered on the server at startup\n    runner.register_with_schedule(\"billing:invoice\", \"5m\", |ctx: ExecutionContext| async move {\n        println!(\"Processing: {}\", ctx.execution_id);\n        Ok(())\n    }).await;\n\n    runner.start().await.unwrap();\n}\n```\n\n---\n\n## Architecture\n\n```mermaid\ngraph LR\n    CF[Croniqfile] --\u003e S[croniq-server]\n    API[REST API] --\u003e S\n    SDK[Runner SDK] --\u003e S\n    S --\u003e Q[Work Queue]\n    Q --\u003e R1[Runner 1]\n    Q --\u003e R2[Runner 2]\n    Q --\u003e R3[Runner N]\n    S --\u003e M[\"Metrics (:9900)\"]\n    S --\u003e UI[React Dashboard]\n    S --\u003e MCP[MCP Server]\n```\n\n### Crates\n\n| Crate | Description |\n|---|---|\n| `croniq-config` | DSL parser, compiler, formatter, validator |\n| `croniq-scheduler` | Cron engine, calendar evaluation, trigger state machine |\n| `croniq-store` | Persistence traits + SQLite / Postgres |\n| `croniq-execution` | Retry, timeout, dead-letter pipeline |\n| `croniq-runner` | HTTP Pull-API server, registry, work queue |\n| `croniq-bridge` | JobConfig to WorkItem translation |\n| `croniq-auth` | JWT, API key hashing, password auth |\n| `croniq-server` | HTTP server with ~35 REST endpoints |\n| `croniq-mcp` | MCP server for AI assistants |\n| `croniq-cli` | CLI: validate, fmt, compile, init, migrate, quickstart |\n| `croniq-runner-sdk` | Client library for building runners |\n| `croniq-demo-runner` | Ready-made runner binary used by the Docker Compose quickstart |\n\n---\n\n## REST API\n\nAll `/v1/` endpoints require authentication (`Authorization: Bearer \u003cjwt\u003e` or `Authorization: ApiKey \u003ckey\u003e`).\n\n| Group | Endpoints |\n|---|---|\n| Auth | `POST /v1/auth/login`, `/refresh`, `/logout` |\n| Jobs | `GET/POST /v1/jobs`, `GET/DELETE /v1/jobs/{key}`, `POST .../activate`, `POST /v1/jobs/register` |\n| Schedules | `GET/POST /v1/schedules`, `GET/DELETE /v1/schedules/{id}` |\n| Runners | `GET /v1/runners`, `GET /v1/runners/stream` (SSE), `DELETE /v1/runners/{id}` |\n| Work | `POST /v1/work/poll`, `/ack`, `/renew`, `/{id}/events` |\n| Executions | `GET /v1/executions`, `GET /v1/executions/{id}/logs` |\n| Dead Letters | `GET /v1/dead-letters`, `GET/DELETE .../dead-letters/{id}`, `POST .../replay` |\n| Calendars | `GET/POST /v1/calendars`, `GET/DELETE /v1/calendars/{id}` |\n| Dashboard | `GET /v1/dashboard/forecast` |\n| API Clients | `GET/POST /v1/api-clients`, `DELETE .../api-clients/{id}`, `POST .../tokens` |\n| API Keys | `POST /v1/api-keys`, `DELETE /v1/api-keys/{id}` |\n| Health | `GET /health` (public) |\n| Metrics | `GET /metrics` (separate port) |\n\nFull specification: [`openapi.yaml`](openapi.yaml)\n\n### Scopes\n\nEvery endpoint requires the matching scope on the caller's token. `admin` acts as a wildcard. The CLI's `croniq init` issues an admin client by default; for production runners and dashboards, mint API keys with the minimum scope set.\n\n| Endpoint group | Read scope | Write scope |\n|---|---|---|\n| Jobs | `jobs:read` | `jobs:write` (`jobs:register` for `/v1/jobs/register`, `jobs:trigger` for `/v1/trigger`) |\n| Schedules | `schedules:read` | `schedules:write` |\n| Calendars | `calendars:read` | `calendars:write` |\n| Executions + logs | `executions:read` | — |\n| Dead letters | `dead-letters:read` | `dead-letters:write` (delete + replay) |\n| Runners | `runners:read` (incl. SSE) | `runners:write` |\n| Runner pull-protocol | — | `work:poll`, `work:ack`, `work:renew`, `work:events` |\n| Dashboard forecast | `jobs:read` | — |\n| API clients | `api-clients:admin` | `api-clients:admin` |\n| API keys | — | `api-keys:admin` |\n| Admin reload | — | `admin` |\n\nA 403 with no body is returned when the scope is missing. Auth-disabled mode (no `pull_api.auth` and no `CRONIQ_JWT_SECRET`) injects a synthetic admin context so unconfigured dev servers stay open — production must configure JWT or refuse to start.\n\n---\n\n## CLI\n\n```sh\ncroniq quickstart                          # Zero-to-running: init + sample Croniqfile\ncroniq init --data-dir .data               # Seed admin user (add --api-key to also seed a default client)\ncroniq validate Croniqfile                 # Check for errors\ncroniq fmt Croniqfile --write              # Format in place\ncroniq compile Croniqfile                  # Print compiled JSON\ncroniq convert '*/15 * * * *'             # Cron expression to DSL\ncroniq migrate crontab.txt -o Croniqfile   # Convert crontab to Croniqfile\ncroniq status                              # Live scheduler status\ncroniq list-runners                        # Connected runners\ncroniq trigger billing:invoice             # Fire job immediately\ncroniq dead-letters --data-dir .           # List dead letters\n```\n\n## Environment Variables\n\n| Variable | Description | Default |\n|---|---|---|\n| `RUST_LOG` | Log level filter | `info` |\n| `CRONIQ_JWT_SECRET` | JWT signing secret | random per-start |\n| `CRONIQ_ADMIN_USER` | Docker auto-init username | `admin` |\n| `CRONIQ_ADMIN_PASSWORD` | Docker auto-init password (random if unset) | _generated_ |\n| `CRONIQ_ON_FAILURE_CMD` | Shell command on execution failure | — |\n\n---\n\n## Documentation\n\n| Document | Purpose |\n|---|---|\n| [`README.md`](README.md) | This file — overview, quick start, architecture |\n| [`openapi.yaml`](openapi.yaml) | OpenAPI 3.1 specification for all REST endpoints |\n| [`Croniqfile.example`](Croniqfile.example) | Full DSL example with calendars, retries, metadata |\n| [`Croniqfile.demo`](Croniqfile.demo) | Minimal demo profile used by `docker compose up` |\n| [`docker-compose.yml`](docker-compose.yml) | Quickstart stack: server + demo runners |\n| [`install.sh`](install.sh) | `curl \\| sh` installer for Linux/macOS |\n| [`AGENTS.md`](AGENTS.md) | AI assistant guidance for contributing |\n| [`crates/croniq-runner-sdk/examples/`](crates/croniq-runner-sdk/examples/) | Runner SDK usage examples |\n\n---\n\n## Development\n\n```sh\ncargo build --workspace              # Build all crates\ncargo test --workspace               # Run all tests\ncargo clippy --workspace -- -D warnings  # Lint\n\ncd ui \u0026\u0026 npm run dev                 # Vite dev server on :5173\ncroniq-server --config Croniqfile.example --data-dir .data  # API on :4000\n```\n\n## License\n\nLicensed under either of\n\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n- MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n\nat your option.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnuetzliches%2Fcroniq","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnuetzliches%2Fcroniq","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnuetzliches%2Fcroniq/lists"}