---
title: Cron job — Session spawn type
---

# Cron job — Session spawn type

## What it is

Omniscio's cron scheduler supports three job types: **Script**, **Recipe**, and **Session**. The **Session** type spawns a brand-new Claude session inside a project at the scheduled time, using a prompt you wrote when you created the job — exactly equivalent to clicking "+ Session" in that project and typing the prompt yourself, but on a recurring schedule.

This is useful when you want a recurring conversation rather than a recurring shell command (Script) or a recurring multi-step playbook (Recipe). For example: "every weekday morning at 8:30 a.m., start a Claude session in my work project with the prompt 'summarize yesterday's git activity and draft today's plan'." Each fire creates a fresh session — past runs do not influence future runs because there is no shared memory between them.

## Where to find it

Session jobs are created exactly like the other two kinds. Open the Cron Jobs view from the Cron
row in the sidebar, click the new-job button, and pick Session on the job-type radios — the
Project and Prompt fields appear in place of the script or recipe settings, and the schedule
controls underneath are the same ones every job uses.

## How it behaves

### How to use it

1. **Open the Cron Jobs view.** Click the **Cron** virtual project in the Omniscio sidebar, or open the Cron Jobs pane from the Settings → Cron Jobs section.
2. **Click "+ New Job"** to open the job editor dialog.
3. **Set the job type to "Session".** The form has three radio buttons: Script, Recipe, Session — pick **Session**. Two new fields appear:
   - **Project** (required) — pick the project the session should spawn in. Must be a real (non-virtual) project.
   - **Prompt** (required) — the initial message the new session opens with. This is identical to typing into the composer of a fresh session and hitting Enter.
   - **Session Name** (optional) — the label each spawned session carries. **Leave it blank and the job's own name is used**, so you never have to fill this in to get a sensible title; set it only when you want the sessions labelled differently from the job.

   **Every spawned session is numbered, always.** Whichever base name applies, each fire appends its **run number** — `Performance watch review #10`, `overseer heartbeat #59` — so a job that has fired ten times leaves ten distinguishable sessions instead of ten look-alikes. The number is the job's own run count, so it matches the run history in the job panel, and a gap means a fire that errored before it could create a session.

   Because a numbered name is not a placeholder, the AI auto-titler stands down for these sessions (it only ever renames a session still called `Session N`). That is deliberate: an invented title differs on every run, which is the very thing the number is there to fix — one nightly job had accumulated seven near-identical variants of "Nightly PR Janitor for Agent-Orchestrator". It also means a scheduled session costs no title-generation call.
4. **Set the schedule.** Same controls as Script and Recipe jobs — pick a preset (daily at 9am, hourly, etc.) or type a 5-field cron expression. Pick a timezone. Choose run mode (recurring / one-off / limited).
5. **Save.** Like every cron job, AI-created jobs land as **Awaiting approval** until you click Approve in the inbox; jobs you create yourself in the Omniscio UI arm immediately.

### How it works

A **session-spawn cron job** stores its config in the `cron_jobs.job_config` JSON column with shape `{ projectId, prompt, sessionName }`. When the 60-second engine tick determines the job should fire, the engine routes the run to the **`SessionExecutor`** ([/src/main/services/cron-session-executor.ts](/src/main/services/cron/cron-session-executor.ts)) instead of the Script or Recipe executor. The executor validates the config (non-empty `projectId`, non-whitespace `prompt`) and calls the same `createSessionWithPrompt` path the Omniscio UI uses when you click "+ Session" — so spawn behavior matches manual session creation exactly: account auto-pick, worktree resolution, isolation, the works.

**Failure detection covers two paths.** If `createSessionWithPrompt` throws (project missing, `processManager.launch` crashed, etc.), the run is reported `failed` and the standard cron failure-alert pipeline kicks in (toast + inbox card + optional self-heal — see [cron-failure-alerts.md](cron-failure-alerts.md)). If `createSessionWithPrompt` returns successfully but the **API-key session-spawn guard** flips the new session to `status='error'` without throwing, the executor reads the post-launch status and reports `failed` with a clear message ("check active account and 'Allow API keys to run sessions' setting"). This handles the silent-failure case where an API-key account is active but `allowApiKeySessionSpawn` is `false` — a common cause of "the session row appeared but never started".

**Sessions are fire-and-forget — but the job will not stack a second one on top of a live one.** Once the executor confirms the spawn succeeded, the cron run is `success` regardless of what the agent inside that session does next. The new session shows up in the project sidebar like any other session — you interact with it normally, archive it normally, etc. The run's success never depends on the agent's work.

