---
title: Cron jobs (scheduling work to run on its own)
---

# Cron jobs (scheduling work to run on its own)

## What it is

A cron job is something Omniscio does for you on a timetable — run a command, fire a saved recipe,
or start a fresh Claude session — without you being there to press go. The Cron Jobs screen is
where you create those jobs, see the ones you have, look at whether they actually ran, and fire one
by hand when you do not want to wait for its next turn.

Every run is recorded, so "did the nightly job actually happen?" has an answer rather than a guess.

## Where to find it

- **The Cron Jobs screen** is a virtual project in the sidebar, headed **Scheduled Tasks (Cron)**.
  It lists your jobs; **+ Add Job** creates one, and there is a **Search jobs…** box.
- **Grouping and filtering** sit above the list: group **By <project>** or **By Frequency**, and a
  **Show completed** checkbox to include the jobs that are finished rather than active.
- **A job is one row.** Clicking it opens its detail view — the settings, its run history, and
  **Run Now**. The row itself also carries Run Now (hover it: *"Run this job once right now."*),
  which asks you to confirm first.
- Creating or editing a job opens a **New Cron Job** / **Edit Cron Job** dialog, described below.

## How it behaves

### Creating a job

The dialog is a set of cards, top to bottom.

- **Name** and **Description** — the name is what you will see in the list, so make it say what the
  job does.
- **Job Type** — one of three, and the choice changes the rest of the form:
  - **Script** — run a shell command. You give it a **Command** and a **Working Directory**, and
    there is a **Run in a visible window** option for commands that need to show you something
    (most should stay hidden).
  - **Recipe** — run one of your saved recipes, chosen with a recipe picker. Use this when the work
    is already a multi-step workflow you maintain elsewhere.
  - **Session** — start a **new Claude session** with a **prompt** you write, optionally giving the
    session a fixed **Session Name**, and optionally **setting a specific AI engine** for it. This is
    the type to use when the scheduled work needs judgement rather than a fixed command.
- **Project** — which project the job belongs to.
- **Visibility** — whether a job that starts a session is **silent**, **visible**, or follows the
  recipe's own default. Silent sessions stay out of your attention path.
- **Schedule** — when it fires.
- **Require Approval** — when on, **every run lands in your inbox as an approval before it
  happens**. Use it for anything that costs money or touches something you would not want running
  unattended.
- **Self-Healing** — whether a job that fails for good is handed to a fresh session to diagnose and
  fix; see [Cron self-healing](cron-self-healing.md).
- **Run if missed** — whether a fire that was missed while the app was closed runs late.
- **Environment Variables** — extra variables for this job's runs only, stored encrypted.

### Running a job after another one finishes

A job can be made to follow another instead of running on its own clock. In the editor this is the
**Run After** card: **Trigger After Job** picks the job to follow — the card says plainly, *"Run this
job after another job completes."* — and **Trigger Condition** decides when that counts.

Three conditions are offered, and the difference between the last two matters:

- **Success** — fire only if the job it follows finished successfully. This is the default.
- **Failure** — fire only if it failed. The usual use is a cleanup or a notification that should
  happen exactly when the main job breaks.
- **Always** — fire whichever way it ended, success or failure.

Whichever you pick, it is judged on the job's **final** result: a job that retries is allowed to
finish retrying first, so a chain never fires on a failure the job then recovered from.

**The one thing to know before you chain two jobs together: nothing stops a loop.** There is no
limit on how deep a chain may go and no check that a chain has come back around to a job it already
ran, so if you set A to follow B and B to follow A, the pair will keep firing each other for as long
as both are enabled. Chaining is a plain "follow this one" link, not a guarded graph — so when you
build a chain, satisfy yourself that it ends somewhere, and use the job's own enable/disable toggle
to stop one if it does not.

### Watching what a job did

Each job keeps a **run history** — one entry per firing, so you can see whether it ran, when, and
how it ended, without reading logs. Beside it, **Run Now** fires the job immediately; it asks you to
confirm first, because a job that spawns a session or sends something is not a free action.

