Agent Scheduling
Crewship includes a built-in scheduler that triggers agent runs on a cron schedule. Scheduled runs are fully autonomous — the agent receives a prompt, executes inside its crew container, and the result is recorded as a run with theSCHEDULED trigger type.
How It Works
The scheduler is a background goroutine that manages a pool of cron jobs, one per enabled agent schedule. On startup it loads all agents withschedule_enabled = 1 from the database and registers their cron expressions. Each tick of a cron job triggers a full agent run cycle.
Enabling a Schedule
Schedules attach to a saved routine (pipeline) and fire it on a cron expression. Manage them viacrewship routine schedules:
| Subcommand | Purpose |
|---|---|
list [--slug <routine>] | List schedules in this workspace, optionally filtered by target routine. |
create --slug <routine> --cron '<expr>' [--name ...] [--timezone <IANA>] [--inputs <json>] [--enabled=false] | Create a schedule. --slug and --cron are required; defaults to UTC and enabled. |
update <id> [--cron ...] [--timezone ...] [--name ...] [--enabled / --enabled=false] [--inputs <json>] | Patch one or more fields. |
enable <id> | Enable a paused schedule (fires on next 30 s tick). |
disable <id> | Pause a schedule without deleting it. |
now <id> | Force-fire out of cycle (useful for smoke tests). |
delete <id> [--yes] | Permanently delete; prompts for confirmation unless --yes. |
/api/v1/workspaces/{ws}/pipeline-schedules — the CLI is a thin wrapper around those endpoints, so server-side behavior (cron parsing, 30 s tick resolution, timezone handling) is identical.
Pinning a schedule to a routine version
By default a schedule fires the routine’s head (latest) version — an edit to the routine immediately changes what the next tick runs. For production schedules that must not drift when an agent (or teammate) edits the routine, pin the schedule to an immutable version:- A pinned schedule executes exactly the pinned version’s definition on every fire, no matter how far head has moved. The run record stores the executed version (
pipeline_version) and that version’sdefinition_hash, so the Runs view always shows what actually ran. - The pin survives unrelated updates: patching the cron, timezone, name, inputs, or enabled state keeps the existing pin. Only an explicit
--pin-version N/--unpin(API:target_pipeline_version: N/: null) changes it. - If the pinned version no longer exists, the fire fails with a legible error and raises the standard scheduled-run-failed inbox alert (MANAGER-targeted). It deliberately does not fall back to head — silently running an unexpected definition is the exact hazard pinning exists to prevent. Fix by re-pinning to an existing version or unpinning.
- Governance still reads the live routine: a
disabledorproposedroutine refuses to fire even when pinned, and per-step operator overrides apply on top of the pinned definition just as they do on head. - A pinned run that parks on an approval gate resumes against the same pinned version, even if head moved while it waited.
- Webhooks support the identical pin (
target_pipeline_versionon the webhook;crewship routine webhooks create --pin-version N). A webhook whose pinned version is missing answers409instead of dispatching.
slug@vN.
Fire outcomes (last_status)
Each fire records one of:
| Status | Meaning |
|---|---|
COMPLETED | The run finished successfully. |
FAILED | The run errored (or the pinned version was missing). A failed_run inbox alert is raised for MANAGERs. |
SKIPPED | The target routine is not active (proposed / disabled); the tick advanced quietly. |
WAITING | The run parked on a human approval gate (wait step). This is a healthy, non-terminal outcome — the run resumes when the approval lands. No failure alert is raised; the waitpoint itself creates the approval inbox card. |
Per-agent Read it back with
schedule_cron fields described elsewhere in this guide are a separate legacy path. New work should go through crewship routine schedules against a saved routine.For a quick single-agent cron, the per-agent path is now settable straight from the CLI (previously API-only):crewship agent get <agent> — when a cron is set the
detail view surfaces a Schedule row (cron (enabled|disabled)), the
Schedule Prompt, and the resolved Next Run / Last Run, so you can
confirm the cron is live without hitting the raw API. Clear it with
--schedule-cron '' (or pause with --schedule-enabled=false).Runs that the scheduler dispatches this way get the tighter routine turn cap automatically — the internal orchestrator.RoutineMaxTurns (20), applied to scheduled dispatches (not a CLI flag you pass).Cron Expression Format
The scheduler uses standard 5-field cron expressions:| Expression | Schedule |
|---|---|
0 9 * * 1-5 | Every weekday at 9:00 AM |
*/15 * * * * | Every 15 minutes |
0 0 * * * | Daily at midnight |
30 8 1 * * | 8:30 AM on the 1st of every month |
0 */2 * * * | Every 2 hours |
Execution Flow
Each scheduled trigger follows this sequence:Chat Session Creation
A new chat session is created with a deterministic ID (format:
sched_{unixnano}_{random_hex}). The chat title is set to "Scheduled: {agent_name}".Chat Resolution
The
ResolveChat call loads the full agent context: credentials, system prompt, skills, MCP servers, network policy, and crew membership. This ensures the scheduled run has the same capabilities as an interactive run.Container Management
If a container provider is configured, the scheduler ensures the crew container is running before execution. Default resource limits:
| Resource | Default |
|---|---|
| Memory | 4096 MB |
| CPUs | 2.0 |
Run Record
A run record is created with trigger type
SCHEDULED before execution begins. Metadata includes the CLI adapter, crew info, and a scheduled tag.Agent Execution
The agent runs through
orchestrator.RunAgent with the same pipeline as interactive runs: sidecar proxy, credential injection, memory, and conversation history.Timeout
Each scheduled run has a 45-minute timeout. If the agent does not complete within this window, the context is cancelled and the run is marked as failed.This is separate from the per-agent
timeout_secs setting, which controls the LLM execution timeout within the container. The 45-minute limit is the outer boundary for the entire scheduled run cycle including container startup and chat resolution.Monitoring
Timestamp Fields
The scheduler maintains two fields on each agent:| Field | Updated | Description |
|---|---|---|
schedule_last_run | After each run | ISO 8601 timestamp of the last completed run |
schedule_next_run | After each run and on schedule update | ISO 8601 timestamp of the next scheduled run |
schedule_next_run always reflects the next trigger time so monitoring dashboards stay accurate.
Run Records
Scheduled runs appear in the standard agent runs list withtrigger = "SCHEDULED":
trigger:"SCHEDULED"tags:["scheduled", "{cli_adapter}"]duration_ms: execution timetotal_cost_usd: LLM cost (if reported)num_turns: conversation turnsstatus:COMPLETEDorFAILED
Dynamic Updates
When an agent’s schedule is modified via the API, the scheduler hot-reloads the cron entry without restart. TheUpdateSchedule method:
- Removes the old cron entry (if any)
- Registers the new cron expression
- Immediately calculates and persists the next run time
schedule_enabled: false) removes the cron entry and stops future triggers. The schedule_last_run timestamp is preserved for audit purposes.
Error Handling
If any step in the trigger pipeline fails (chat creation, resolution, container startup), the scheduler:- Logs the error
- Updates
schedule_next_runto the next trigger time - Does not update
schedule_last_run(only updated on successful execution start)
What’s Next
- Orchestration — multi-agent missions with task dependencies
- Agent memory — persistent agent memory across runs