Scheduled runs with the Gateway cron
5 min read
The Gateway has a built-in scheduler: define a job once and it starts an agent session on a schedule - every morning, every hour, or once at a specific time - with nobody at the keyboard. It is the DevThrottle-native alternative to wiring up an external task scheduler.
The Schedule page
The friendly way to manage jobs is the Schedule page in the Cockpit (open Cockpit from the Director toolbar, then Schedule in the left navigation). Create, edit, enable or disable, and run jobs there, and see each job's run history. Everything below is the REST surface behind that page, for when you want to script it.
The REST API
On a self-hosted Gateway, the scheduler lives on the Gateway's local API at http://127.0.0.1:7878. Requests need the machine's shared Gateway token as a bearer token; it is in %LOCALAPPDATA%\cc-director\config\director\gateway-token.txt.
On the hosted Gateway the same /cron surface rides your Gateway's hosted address instead, behind your account's sign-in and device authentication - there is no local port and no shared token file. The Schedule page in the Cockpit uses exactly this surface, so anything it can do, a script signed in the same way can do.
/cron/jobsid and computed nextRunUtc), or 400 if the schedule is invalid./cron/jobs{ "jobs": [...] }./cron/jobs/{id}PUT to the same path updates it; DELETE removes it./cron/jobs/{id}/run/cron/jobs/{id}/runsJob fields
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Required | Display name for the job. |
enabled | boolean | Optional | Whether the job fires. Defaults to true. One-off jobs disable themselves after firing. |
scheduleKind | string | Required | "recurring" (uses cronExpression) or "oneOff" (uses runAt). |
cronExpression | string | Optional | Standard 5-field cron expression, e.g. "0 7 * * 1-5" for 7:00 on weekdays. Required when scheduleKind is "recurring". |
runAt | string | Optional | Local wall-clock timestamp, e.g. "2026-07-10T18:00:00", interpreted in timeZoneId. Required when scheduleKind is "oneOff". |
timeZoneId | string | Required | Time zone the schedule is evaluated in, e.g. "America/New_York". |
target.machine | string | Required | The machine the session runs on. Machine names come from GET /directors. |
action.repoPath | string | Required | Working folder for the session. Use forward slashes, e.g. "D:/repos/my-project". |
action.seed | string | Optional | The prompt the new session starts with. Either seed or workListName must be set. |
action.workListName | string | Optional | Instead of a seed prompt, drain a named work list. Takes precedence over seed. |
action.autoDismiss | boolean | Optional | When true (the default for seed actions), the session closes itself once it finishes with nothing needing a human, instead of lingering in the rail. Set false to keep the session open like an interactive one. |
preventOverlap | boolean | Optional | Reject a fire attempt while another fire attempt for the same job is mid-start. Defaults to true. Does not guard against the session a previous run started - see below. |
notifyOn | string | Optional | "none" (default), "always", or "failure" - when to notify. The notification reports whether the run started; "failure" means it failed to start. |
notifyWebhookUrl | string | Optional | Webhook URL for run notifications. |
The Gateway fills in the rest: id, createdUtc, lastFiredUtc, lastStatus, and nextRunUtc - the next due time in UTC, computed from your schedule and time zone, and recomputed whenever the Gateway restarts.
Example: a weekday morning run
{
"name": "Morning triage",
"scheduleKind": "recurring",
"cronExpression": "0 7 * * 1-5",
"timeZoneId": "America/New_York",
"target": { "machine": "MY-PC" },
"action": {
"repoPath": "D:/repos/my-project",
"seed": "Review open issues in this repo and post a triage summary."
},
"preventOverlap": true
}$token = Get-Content "$env:LOCALAPPDATA\cc-director\config\director\gateway-token.txt"
curl.exe -s -X POST http://127.0.0.1:7878/cron/jobs `
-H "Authorization: Bearer $token" `
-H "Content-Type: application/json" `
--data-binary "@create-job.json"For a one-off, set "scheduleKind": "oneOff" and "runAt": "2026-07-10T18:00:00" instead of the cron expression - the job fires once at that local time and disables itself.
How firing works
The Gateway sweeps the job list about once a minute. When a job is due, it looks for a running Director on the target machine. If none is running, it asks that machine's connected Launcher to start one and waits briefly for it to register. If the machine is off - or nothing registers in time - the launch fails and the run is recorded as not-started. Either way, a recurring job then advances to its next future occurrence: a missed fire is a single skipped beat, never a replay of every missed interval, and never a run that triggers later when the machine wakes up.
The run record keeps the start outcome - did a session start, on which machine and Director, with which session id. It does not track what the agent went on to do: the run's task status is recorded as unknown and stays that way. To see what a scheduled run actually did, open the session it started.
preventOverlap guards the fire attempt, not the session. It rejects a second fire while the first is still mid-start, and is released as soon as the start attempt finishes - so tonight's run will still fire even if last night's session is working to this moment. If two of the same job must never run at once, make the prompt itself tolerant of a sibling session, or space the schedule wider than the work.Where jobs live
Job definitions and run history live in the Gateway's database - the cron_jobs and cron_runs tables, in the local SQLite file on a self-hosted Gateway and in PostgreSQL on the hosted one - so schedules survive restarts and reboots. (The old cronjobs.json and cronruns.json files were a previous format; on first run after an upgrade they are imported once and renamed aside as backups.) See keeping agents running for the full restart story.