**Run history is kept for 30 days and then deleted, permanently.** The app sweeps once a day and
drops every run record older than that, so a job's history is a rolling month rather than a
permanent log — and nothing in the app can bring a purged record back. **30 days is fixed; it is not
tied to your data-retention setting and there is no way to raise it**, so if you need a durable
record of what a job did, have the job write it somewhere yourself rather than relying on the run
list. The same 30-day sweep covers the app's other run-audit records — automation runs and their
dead letters, the PR merge queue's runs, the PR reconcile ledger, the PR janitor's runs and
dispositions, resolved recipe-approval requests, cron heal attempts, workflow-coach runs, and
aggregation runs — so "a month" is the honest answer for how far back this class of history goes
anywhere in Omniscio.

### The jobs Omniscio adds for itself

Some jobs in the list **you did not create**. Omniscio maintains a handful of housekeeping jobs and
seeds them into the same Cron Jobs list, each carrying a description that ends **"(auto-managed)"**
so they are recognisable. They exist so the app can notice its own drift instead of waiting for a
person to notice. On a machine where the thing they watch does not apply, they are never seeded at
all — so you may have none of these, or several.

- **`plugin-publish-drift-check`** — every 6 hours. Alerts when a marketplace plugin's repo has
  finished work that was never published.
- **`firestore-rules-drift-check`**, **`firestore-indexes-drift-check`**,
  **`firebase-functions-drift-check`** — every 6 hours each. Alert when what is committed is newer
  than what is deployed, for rules, indexes and cloud functions respectively. On a non-default
  Firebase project these carry a `-<project>` suffix in their names.
- **`cloud-billing-budget-drift-check`** — every 6 hours. Alerts when the cloud project has no GCP
  billing budget or alert set up, so a spend surprise cannot arrive silently.
- **`worktree-triage-dispatch`** — every 30 minutes. Dispatches triage for worktrees whose work was
  left stranded.
- **`master-sync-conflict-escalate`** — every 15 minutes. Finishes a conflicted sync of the shared
  branch: resolves what it can for free and buys a capped session for the rest. This one is seeded
  **switched off**; turn it on if you want it.
- **`friction-triage-dispatch`** — every 2 hours. Dispatches triage for the agent-friction ledger.

These are ordinary jobs once seeded, so you can inspect, disable, re-schedule or delete any of them
like one of your own. Be aware that deleting one is not permanent in the way you might expect:
the seeding is idempotent and re-runs on startup, so the app will recreate a housekeeping job it
still needs.

### A scheduled fire can start late, on purpose

Three rate controls sit between "the schedule came due" and "the job started". All three exist
because of one measured failure: a **process-creation storm** — a wave of sessions and two cron
jobs starting processes 12ms apart — was enough to hang the machine. Spacing out *starts* is the
fix, and the cost is that a fire's start time is not exactly its scheduled time.

1. **A per-job phase offset — up to 45 seconds.** Before anything else, each scheduled fire waits an
   offset derived from **its own job id** (the id hashed into a window, default 45 seconds, capped at
   59 so a fire stays inside its scheduled minute). This matters most for schedules like `*/5`, where
   several jobs come due on the *same* tick every five minutes: without the offset they would all
   begin ramping at once. It is **deterministic** — a given job always fires at the same second of its
   minute, so it never drifts and nothing needs to be stored.
2. **A calm wait — up to 90 seconds, only while the machine is busy.** If the whole computer is under
   heavy load (roughly 85% busy), the fire waits in 10-second steps until things calm down, up to a
   **90-second ceiling**. It is a **ceiling, not a skip**: past it the job proceeds regardless, so a
   loaded machine delays a run and can never lose one.
3. **A serialising start throttle — 4 seconds.** Two dispatch releases are spaced at least 4 seconds
   apart, so two due jobs can never start their process trees in the same instant.

**What this looks like in practice.** On an idle machine a job scheduled for 09:00 starts within its
09:00 minute, as you would expect (the phase offset is deliberately bounded to keep it there). On a
loaded machine it can be pushed past the minute — a 45-second offset plus a 90-second calm wait is
the worst case, so if you are comparing a log timestamp against your schedule, **a start up to about
two minutes late is normal behaviour under load, not a fault.** If you need to know whether the
scheduler is healthy rather than merely slow, the alerts below are the signal to read.

### A manual Run now is never delayed

**Run now** dispatches the job directly rather than through the scheduled path, so none of the three
controls above apply to it. It is never held behind a phase offset or a calm window.

### Alerts the scheduler can raise

