Skip to main content

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 the SCHEDULED 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 with schedule_enabled = 1 from the database and registers their cron expressions. Each tick of a cron job triggers a full agent run cycle.
Scheduler goroutine
    |
    v
cron library fires at scheduled time
    |
    v
triggerAgent(agent)
    |
    +--> Create chat session (deterministic ID)
    +--> Resolve chat (credentials, system prompt, skills)
    +--> Ensure crew container is running
    +--> Create run record (trigger: SCHEDULED)
    +--> Execute agent via orchestrator.RunAgent
    +--> Update run record (COMPLETED or FAILED)
    +--> Update schedule_last_run and schedule_next_run

Enabling a Schedule

Schedules attach to a saved routine (pipeline) and fire it on a cron expression. Manage them via crewship routine schedules:
SubcommandPurpose
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.
crewship routine schedules create --slug summarize-text \
  --name "daily-summary" --cron "0 9 * * *" --inputs '{"text":"..."}'
crewship routine schedules list --slug summarize-text
crewship routine schedules now <schedule_id>
The same operations are also available on the REST API under /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:
crewship routine versions daily-digest          # see the version history
crewship routine schedules create --slug daily-digest \
  --cron "0 8 * * *" --pin-version 3            # every fire executes v3
crewship routine schedules update <schedule_id> --pin-version 4   # re-pin
crewship routine schedules update <schedule_id> --unpin           # track head again
Pinning semantics:
  • 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’s definition_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 disabled or proposed routine 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_version on the webhook; crewship routine webhooks create --pin-version N). A webhook whose pinned version is missing answers 409 instead of dispatching.
The schedules list shows the pin in the ROUTINE column as slug@vN.

Fire outcomes (last_status)

Each fire records one of:
StatusMeaning
COMPLETEDThe run finished successfully.
FAILEDThe run errored (or the pinned version was missing). A failed_run inbox alert is raised for MANAGERs.
SKIPPEDThe target routine is not active (proposed / disabled); the tick advanced quietly.
WAITINGThe 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 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 update <agent> --schedule-cron '*/5 * * * *' \
  --schedule-prompt "check the queue" --schedule-enabled
Read it back with 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:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sun=0)
│ │ │ │ │
* * * * *
Examples:
ExpressionSchedule
0 9 * * 1-5Every 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
Invalid cron expressions are rejected at registration time. The API call will return an error if the expression cannot be parsed.

Execution Flow

Each scheduled trigger follows this sequence:
1

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}".
2

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.
3

Container Management

If a container provider is configured, the scheduler ensures the crew container is running before execution. Default resource limits:
ResourceDefault
Memory4096 MB
CPUs2.0
4

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.
5

Agent Execution

The agent runs through orchestrator.RunAgent with the same pipeline as interactive runs: sidecar proxy, credential injection, memory, and conversation history.
6

Result Recording

After execution completes, the run record is updated with:
  • Status: COMPLETED or FAILED
  • Duration in milliseconds
  • Cost and usage metadata (if available from the LLM provider)
The assistant response is persisted to the conversation store and the message count is incremented.

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.
ctx, cancel := context.WithTimeout(s.ctx, 45*time.Minute)
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:
FieldUpdatedDescription
schedule_last_runAfter each runISO 8601 timestamp of the last completed run
schedule_next_runAfter each run and on schedule updateISO 8601 timestamp of the next scheduled run
These fields are updated even on error — 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 with trigger = "SCHEDULED":
# List scheduled runs for an agent
curl http://localhost:8080/api/v1/agents/{id}/runs?trigger=SCHEDULED
Each run record contains:
  • trigger: "SCHEDULED"
  • tags: ["scheduled", "{cli_adapter}"]
  • duration_ms: execution time
  • total_cost_usd: LLM cost (if reported)
  • num_turns: conversation turns
  • status: COMPLETED or FAILED

Dynamic Updates

When an agent’s schedule is modified via the API, the scheduler hot-reloads the cron entry without restart. The UpdateSchedule method:
  1. Removes the old cron entry (if any)
  2. Registers the new cron expression
  3. Immediately calculates and persists the next run time
Disabling a schedule (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:
  1. Logs the error
  2. Updates schedule_next_run to the next trigger time
  3. Does not update schedule_last_run (only updated on successful execution start)
The next scheduled trigger will attempt the full pipeline again. There is no exponential backoff — the cron schedule determines retry timing.

What’s Next