> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduling

> Cron-based automated agent runs with custom prompts, timeout management, and execution tracking.

# 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`:

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

```bash theme={null}
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:

```bash theme={null}
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:

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

<Note>
  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):

  ```bash theme={null}
  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).
</Note>

## 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:

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

<Warning>
  Invalid cron expressions are rejected at registration time. The API call will return an error if the expression cannot be parsed.
</Warning>

## Execution Flow

Each scheduled trigger follows this sequence:

<Steps>
  <Step title="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}"`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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     |
  </Step>

  <Step title="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.
  </Step>

  <Step title="Agent Execution">
    The agent runs through `orchestrator.RunAgent` with the same pipeline as interactive runs: sidecar proxy, credential injection, memory, and conversation history.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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

```go theme={null}
ctx, cancel := context.WithTimeout(s.ctx, 45*time.Minute)
```

<Info>
  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.
</Info>

## 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 |

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"`:

```bash theme={null}
# 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

* [Orchestration](/guides/orchestration) -- multi-agent missions with task dependencies
* [Agent memory](/guides/agent-memory) -- persistent agent memory across runs