Six alerts are raised by the scheduler's own alert builders (`cron-engine-alerts.ts`), on top of the
job-failure alert that has a page of its own ([cron failure alerts](cron-failure-alerts.md)) — so
seven conditions can land in your inbox from cron, and every one of them means something different. A
job that is slow, a job that is broken, and a *scheduler* that has stopped are three separate problems
with three separate fixes, and before this list five of them had no published explanation at all.

Every one of these is **deduped** (one row per condition, updated rather than repeated) and every one
**clears itself** when the condition goes away — so an alert disappearing is the all-clear.

| Alert | What it means | What to do |
| --- | --- | --- |
| **A scheduled job fails** (toast + card) | A run failed, and self-healing is not taking it over. | Its own page: [cron failure alerts](cron-failure-alerts.md). |
| **A scheduled job keeps timing out** | The job *starts* but never finishes — it has started and been stopped for timing out several times in a row, so it never completes. It is probably waiting on something that never responds. | Open its schedule and review what it waits on, or turn it off. |
| **Scheduled jobs have stopped running** | The **scheduler itself** is unhealthy: every recent tick threw. This is the broad one — it means backups, reports and automations may simply not be running. Usually a locked or busy local database. | **Restart Omniscio.** It normally clears this, and the alert clears itself once the scheduler recovers. |
| **A scheduled job has stopped running** *(overdue)* | The scheduled time passed and the job never ran, and its schedule has fallen behind. The job may be stuck, or the scheduler may need a restart. | Open its schedule to review or turn it off. Clears when the job runs again. |
| **A scheduled job keeps failing to start** | The job has failed to start on several recent checks — it is not running at all. Usually a bad setting: an environment variable that no longer resolves, or a project that no longer exists. | Open its schedule and check its settings. Clears when the job starts again. |
| **A scheduled job is paused until restart** | The job started, got stuck, and **could not be stopped cleanly** — so it was paused deliberately, to avoid running a duplicate of work that may already be under way. | It will not run again until you **restart Omniscio** (or turn it off). Clears once it runs again. |
| **Some automation actions could not be completed** | An automation's action permanently failed after every retry and was set aside rather than dropped. | See [automations and auto-replies](automations-and-auto-replies.md). |

Two of these are worth telling apart at a glance, because the fixes are different sizes: **"Scheduled
jobs have stopped running"** is plural and about the *scheduler* (restart), while **"A scheduled job
has stopped running"** is singular and about *one job* (inspect it). If several jobs are misbehaving at
once, believe the plural one.


## For agents

- The screen is a virtual project; the editor is `src/renderer/src/features/cron/JobEditorDialog.tsx`,
  the list and dashboard `CronJobsSidebar.tsx` / `CronDashboard.tsx`, and a job's detail view with
  its run history `JobDetailPanel.tsx`.
- The same jobs are creatable, editable, deletable, fireable and toggleable over the CLI control
  server at `/cron/jobs` (`POST` to create, `PATCH`/`DELETE /cron/jobs/:id`, `POST
  /cron/jobs/:id/run`, `POST /cron/jobs/:id/toggle`), all bearer-authed and capped at 10 mutations
  a minute, with a ceiling of 200 jobs.
- Jobs created by an AI arrive **approval-gated** — `requiresApproval` is forced on for them, so an
  agent cannot quietly schedule work that runs without you.
- Chaining is two columns on the job row: `run_after_job_id` (the job to follow) and
  `run_after_trigger` (`success` | `failure` | `always`, default `success`). The only guard is a
  durable at-most-once-per-parent-run check, which keys on *this* parent run — a fresh parent run
  gets a fresh id, so it does **not** break a cycle. Do not build a loop.

## Related

- [Create a cron job with AI](create-cron-job-with-ai.md) — handing the job description to an AI, and
  the approval step that follows.
- [Cron session jobs](cron-session-jobs.md) — the **Session** job type in depth: how each run is
  numbered, the live-session guard, and the five-minute floor.
- [Cron self-healing](cron-self-healing.md) — what happens when a job fails for good.
- [Cron run if missed](cron-run-if-missed.md) — the catch-up switch.
- [Cron failure alerts](cron-failure-alerts.md) — how a failing job gets your attention.
- [Cron group rename](cron-group-rename.md) — naming the groups the sidebar sorts jobs into.
- [CLI Control](cli-control.md) — the local server the `/cron/jobs` routes belong to.
