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.
From the command line
The same schedules are on the cc-devthrottle command line, which is the surface an agent or a script reaches for. These talk to your Gateway wherever it is - hosted or your own - so there is no address to configure.
cc-devthrottle schedule list # every schedule on the Gateway
cc-devthrottle schedule get <id> # one schedule in full
cc-devthrottle schedule runs <id> # its run history
cc-devthrottle schedule endpoint # which Gateway these commands are talking tocc-devthrottle schedule run <id> # fire it now, whatever its schedule says
cc-devthrottle schedule disable <id> # stop it firing, keep the definition
cc-devthrottle schedule enable <id>
cc-devthrottle schedule delete <id>Creating one takes the four things a schedule cannot be without - --name, --machine, --repo and --tz - plus either --cron for a recurring five-field expression or --at for a one-off local timestamp, and either --seed for the prompt the session starts with or --worklist to drain a named work list instead:
cc-devthrottle schedule create \
--name "Morning triage" --machine MY-PC --repo D:/repos/my-project \
--tz America/New_York --cron "0 7 * * 1-5" \
--seed "Review open issues in this repo and post a triage summary."The reading commands all take --json, so an agent can act on the answer rather than parse a table.
The REST API
Underneath both of those is the Gateway's own REST surface. Reach for it when you are writing something that is not a DevThrottle session - a build step, a webhook, another service.
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.