Your app runs in an always-on slot, the process never stops, so scheduled work is first-class on DenkOps. There are two paved ways to schedule a job, denkops.cron for in-process scheduling from the SDK, and managed declarative cron in denkops.json for platform-scheduled HTTP calls. You can still write a raw in-process loop if you prefer, but for most apps one of the two paved paths is simpler and safer.
Why does an always-on slot make cron easy?
Cron services on serverless platforms exist to solve one problem: your code is not running when the schedule fires, so something external has to wake it up, cold-starting a fresh function with no memory of the last run and a cap on runtime.
A slot is a process, not a function. It stays up between requests, so a schedule can either fire directly inside that process or be called into by the platform. Both paved paths below build on that.
Path 1: denkops.cron, in-process scheduling
If your app is Bun/JS and the job is just code that already lives in your slot, use denkops.cron from the DenkOps SDK. Schedule it right next to your routes, in the same file you already deploy:
import { Hono } from "hono";
import denkops from "@denkopsai/sdk";
const app = new Hono();
async function nightlyCleanup() {
const last = denkops.store.get("cleanup:last-run");
console.log("running nightly cleanup, last run:", last);
// ... do the work ...
denkops.store.set("cleanup:last-run", new Date().toISOString());
}
denkops.cron("0 2 * * *", nightlyCleanup, {
name: "nightly-cleanup",
timezone: "UTC",
catchUp: true,
overlap: false,
onError: (err) => console.error("nightly cleanup failed", err),
});
app.get("/health", (c) => c.json({ ok: true }));
export default { port: 3000, fetch: app.fetch };The schedule is a standard 5-field cron expression (it also accepts @daily, @hourly, or @every 5m). Run state is persisted to /persist, so restarts and redeploys do not lose track of the last run. There is no HTTP hop, the function runs directly in your process, and you can check on it from inside your own code with denkops.cron.status(name).
Path 2: managed declarative cron
If your app is Python, or any runtime other than Bun, or you want the schedule visible and manually triggerable outside your code, declare it in denkops.json instead:
{
"cron": [
{
"schedule": "0 2 * * *",
"path": "/jobs/nightly",
"method": "POST",
"timezone": "UTC",
"name": "nightly"
}
]
}schedule and path are required, method defaults to POST, timezone defaults to UTC, and name is optional. DenkOps calls your app's path on schedule, a platform-scheduled HTTP request into your always-on slot. Because the platform owns the schedule, it shows up in the dashboard's "Scheduled jobs" panel and can be listed or triggered manually with the MCP tools list_crons and run_cron, from your own agent or from a teammate poking at the dashboard.
Which one should I use?
- Bun app, the job is code you already have in the slot, and you do not need it visible outside your app: use
denkops.cron. - Python app, or any non-Bun runtime, or you want the schedule visible and re-runnable from the dashboard or an agent, or you would rather declare it in
denkops.json: use managed cron.
Both run against the same always-on slot, both use standard 5-field cron syntax, and both default to UTC.
Can I still write my own loop?
Yes. If neither paved path fits, a plain in-process scheduler like setInterval in Bun or APScheduler in Python still works, because the process never stops. The FastAPI cron guide and the Express version cover that pattern in detail. The tradeoff is that you own the restart-safety and observability yourself instead of getting them from denkops.cron or the dashboard.
What happens to my schedule on a redeploy?
denkops.cron persists its run state to the durable /persist disk, so a redeploy does not lose track of the last run. Managed cron is scheduled by the platform itself, independent of any one process, so a redeploy does not affect it either. If you roll your own loop, checkpoint progress to denkops.store, which also lives on /persist and survives redeploys:
- Write a checkpoint when a run finishes: a timestamp, a cursor, the last processed ID.
- Read it at the start of each run and continue from there.
- If a redeploy lands mid-run, the next tick picks up from the last checkpoint instead of starting over or double-processing.