The one coupling that does exist: **while a session this job started can still make progress, the next fire skips its spawn** and records the skip as a successful run naming that session. The guard clears the moment the session stops holding the job — once its turn is over and it is waiting on you, or it has ended, been archived, paused, or errored out. It does **not** wait for you to tidy your inbox, and a session you pause is not treated as a reason to stop the schedule.

This matters because a job's run is finished the moment the session is *created* — the schedule does not know the session is still working — so a job whose schedule is shorter than its sessions would otherwise stack one billable Claude session per fire. That is real: a 2-hourly evaluator job once had three of its sessions running at once, all doing the same work. Skipping is reported as success rather than failure on purpose: a failure would go to the retry and self-heal machinery, which would re-dispatch the executor and recreate the duplicate.

**Because a skip is a success, a job that has stopped firing still looks healthy.** If a scheduled session job quietly stops producing sessions, open its run history and read the output: a run whose output begins `Skipped:` was held by this guard, and the session id it names is the reason. The job's own **last run** timestamp and status stay green throughout — they answer "did the scheduler tick?", never "did a session get created?".

**Run it hidden (`force_silent`).** Set the job's Visibility radio to **Force silent** to spawn the session hidden — it stays out of the sidebar and inbox, auto-archives when it finishes, and only surfaces if it errors or stalls. Leave it on **Use recipe default** / **Force visible** for the normal visible behavior above. Details: [silent-recipe-sessions.md](silent-recipe-sessions.md).

**In the Cron Jobs view, a session job shows its Type as "Session" and groups under the project it spawns into.** That project comes from the job's spawn target (the **Project** you picked when creating it). Because a session job stores its project inside `job_config` rather than the job's top-level project field, Omniscio reads the spawn target for the **By Project** grouping, project search, and the detail panel's **Project** row — and mirrors it into the top-level field whenever the job is created or edited, so the grouping, search, and the `?project=` REST filter all agree on where the job lives.

**Cost protection: 5-minute minimum interval.** The job-create Zod schema rejects session-spawn jobs whose cron expression fires more often than once per 5 minutes. (Recurring `*/4 * * * *` would be blocked; `*/5 * * * *` is the floor.) This is a guardrail against accidental account drain — every fire spawns a real Claude process, and a misconfigured per-minute schedule could burn through a rate-limit window in seconds. The Recipe and Script types do not have this floor because they are not guaranteed to spawn a Claude process per fire (a Script that runs `git status` costs nothing; a Recipe with no LLM steps costs nothing).

**Telemetry.** A successful spawn fires the `cron_session_spawned` feature event ([/src/shared/feature-registry/index.ts](/src/shared/feature-registry/index.ts)) with `projectId` as the only allowed metadata key — prompt content and session names never leave the local DB. Failed spawns are not tracked under this event (they show up in the standard cron-run logs and the cron-failure-alert pipeline).

### Waking a session instead of starting a new one (wake schedules)

The same machinery runs in a second direction. Instead of starting a fresh session on a timetable, a
job can be pointed at **a session that already exists** and used to **wake it** — the scheduled run
delivers its prompt into that session rather than creating a new one, cold-resuming the session's
process first if it had ended. This is a **wake schedule**, and it is what lets a session put
*itself* to sleep and come back later: an Overseer checking on a long job, or any agent that wants
to look again in twenty minutes without holding a turn open.

The difference that matters is what it costs. A normal session job spawns a **new** session on
every fire; a wake schedule never can — its whole purpose is to re-use the one it names — so
waking a session does not add a session to your sidebar or bill a second one.

**What a wake schedule cannot do** (the two types share a table but not their full behaviour): it
never asks for approval, never retries, never catches up a missed fire,
and it is device-scoped. Those are deliberately fixed rather than configurable, because a wake is a
nudge to a conversation that already exists — there is nothing to approve and nothing to spawn.

**The limits**, all of which a caller hits rather than configures:

- **Cadence is bounded on both ends** — at least **5 minutes** apart, at most **24 hours**.
- **At most 3 wake schedules per session** at a time, or **12** for an Overseer session, which needs
  more of them because it supervises other work.
