Scheduling lets you run missions automatically at a given time — either once (a deploy next Tuesday at 14:00) or on a recurring basis (nightly tests, daily health checks, weekly security audits). You define a cron expression or an ISO timestamp, and Polpo takes care of the rest. No external cron job or CI pipeline needed.
Quick Start
Use the create_schedule orchestrator tool to schedule a mission. The mission status transitions to scheduled (one-shot) or recurring:
Recurring (cron)
One-shot (timestamp)
create_schedule(missionId, "0 2 * * *", recurring=true)
This runs every day at 02:00. After each run completes, the mission returns to recurring status and waits for the next scheduled time.create_schedule(missionId, "2026-03-15T14:00:00Z")
This runs once at the specified time. On success, the mission transitions to completed and the schedule is disabled. On failure, it returns to scheduled for automatic retry.
Mission Lifecycle
Scheduled missions use dedicated statuses instead of remaining in draft:
One-shot: draft → scheduled → active → completed (success) / scheduled (failure, auto-retry)
Recurring: draft → recurring → active → recurring (always returns for next tick)
| Recurring | One-Shot |
|---|
| Mission status | recurring | scheduled |
| Schedule expression | Cron expression (0 2 * * *) | Cron or ISO timestamp (2026-03-15T14:00:00Z) |
| After success | Returns to recurring | Transitions to completed, schedule disabled |
| After failure | Returns to recurring | Returns to scheduled for retry |
| Use case | Repeated tasks (tests, audits, health checks) | Single future event (deploy, migration) |
A recurring mission never reaches completed or failed status — it always returns to recurring after each execution cycle. The individual task statuses reflect success/failure of each run.
Cron Expressions
Polpo uses standard 5-field cron expressions:
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
Syntax
| Syntax | Example | Meaning |
|---|
| Number | 30 | Exact value |
* | * | Every value in range |
| Range | 1-5 | Values 1 through 5 |
| Step | */15 | Every 15th value |
| List | 1,3,5 | Values 1, 3, and 5 |
| Range + Step | 0-30/10 | 0, 10, 20, 30 |
Common Patterns
| Expression | When it runs |
|---|
0 2 * * * | Every day at 02:00 |
*/15 * * * * | Every 15 minutes |
0 9 * * 1-5 | Weekdays at 09:00 |
0 0 1 * * | First day of every month at midnight |
30 8,12,18 * * * | At 08:30, 12:30, and 18:30 daily |
0 22 * * 0 | Every Sunday at 22:00 |
0 6 * * 1 | Every Monday at 06:00 |
Shorthand aliases like @daily, @weekly, @monthly are not supported — use the 5-field format. Seconds, L, W, and # modifiers are also not supported.
Managing Schedules
| Tool | Description |
|---|
create_schedule | Schedule a mission (sets status to scheduled or recurring) |
list_schedules | List all schedule entries |
update_schedule | Change expression, recurring/one-shot mode, or enabled flag |
delete_schedule | Remove schedule and reset mission to draft |
Disabling vs Deleting
- Disable:
update_schedule(missionId, enabled=false) — keeps the schedule configuration but stops execution
- Re-enable:
update_schedule(missionId, enabled=true) — resumes execution
- Delete:
delete_schedule(missionId) — permanently removes the schedule and resets the mission to draft
- Abort: aborting a scheduled mission automatically removes its schedule
- Switch mode:
update_schedule(missionId, recurring=true) — changes between one-shot and recurring
End Date
Recurring schedules can have an optional end date — an ISO timestamp after which the schedule automatically expires. When the scheduler checks a mission at trigger time and finds the end date has passed, it:
- Disables the schedule entry
- Transitions the mission to
completed
- Emits a
schedule:expired event
Setting an End Date
create_schedule(missionId, "0 2 * * *", recurring=true, endDate="2026-06-30T23:59:00Z")
This runs every day at 02:00 until June 30, 2026. After that date, the mission is marked as completed automatically.
Removing an End Date
To remove an end date from an existing schedule (making it run indefinitely):
update_schedule(missionId, endDate=null)
Behavior
| Scenario | What happens |
|---|
| End date in the future | Schedule runs normally |
| End date in the past at trigger time | Schedule disabled, mission → completed |
End date removed (null) | Schedule runs indefinitely |
| One-shot scheduled mission with end date | End date is checked before execution |
The end date is checked at trigger time, not at registration time. A mission registered with a future end date will run normally until that date passes. The endDate is stored on the Mission object and persisted across restarts.
Examples
Nightly integration tests
create_schedule(missionId, "0 2 * * *", recurring=true)
Mission with tasks:
{
"tasks": [
{
"title": "Run full test suite",
"description": "Run all integration and unit tests. Report any failures.",
"assignTo": "test-agent"
},
{
"title": "Generate coverage report",
"description": "Generate a coverage report and save to coverage-report.md.",
"assignTo": "report-agent",
"dependsOn": ["Run full test suite"]
}
]
}
Scheduled one-time deploy
create_schedule(missionId, "2026-03-15T14:00:00Z")
Mission with tasks:
{
"tasks": [
{
"title": "Build release artifacts",
"assignTo": "build-agent"
},
{
"title": "Deploy to production",
"assignTo": "devops-agent",
"dependsOn": ["Build release artifacts"]
}
]
}
Cancelling a Scheduled Run
The before:schedule:trigger hook fires before each scheduled execution. Return a cancellation to skip that run — useful for maintenance windows or conditional logic:
{
"hooks": {
"before:schedule:trigger": {
"command": "node scripts/check-maintenance-window.js"
}
}
}
If the hook cancels, the schedule remains active — it will try again at the next cron occurrence. The run is skipped, not deleted.
Internals
The scheduler is tick-driven — it runs on every Polpo tick (default every 30 seconds), checks all registered schedules, and triggers mission execution when a schedule is due.
Lifecycle
- Registration — on startup, Polpo scans all missions with a
schedule field in scheduled or recurring status and registers them. Missions scheduled later are registered via create_schedule.
- Check — on every tick, the scheduler evaluates each active schedule. If
nextRunAt is in the past, the mission is triggered.
- Trigger — the
before:schedule:trigger hook runs (can cancel). If not cancelled, the mission is executed (transitions to active).
- After execution — for recurring missions, the status returns to
recurring and the next run time is calculated. For one-shot missions on success, the status becomes completed and the schedule is disabled. On failure, the status returns to scheduled for retry.
Triggerable States
The scheduler only triggers missions in scheduled or recurring status:
scheduled — one-shot mission waiting for trigger
recurring — recurring mission waiting for next cron tick
Missions in active, paused, draft, completed, failed, or cancelled status are skipped. This prevents double execution of active missions.
Events
| Event | When | Payload |
|---|
schedule:created | A schedule is registered | scheduleId, missionId, nextRunAt |
schedule:triggered | A scheduled mission starts executing | scheduleId, missionId, expression |
schedule:completed | A scheduled mission finishes | scheduleId, missionId |
See Events reference for the full event catalog.
Schedule Entry Fields
Each schedule is tracked internally as a ScheduleEntry:
| Field | Type | Description |
|---|
id | string | Unique ID (sched-{missionId}) |
missionId | string | The mission to execute |
expression | string | Cron expression or ISO timestamp |
recurring | boolean | Whether this schedule repeats (derived from mission status) |
enabled | boolean | Whether the schedule is active |
lastRunAt | string? | When it last ran |
nextRunAt | string? | When it will run next |
createdAt | string | When the schedule was created |