Skip to content

Schedules reference

The declarative reference for schedules: the trigger schema, the older standalone schedules block, the self-MCP tools a keeper uses to manage its own schedules, and the REST surface behind the Triggers tab.

A schedule is a trigger persisted in a project’s project.yaml under the triggers map, keyed by name. Each trigger is trigger (when) + run (what) + enabled:

project.yaml
triggers:
morning-triage:
trigger: # WHEN — the schedule
type: schedule
cron: "0 9 * * *" # 5-field cron (or @daily / @hourly), host-local
# interval: "30m" # …or a duration; exactly ONE of cron / interval
run: # WHAT — the fired agent turn
prompt: Triage overnight issues and post a summary.
# promptFile: triage.md # …or a .md file under .paddock/triggers/; exactly ONE
session: new # "new" (default) | "resume"
tools: [Read, Grep] # allow-list = capability; [] = run as the keeper
model: "" # optional model override
permissionMode: acceptEdits # default | acceptEdits | bypassPermissions | plan
maxTurns: 30 # optional cap (default 30)
maxSpawnDepth: 0 # optional; 0 = may not spawn children
enabled: true

The fields:

FieldValuesNotes
trigger.typescheduleThe discriminant. Triggers can also be event (an onArchive / afterTurn lifecycle event) or webhook — the latter is shape-reserved but not yet fireable (no inbound ingress yet). The afterTurn event is reserved for the built-in sweeper/curator.
trigger.cron5-field stringe.g. 0 9 * * *; @daily / @hourly accepted. Host-local time.
trigger.intervalduration stringe.g. 30m, 1h, 15m.
run.promptstringThe instruction the firing runs.
run.promptFile*.md nameRead fresh at firing from .paddock/triggers/; traversal and non-.md are rejected.
run.sessionnew | resumenew (default) = a fresh chat each firing; resume = one owned accreting session.
run.toolsstring arrayThe fired agent’s allow-list. Empty (default) = runs as the keeper with full tools; non-empty = its own scoped trigger-<slug>-<name> agent with exactly those tools.
run.modelstringOptional per-trigger model override.
run.permissionModedefault | acceptEdits | bypassPermissions | planPermission mode the fired turns run under.
run.maxTurnsintegerUpper bound on agent turns (default 30).
run.maxSpawnDepthinteger ≥ 0Bounds internal spawning (0 = may not spawn).
enabledbooleanWhether it’s armed. A trigger created through the UI or MCP defaults to disabled.

The schedule editor maps one-to-one onto this schema:

The schedule editor form, mapping onto the trigger schema fields

Before triggers were unified, schedules lived in their own top-level schedules map. That form is still honored and armed — a simpler shape when you only need a timer:

# project.yaml — legacy standalone schedules (still supported)
schedules:
nightly-scan:
type: cron # cron | interval
cron: "0 3 * * *"
prompt: Scan for dependency advisories.
resume_session: false # note: snake_case here (false = fresh chat each fire)
enabled: true

Note the differences from the unified run block: resume_session (snake_case) in place of session: new|resume, and promptFile here resolves under .paddock/schedules/ rather than .paddock/triggers/. New work is better expressed as a triggers entry (it’s what the Triggers tab and the MCP tools read and write), but existing schedules blocks keep working.

When the trigger-management MCP is enabled — the self-MCP write layer (PADDOCK_SELF_MCP + PADDOCK_SELF_MCP_WRITE) plus PADDOCK_HOOKS_MCP (or a per-project hooksMcpEnabled) — the keeper is given four tools. They manage every trigger type; for a schedule, use type: "schedule".

Create or update a trigger (a partial patch — an enabled-only call just flips the toggle). Parameters (note the snake_case MCP argument names):

ParameterTypeNotes
namestringThe trigger’s stable key. Required.
typeschedule | event | webhookOmit on an edit to keep the existing when. webhook is reserved (not yet fireable); afterTurn events are reserved for the curator.
cronstringFor a schedule: a 5-field expression (host-local). Exactly one of cron / interval.
intervalstringFor a schedule: a duration (30m, 1h).
promptstringInline instruction. Provide this or prompt_file.
prompt_filestringA .md file under .paddock/triggers/, read at firing.
sessionnew | resumenew (default) = fresh chat each firing; resume = one owned session.
toolsstring / arrayAllow-list (one per line, comma-separated, or a JSON array). Empty = tool-less/keeper.
modelstringModel override for the fired agent.
permission_modedefault | acceptEdits | bypassPermissions | plan
max_spawn_depthnumber0 = may not spawn.
max_turnsnumberCaps the fired turn.
enabledbooleanDefaults false on a new trigger; omitted on an existing one leaves it unchanged.
projectstringTarget project slug; omit for the current project.

List a project’s triggers (all types) — what’s declared, and their state.

Delete a trigger by name.

Fire a trigger immediately, without waiting for its cron or event — the same “Run now” path the Triggers tab uses. Handy for testing a prompt before you leave it to a schedule. Like the REST route, it starts a real turn, so it is gated with the rest of this family.

The Triggers tab drives the unified trigger surface. These endpoints are always available (they don’t require the schedule-mutation gate):

Method & pathPurpose
GET /api/projects/:slug/triggersList triggers + the picker catalog (grantableTools, events, triggerTypes).
GET /api/projects/:slug/triggers/:nameGet one trigger.
PUT /api/projects/:slug/triggers/:nameCreate or replace one trigger (full { trigger, run, enabled } record). Enable/disable is this call with enabled flipped.
DELETE /api/projects/:slug/triggers/:nameDelete and disarm one trigger.
GET /api/projects/:slug/triggers/runtimeJust the armed / next-fire runtime state, so the Triggers tab can poll cheaply without re-fetching the config and picker catalog. A static segment, matched before /:name.
POST /api/projects/:slug/triggers/:name/runRun now — fires through the same hub path a cron or event fire uses. 202 with the session id; 404 for an unknown trigger, 502 if the fire started no chat.