- **A schedule ends by running out, not by a date.** Each wake schedule carries a maximum number of
  fires (288 by default — a day's worth at the five-minute floor) and simply stops when it has used
  them. There is no "expires at" time to set; if you want it gone sooner, delete it.

A session manages only **its own** wake schedules: it can create, list, edit or delete the ones
bound to it and can neither see nor touch another session's.

**A heartbeat that keeps finding nothing is handled for you.** A wake round that comes back
perfectly healthy but *declares* it found nothing new is counted against that schedule, and three
consecutive quiet rounds are treated as a heartbeat nobody needs. Omniscio then escalates:

- **At 3** the agent is told on its own next wake — how many rounds in a row found nothing, what
  they cost, and its options (slow the cadence, retire the schedule, hibernate, or close itself out).
- **At 12** you get **one inbox card** naming that schedule, its count and its cost. It links the
  session it keeps waking — tap the name to jump to what the agent is actually doing — and offers
  three controls: **Open this job** (the schedule, where the cadence and the switch live), **Turn
  off this wake** (switch that one schedule off on the spot; reversible from the Cron Jobs panel),
  and **Stop the session** (end the running agent itself — it confirms first, because unlike the
  other two it cannot be undone).
- **At 25** the schedule is **switched off**. The fire before that announces itself as the last one,
  so the agent can re-arm the heartbeat if the guard is still wanted, and nothing is deleted — the
  row, its prompt and its history survive, and turning it back on through the normal control clears
  the retirement.

**Either way out starts the count again.** Re-arming a switched-off heartbeat, and giving a live one
a new cadence, both end the current run of quiet rounds — so the schedule is judged on what it does
from that point, rather than carrying the count that condemned it. That is what makes the two things
the escalation asks for (slow the cadence, or turn it back on) actually take effect.

Two rules keep it from firing on a heartbeat that is working. A round counts as quiet **only** when
the agent itself said so (the app's no-op sentence, `NOTHING_TO_DO`, the quiet marker, or the
hibernate marker) — never inferred from a short reply or a low tool count. And a round whose verdict
could not be observed counts as **productive**, so a streak only ever advances on evidence.
`AMC_DISABLE_HEARTBEAT_GOVERNOR=1` turns the whole governor off.

## For agents

A wake schedule is an ordinary `cron_jobs` row of type `session` whose `jobConfig.targetSessionId`
redirects the session executor from SPAWNING to WAKING that existing session. It is deliberately
created with `requiresApproval: false`, `retryCount: 0`, `catchUpIfMissed: false` and
`deviceScoped: true` — a wake has nothing to approve and nothing to spawn.

Four routes serve it, all bound to the CALLING session via its provenance
(`src/main/services/cli/cron/wake-schedule-routes.ts`):

- `POST /wake-schedules` — create one for the calling session.
- `GET /wake-schedules` — list the caller's own **armed** schedules only.
- `PATCH /wake-schedules/:id` — edit or toggle one of the caller's own.
- `DELETE /wake-schedules/:id` — delete one of the caller's own. A required Overseer wake type is
  refused with `409`.

The shape, the timing bounds and the per-session cap live in `cron/wake-schedule-create.ts`, not in
the routes — the routes are only one of three write surfaces (the Overseer keeper and the owner's UI
control write the same rows without HTTP). Constants: `MAX_WAKE_SCHEDULES_PER_SESSION = 3`,
`MAX_WAKE_SCHEDULES_PER_OVERSEER_SESSION = 12`, `MIN_WAKE_CADENCE_MINUTES = 5`,
`MAX_WAKE_CADENCE_MINUTES = 1440`, `DEFAULT_WAKE_MAX_RUNS = 288`. `AMC_DISABLE_WAKE_SCHEDULES=1`
turns the whole surface off with a `403`.

## Related

The manual version of what this type does on a schedule — starting a session by hand and what
happens as it comes up — is on the [start a new session](start-a-new-session.md) page, and how
Omniscio picks which Claude account a spawned session runs on is on
[account pool](account-pool.md). The other per-job switches a cron job can carry are on
[run if missed](cron-run-if-missed.md) and [cron self-healing](cron-self-healing.md).

- [create-cron-job-with-ai.md](create-cron-job-with-ai.md) — create cron jobs by asking an external AI (works for all three types)
- [cron-failure-alerts.md](cron-failure-alerts.md) — toast + inbox card when a cron run fails permanently
- [cron-self-healing.md](cron-self-healing.md) — opt-in self-heal for cron failures
- [start-a-new-session.md](start-a-new-session.md) — manual session spawn (what this cron type triggers on a schedule)
- [account-pool.md](account-pool.md) — how Omniscio auto-picks a Claude account when a session spawns