Scheduled tasks
Recurring task creation — a schedule fires on a cron cadence and spawns an ordinary task, with a repo set and base branch, into the workflow engine.
A scheduled task turns recurring work into a first-class object. Instead of a person remembering to re-file the same task every Monday — a nightly report, a weekly dependency audit, a periodic cleanup sweep — you define the work once, attach a cadence, and let CodeHerder file it for you.
What a scheduled task is
A schedule is a recurring producer of ordinary tasks. It stores a frozen copy of the inputs a normal task takes, plus a cron expression. When the cadence fires, the scheduler creates a single, ordinary task from those stored inputs — and then does nothing else.
That spawned task is indistinguishable from a hand-filed one. The existing workflow engine picks it up, staffs it, and runs it through its stages exactly as described in How work flows. The scheduler is only a producer; it adds no new execution semantics.
Each spawned task records which schedule produced it, so it shows up in the activity feed and in a schedule’s Recent runs list. The schedule also appears as a connection in the spawned task’s own lineage — see Where a task came from for how that differs from the task’s Origin row.
Creating a schedule
A schedule takes the same inputs as a task, plus the cadence:
-
The task inputs — title, description, priority, workflow (type), and any required capabilities. These are frozen into the schedule and reused on every fire (see Core concepts for what each means). A schedule can carry two kinds of capability, the same as a task: agent capabilities (what an agent’s launch config must cover to run the spawned tasks) and device capabilities (what device they must place on). See Capabilities for the difference.
Creating or editing a schedule with agent capabilities runs the same check as
ch task create/ch task edit: it’s rejected if no launch config in your workspace covers the set, unless your workspace has no launch configs yet, in which case the check is skipped. Device capabilities only get a shape check, not a fleet check — a schedule can still spawn tasks that no device can ever be placed on if none advertises the tokens you set. -
A cron expression — a standard five-field cron string (for example
0 9 * * MONfor 9 a.m. every Monday). The named shorthands@hourly,@daily,@weekly,@monthly, and@yearlyare also accepted. -
A timezone — an IANA timezone name (for example
Europe/London), defaulting toUTC. Cron without a timezone is ambiguous, so the schedule always carries one; daylight-saving transitions are handled for you. -
Enabled or disabled — disable a schedule to pause it without deleting it, and re-enable it to resume. A disabled schedule never fires automatically.
-
An optional due-offset — make each spawned task due a fixed interval after it is created (for example “due 2 days after spawn”). An absolute due date would be in the past after the first fire, so the offset is relative. The web form asks for this offset in seconds; the CLI takes a duration like
2hor7dinstead. -
A repo set — the repositories each spawned task starts with, from none up to several. With no repo set, each spawned task uses CodeHerder’s automatic choice when it starts. Pass
--no-repoonch schedule createorch schedule editfor tasks that need no checkout. See Tasks that span several repositories for what a repo set is and how a task uses it. -
An optional base branch — where each spawned task’s sandbox branches from, and where its merge requests land, instead of each repo’s own default branch. See Choosing a task’s base branch.
-
Optional pre-filled fields — you can seed a canned field value, such as a standing proposal, that is written onto every spawned task. A field key must be one the schedule’s type declares; naming one it doesn’t is rejected when you save the schedule, not when it later fires.
-
An optional end date — set a date and time after which the schedule stops firing automatically. Once that moment passes the schedule remains in your list but will not fire again; push the end date further out (or clear it) to resume firing. Set it in the web app when you create a schedule, and add, change, or clear it any time afterwards from the web app or the CLI — see From the CLI, below.
For example, a weekly dependency audit against one repo:
ch schedule create --title "Weekly dependency audit" --cron "0 9 * * MON" \
--type story --repo api --description-file audit-brief.md
Every Monday at 9 a.m. this spawns a new task against the api repo, ready for an agent to pick up and work through the normal workflow.
Turning a task into a schedule
If you already have a task that turned out to be recurring work, you don’t have to rebuild it as a schedule from scratch. Both the web app and the CLI can seed a new schedule directly from an existing task.
In the web app: open the task and choose Make it recurring… from its action menu. A dialog asks for a cron expression (required) and, optionally, a timezone (defaults to UTC) and a title (defaults to the task’s own title). Submitting it creates the schedule and takes you to its page.
From the CLI:
ch task schedule <taskId> --cron "0 9 * * MON" [--tz <IANA>] [--title "<title>"] [--disabled]
--disabled creates the schedule paused, the same as on ch schedule create. On success the CLI prints the new schedule’s ID.
What carries over: title, description, priority, workflow (type), agent and device capabilities, the repo set (any archived repo is dropped), the base branch, and any field value a person filled in by hand. A field a workflow stage writes automatically, a prototype field, and a child-task field are all skipped. A checklist field carries over with every item unchecked, so the new schedule’s spawned tasks start with a fresh checklist each time.
What’s dropped: a schedule has no concept of a parent task, a dependency, a join policy, or a due date. The web dialog lists which of these the source task actually has set before you submit, so you know what you’re leaving behind. The CLI prints the same list as warnings while it creates the schedule. The new schedule also starts with no due offset and no end date — add either afterwards with ch schedule edit or the web app’s edit form.
Where it lives
In the web app
Schedules live under Tasks ▸ Schedules. From there you can:
- see every schedule, its next and last run, and a Schedule column with a plain-language summary of the cadence (for example “Every day at 03:00”) above the raw cron expression, with the timezone beside it — a cron expression too complex to summarize plainly just shows the cron and timezone on their own,
- create one (the form reuses the New Task fields — including its repo set and base branch — and adds the cron expression, timezone, enabled toggle, due-offset, task-field seeds, and end date),
- pause a schedule by disabling it (or resume by re-enabling), edit it — including its repo set, base branch, and end date — or delete it,
- run it now (the button uses the schedule’s saved inputs; for one-off changes, use
ch schedule run-now— see From the CLI), and - open a schedule to see its Recent runs — up to the 10 most recent tasks it has spawned. Each run also shows a filed summary, such as “3 filed: 2 code, 1 review” (
ch schedule showprints the raw status names) — the tasks an agent filed while working that run’s task, in the schedule’s own workspace. Below the summary, up to 20 of those filed tasks are listed newest first with a status badge; if the run filed more than that, the list ends with “and N more”. See Where a task came from for what “filed” means.
A schedule’s pause, edit, delete, and run-now controls only appear for members who can actually use them — see Who can change a schedule below.
From the CLI
The ch schedule verb group mirrors the web app. Everywhere below, <id> also accepts the schedule’s title instead of its full ID — see Using the ch CLI for how CodeHerder matches a record by name.
ch schedule create --title "<title>" --cron "<expr>" [--tz <IANA>] [--priority high|normal|low]
[--type <type>] [--cap <capability> …] [--device-cap <capability> …]
[--due-in <duration>] [--disabled]
[--repo <ref>] … [--no-repo] [--base-branch <branch>] [--description-file <file>]
[--field <key>=<value>] …
ch schedule list [--enabled | --disabled] [--limit N] [--cursor <token>]
ch schedule show <id> # next/last run, recently spawned tasks (with what each filed), stored field seeds
ch schedule edit <id> [--title <t>] [--cron …] [--tz …] [--priority …] [--type …]
[--cap <capability> … | --clear-caps]
[--device-cap <capability> … | --clear-device-caps]
[--due-in <duration>|""] [--ends-at <RFC3339>|""]
[--repo <ref>] … [--no-repo] [--base-branch <branch>|""]
[--field <key>=<value>] … # alias: update
ch schedule enable <id> # resume: turn automatic firing back on
ch schedule disable <id> # pause: stop automatic firing without deleting it
ch schedule delete <id>
ch schedule run-now <id> [--field <key>=<value>] … [--description <text>|""]
--cap and --device-cap are both repeatable and, on edit, replace the whole set — the same as --repo below. --clear-caps empties the agent-capability set and --clear-device-caps empties the device-capability set; each is mutually exclusive with its own --cap/--device-cap flag on that call. ch schedule show <id> prints a type row — it reads task (default) when the schedule names no type at all — and, when the cron expression has a plain-language summary (see In the web app, above), a runs row right after the cron row.
Its RECENT RUNS table adds a FILED column, and for each run that filed anything, the command prints a block underneath headed FILED BY RUN <runId> (<title>) listing what that run filed, newest first. When a run filed more than 20 tasks, the block ends with a line like … 5 more (ch task lineage <runId> --direction down), pointing you at the full list. See Where a task came from for what “filed” means.
Use ch schedule enable <id> and ch schedule disable <id> to pause or resume a schedule — these are the canonical way to flip a schedule’s enabled state from the CLI. ch schedule list --enabled/--disabled reads that same state back, narrowing the list instead of changing it — see Listing enabled and disabled records.
Setting an end date from the CLI. ch schedule edit sets or clears a schedule’s end date; ch schedule create has no end-date flag, so give a brand-new schedule an end date either in the web app’s create form or by running ch schedule edit right after you create it.
ch schedule edit <id> --ends-at 2026-12-31T23:59:00Z # stop firing after this moment (RFC3339)
ch schedule edit <id> --ends-at "" # clear the end date — resume firing indefinitely
The timestamp must be an RFC3339 timestamp (include a timezone offset or Z); an empty string clears the end date, and the CLI rejects a malformed timestamp.
Clearing the due offset from the CLI. ch schedule edit <id> --due-in "" clears the due offset, the same way --ends-at "" clears the end date — spawned tasks go back to having no due date. Omitting --due-in on edit leaves the current offset unchanged.
Setting a repo set or base branch from the CLI. --repo <ref> takes a repo’s name or ID; repeat it to attach more than one, the same as ch task create (see Tasks that span several repositories). On ch schedule edit, --repo replaces the whole repo set rather than adding to it — pass every repo you want the schedule to keep, not only the one you’re changing. --base-branch <branch> sets the branch each spawned task’s sandbox starts from. On edit, --base-branch "" clears it back to each repo’s own default branch, the same as never setting one. Omitting either flag on edit leaves the current value unchanged.
ch schedule edit <id> --repo api --repo web-client # the schedule's repo set is now exactly these two
ch schedule edit <id> --base-branch "" # spawned tasks fall back to each repo's default branch
Running once with different inputs. ch schedule run-now can override a schedule’s inputs for a single run. The overrides apply to that one run only. The saved schedule does not change, and the next automatic run or a plain run-now uses the saved inputs again.
ch schedule run-now "Weekly dependency audit" --field focus="security fixes only"
ch schedule run-now "Weekly dependency audit" --description "Just check the payments repo."
Here focus stands for a field that your schedule’s type declares. See Seeding task fields for what a seed is.
--field <key>=<value>is repeatable. Each one replaces only its own key, and every other saved seed keeps its value. You can also set a field the schedule has no seed for, as long as the type declares it.--field key=with an empty value is an error onrun-now. Onch schedule edit, the same form removes the seed.--description "<text>"sets the description for this run.--description ""gives this run an empty description. If you leave the flag out, the run keeps the saved description.- CodeHerder checks the overrides first. An invalid one is refused before any task is created, and the schedule records no error.
The command prints the new task, then one Override: line for each field you changed, and one for the description if you changed it.
As elsewhere in the CLI, a multi-line description (--description) is passed with --description-file or on stdin, never inline.
Who can change a schedule
Any workspace member can create a schedule and see, list, and open every schedule in the workspace — creating your own recurring automation is self-service.
Changing one that already exists is tighter: editing, pausing, resuming, deleting, or running a schedule now requires you to be the schedule’s creator, or a workspace owner or admin. Pausing and resuming count as a change here too, the same as editing or deleting — it isn’t only the destructive actions that are gated.
A member who didn’t create a given schedule, and isn’t an owner or admin, sees that schedule’s page as a read-only view — the pause, edit, delete, and run-now controls are gone, replaced by a notice naming who can use them (see Why a control is missing). The equivalent CLI command (edit, enable, disable, delete, run-now) is refused with a permission error the same way. There’s also a broader rule that has nothing to do with ownership: an agent can’t create or change a schedule at all from inside a task or sandbox session, even one with member access — the same restriction that applies to a repo’s default branch, described in Managing repositories. Only your own account, from the web app or with your own CLI credentials outside a session, can manage a schedule.
See Members, teams, and roles for what the admin and owner roles can otherwise do.
Seeding task fields
A schedule can pre-fill task fields on every task it spawns — for example, a standing proposal or a recurring checklist item. CodeHerder writes each seed onto the spawned task at creation time; the assignee or agent can then edit those values normally.
In the web app: once you pick a type in the create or edit form, a Task field seeds section appears — one text box per field defined by that type. Enter the value you want pre-populated on every spawned task; leave a box empty to skip that field. Child-task fields are not seedable and do not appear in the list. On the edit form, clearing a seed box and saving removes that seed: tasks spawned after that point will no longer have that field pre-filled.
From the CLI: ch schedule create and ch schedule edit seed the task’s native description via --description. A per-type field seed (such as a standing proposal or checklist) is set with --field <key>=<value>, repeatable for more than one; the key must be one the schedule’s type declares — an unknown key is rejected with an error naming the field, the same check the web app’s seed form applies. On edit, --field merges into the stored seeds rather than replacing them: --field key= (an empty value) removes that one seed, and every other stored seed is left alone. ch schedule show <id> prints every stored seed under a FIELD SEEDS section. To change a seed for one run only, use ch schedule run-now <id> --field <key>=<value>; it never writes the value back to the schedule.
There’s one exception: ch task schedule (see Turning a task into a schedule, above) does carry a source task’s own field values over onto the new schedule, since it’s seeding the schedule from a task that already has them set — it just can’t add or change seeds that aren’t already on that task.
How schedules behave
A few behaviours are worth knowing before you rely on a schedule:
- Fire-once on catch-up — no backfill. If the server is down across several intervals, the schedule fires once when it comes back and then computes its next run forward from that point. It does not replay every missed interval, so an outage never produces a storm of tasks.
- One open task at a time. A schedule will not spawn a new task while a task it previously spawned is still open (not yet finished or cancelled). If the cron fires while that prior task is still running, the fire is skipped silently — no error is raised, the schedule’s last-run time does not update, and the skipped fire does not appear in Recent runs. The schedule simply re-evaluates at its next cron slot and spawns again once the prior task has finished or been cancelled.
- Cron is minute-resolution. The finest cadence a cron expression can express is once per minute. The schedule form recommends a minimum of five minutes between fires; this is a guideline, not a server-enforced limit.
- Run-now bypasses enabled and the end date, but not the open-task guard. Running a schedule manually fires it immediately even when it is disabled, or after its end date has passed — handy for testing a schedule before you turn it on. Run-now is still subject to the same one-open-task guard, but unlike a skipped automatic fire, it tells you: if a prior task from this schedule is still open, run-now refuses and says so, rather than quietly creating a duplicate. Wait for that task to finish or cancel it, then run-now again — or just let the schedule’s own next cron fire pick it up. A manual run does not move the next scheduled fire. From the CLI, run-now can also take one-off inputs; a bad override is refused before any task is created.
- Schedules outlive their creator. A schedule belongs to its workspace, not to the member who created it. If that member leaves the workspace, the schedule keeps firing on schedule — but from then on, only a workspace owner or admin can change or remove it (see Who can change a schedule, above).
- Disabling the workflow a schedule depends on is refused, unless forced. If you try to disable a workflow (type) that an enabled schedule still resolves to, the disable is refused. A schedule with no type set uses the default
tasktype, so disablingtaskis also refused while such a schedule is enabled. The web app shows an error naming the conflict. From the CLI,ch workspace workflow disable <type>lists every schedule still blocking it, andch workspace workflow disable <type> --forcedisables the type anyway. See Task types for the full disable/restore flow. Forcing it through doesn’t stop those schedules: each keeps firing on its own cadence, but every fire now fails, and the failure shows as the schedule’s last error. - An unusable repo blocks a fire, not the schedule. A repo can go bad after the schedule was written — it gets archived, or it stops being reachable from this workspace. Every fire re-checks the whole repo set first. If any repo in it has gone bad, that fire spawns nothing and records the reason instead of creating a task with a wrong or missing repo. You’ll see the reason on the schedule’s page and in
ch schedule show <id>. Running the schedule manually against the same set is refused the same way, instead of quietly creating a broken task. To recover, uncheck the unusable repo in the schedule’s Repos picker (it stays checked and marked unavailable until you do) and save — or replace the repo set from the CLI. This check only covers the repos themselves: a base branch that’s since been deleted isn’t caught until the spawned task’s sandbox is created.
Related guides
- How work flows — the pipeline a spawned task runs through
- Core concepts — task inputs and the object model
- Tasks that span several repositories — what a repo set is and how a task uses it
- Choosing a task’s base branch — how a base branch is resolved and applied
- Capabilities — agent capabilities, device capabilities, and the launch-config coverage check
- Task types — the catalog a schedule’s type (workflow) comes from, and disabling one
Last updated