Cron jobs as code. Your handler is the source of truth β cronix reconciles it onto crontab, systemd-timer, Kubernetes, or AWS EventBridge.
Docs Β· Quick start Β· RFC Β· Examples
cronix-showcase.mp4
β οΈ Under active development. The on-the-wire spec is stable; APIs may evolve before v1.0. Try it and file issues.
Today, "I need a scheduled job" has three answers β none of them tell you the whole picture:
- π§ In-app queue (BullMQ / Agenda) β needs Redis ops, repeats stack on restart, schedule lives in code and in Redis.
- π§ In-process (node-cron / cron) β stops with the process, every replica fires it (N pods β N runs), no audit, no retries.
- π§ Host scheduler (crontab / systemd / k8s) β per-machine install, ssh-edit drift, no who-changed-what audit, silent failures.
Whichever you pick, you can't answer: is this running anywhere right now? who changed the schedule? did the last run succeed?
cronix puts the schedule next to the handler. Your app's /.well-known/cron-manifest endpoint is the source of truth. cronix apply reconciles it against whichever scheduler the host provides. The host scheduler does the firing. A small Go binary, cronix trigger, handles HMAC signing, concurrency locks, timeouts, and retries on every fire.
The protocol is the product. The reconciler and SDKs are reference implementations.
# macOS β Homebrew
brew install awbx/cronix/cronix
# Linux / macOS β one-liner
curl -fsSL https://raw.githubusercontent.com/awbx/cronix/main/install.sh | sh
# Pin a version + custom install dir
curl -fsSL https://raw.githubusercontent.com/awbx/cronix/main/install.sh \
| CRONIX_VERSION=v0.7.2 INSTALL_DIR=/usr/local/bin sh
# Linux packages β grab from the latest release
# https://github.com/awbx/cronix/releases/latest
# cronix_<ver>_linux_amd64.deb (Debian/Ubuntu)
# cronix_<ver>_linux_amd64.rpm (RHEL/Fedora/openSUSE)
# cronix_<ver>_linux_amd64.apk (Alpine)
# Go developers
go install github.com/awbx/cronix/go/cmd/cronix@latest
# Docker
docker pull awbx/cronixVerify:
cronix version# TypeScript
pnpm add @awbx/cronix-sdk
# Framework adapters (only if you need them β see below)
pnpm add @awbx/cronix-adapter-express
pnpm add @awbx/cronix-adapter-fastify
pnpm add @awbx/cronix-adapter-koa
pnpm add @awbx/cronix-adapter-nest
# Go (signature verification only)
go get github.com/awbx/cronix/go/pkg/cronsdkimport { createCron } from "@awbx/cronix-sdk";
import { Hono } from "hono";
const cron = createCron({
app: "billing-service",
baseUrl: "https://billing.example.com",
secret: process.env.CRON_SECRET!,
});
cron.register({
name: "reconcile-payments",
schedule: "*/15 * * * *", // β lives next to the handler
handler: async (ctx) => {
console.log(`fired ${ctx.name} run=${ctx.runId}`);
// your work here
return { ok: true };
},
});
const app = new Hono();
app.all("/.well-known/cron-manifest", (c) => cron.handle(c.req.raw));
app.all("/api/v1/scheduled/:name", (c) => cron.handle(c.req.raw));
export default app;Reconcile from your laptop or CI:
cronix apply \
--manifest https://billing.example.com/.well-known/cron-manifest \
--backend crontab \
--crontab-path /etc/crontab \
--trigger-bin /usr/local/bin/cronix \
--secret-ref env:CRON_SECRETThat's it. Your */15 * * * * line lives in your app code; cron(8) actually fires it; cronix trigger signs the request and POSTs back to your handler.
Runnable mini-apps, each one ~50 lines:
| Example | Stack |
|---|---|
| ts/examples/hono-app | Hono β runs unchanged on Node, Bun, Cloudflare Workers |
| ts/examples/express-app | Express + @awbx/cronix-adapter-express |
| ts/examples/fastify-app | Fastify + @awbx/cronix-adapter-fastify |
| ts/examples/hand-rolled | No framework β just node:http + verifyManifest/verifyTrigger |
| go/examples/go-app | Go net/http server using pkg/cronsdk for HMAC verify |
Each example has a README with the exact pnpm dev (or go run) command and a curl recipe to test end-to-end.
| Backend | What it writes | Setup |
|---|---|---|
crontab |
/etc/crontab lines with # cronix:owned markers |
docs/src/content/docs/backends/crontab.md |
systemd-timer |
.timer + .service units in /etc/systemd/system |
docs/src/content/docs/backends/systemd.md |
kubernetes |
CronJob + ConfigMap per job |
docs/src/content/docs/backends/kubernetes.md |
aws-scheduler |
EventBridge Schedules β cronix-trigger Lambda | docs/src/content/docs/backends/aws.md |
cronix tracks ownership inside each resource β it never touches lines, units, or objects it didn't create. Run alongside hand-edited entries safely.
For frameworks that don't speak Web Fetch natively, install the matching sibling adapter package. Each one exports a handle() that lifts any (req: Request) => Response | Promise<Response> into a framework-native handler:
// Express
import { handle } from "@awbx/cronix-adapter-express";
app.all("/.well-known/cron-manifest", handle((req) => cron.handle(req)));
// Fastify (rawBody installs a wildcard parser to keep bytes-as-sent)
import { handle, rawBody } from "@awbx/cronix-adapter-fastify";
rawBody(app);
app.all("/.well-known/cron-manifest", handle((req) => cron.handle(req)));
// Koa (mount before any body-parser middleware)
import { handle } from "@awbx/cronix-adapter-koa";
router.all("/.well-known/cron-manifest", handle((req) => cron.handle(req)));
// NestJS (Express by default β bootstrap with `bodyParser: false`)
import { handle } from "@awbx/cronix-adapter-nest";
app.use("/.well-known/cron-manifest", handle((req) => cron.handle(req)));Hono, Bun, Workers, Vercel/Next.js, and Deno all serve a Web Request natively β no adapter needed; just call cron.handle(req) directly from your route.
- Documentation site β https://awbx.github.io/cronix/ (sources in
docs/src/content/docs/) - spec/RFC.md β protocol, manifest, authentication, SDK contract, backend contract
- CONTRIBUTING.md β dev setup, repo layout, conformance vectors
- SECURITY.md β vulnerability disclosure
| Area | State |
|---|---|
| Spec | RFC v1 frozen β see spec/RFC.md |
| Backends | crontab, systemd-timer, kubernetes, aws-scheduler β all reconcile end-to-end |
| CLI | init, validate, plan / diff, apply, drift, list, global-status, show, prune, history, trigger, version, completion |
| TypeScript SDK | @awbx/cronix-sdk + 4 framework adapters, conformance-tested against shared spec vectors |
| Go SDK | pkg/cronsdk β HMAC verify only, conformance-tested |
| Distribution | Homebrew tap, deb / rpm / apk, Docker, npm |
cronix is open source under MIT β issues, discussions, and PRs are welcome. A few things worth knowing before you dive in:
- The RFC is the product. Behavior changes are discussed and agreed before code lands. The protocol shape (manifest, signing, headers) is the contract; everything else is an implementation detail.
- Both languages stay in lock-step. Manifest shape, header format, and signing scheme changes must land in TypeScript (
@awbx/cronix-sdk) and Go (internal/manifest,internal/auth) in the same PR, with both passing the sharedmanifest-vectors.jsonandauth-vectors.json. - Conformance vectors are sacred. Adding or modifying one is a spec change.
Full dev setup, branch flow, and release process: CONTRIBUTING.md.
Quick paths to help if you're new:
- File an issue about something that surprised you β bad error messages, missing docs, unclear flags. No issue is too small.
- Add an example for a stack we don't yet cover (Bun-only, Cloudflare Workers, AWS Lambda app, etc.).
- Port the SDK β Python and Ruby SDKs are wide open. The conformance vectors give you a green-light test suite.
MIT Β© Abdelhadi Sabani β see LICENSE.