# Routines

A **routine** makes an agent act on its own, on a schedule. It binds the events to deliver on each fire to a **target** — create a fresh session per run, or send the turn into an existing long-lived session — and a cron **schedule**. Every fire records a **routine run** you can list, and can push webhooks.

## Create one

```bash
curl -X POST https://api.agentsky.dev/v1/routines \
  -H "Authorization: Bearer ast_..." -H "Content-Type: application/json" \
  -d '{
    "name": "Weekly compliance scan",
    "target": { "type": "new_session", "agent": "agent_..." },
    "initial_events": [
      { "type": "user.message", "content": [{ "type": "text", "text": "Run the weekly compliance scan." }] }
    ],
    "schedule": { "type": "cron", "expression": "0 20 * * 5", "timezone": "America/New_York" }
  }'
```

The response echoes the routine with `schedule.upcoming_runs_at` — the next (up to 5) fire times — so you can confirm the cron before it runs. `POST /v1/routines/{id}/run` fires immediately, outside the schedule, to test it.

## Two target modes

- `{"type": "new_session", "agent": "agent_...", "environment_id"?, "vault_ids"?}` — each fire creates a fresh session (lazily provisioned) and delivers `initial_events` as its first turn. Sessions are ordinary sessions: they show up in lists, bill normally, and keep their history.
- `{"type": "session", "session_id": "sess_..."}` — each fire sends `initial_events` as a turn into the same long-lived session, so the agent keeps its memory, files, and history across runs. If the session already has a turn in flight at fire time, the fire is skipped and recorded as a failed run (`session_busy_error`); nothing queues, and the next occurrence tries again.

## Schedule semantics

5-field POSIX cron (`minute hour day-of-month month day-of-week`, day-of-week 0–7 with both 0 and 7 = Sunday) plus a required IANA timezone. Extended syntax (`L W # ?`, `@daily`, seconds fields) is rejected. Matching is literal wall-clock in the configured timezone: `0 20 * * *` in `America/New_York` fires at 8 PM local year-round. On DST days, a time that doesn't exist fires at the first valid instant after the gap and a time that occurs twice fires twice — schedule outside 1–3 AM local (or in UTC) if that matters. Missed occurrences are never backfilled: any number of missed fires collapse into one at the next tick. Minimum granularity is one minute. `schedule` is nullable — a routine without one only fires manually.

## Budgets

Pass the optional `budget` (same shape as a session budget — see the API reference) to bound **each run separately**:

- new_session mode: the cap is copied onto each session the routine creates.
- session mode: each fire grants a delta allowance — the target session's cap becomes `consumed + budget` at fire time (overwritten each run, never accumulated). The target must have been created with a budget.

## Runs

Every fire attempt — scheduled or manual — records a run with exactly one of `session_id` or a typed `error`:

```bash
curl "https://api.agentsky.dev/v1/routine-runs?routine=rtn_...&has_error=true" -H "Authorization: Bearer ast_..."
```

Unrecoverable errors (an archived or deleted agent/session/environment/vault) also **pause the routine** with the error mirrored in `paused_reason`, so you can fix the reference and `unpause` (which resumes from the next occurrence). Transient errors — `insufficient_credits_error`, `rate_limited_error`, `session_busy_error` — leave the routine active.

## Lifecycle

`POST .../pause` suppresses scheduled fires (manual `run` still works); `POST .../unpause` resumes from the next occurrence; `POST .../archive` is terminal. Updates are omit-to-preserve: `initial_events` replaces wholesale, `metadata` is key-patched, `schedule`/`budget` clear with `null`.

## Webhooks

Register an endpoint to be notified without polling:

```bash
curl -X POST https://api.agentsky.dev/v1/webhooks \
  -H "Authorization: Bearer ast_..." -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/agentsky", "events": ["routine_run.failed", "routine.paused"]}'
```

The create response is the only place the `whsec_...` signing secret appears — store it. Deliveries carry `x-asteroids-event`, `x-asteroids-delivery`, `x-asteroids-timestamp`, and `x-asteroids-signature: v1=<hex hmac-sha256(secret, "<timestamp>.<body>")>`; verify the signature, dedupe on the event id, and fetch the resource by `data.id` — payloads are thin and ordering is not guaranteed. Event types: `routine.created/updated/paused/unpaused/archived`, `routine_run.started/succeeded/failed` (scheduled fires only; manual runs emit none), `run.completed/interrupted/failed/blocked` (session run lifecycle; fetch the run via `GET /api/v1/runs/{data.id}`), and `goal.completed/stopped/paused/resumed` (fires when a session goal reaches that status). Goal events also include `data.session_id`; fetch the current goal state via `GET /api/v1/sessions/{data.session_id}/goal`. Failed deliveries retry twice (5s, 30s); sustained failure disables the endpoint until you `PATCH {"status": "active"}`.
