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

# Cron and Webhooks

> Schedule agents on a cron with heartbeat triggers, and wake them from any external system with authenticated custom webhook endpoints.

Two trigger sources need no provider connection: **heartbeat triggers** wake an agent on a cron schedule, and **custom webhook endpoints** give any external system — an alerting tool, a payment provider, your own backend — an authenticated URL that fires an agent trigger. This page is the reference for both.

## Heartbeat (cron) triggers

A heartbeat trigger is declared with `kind: heartbeat` instead of `event:`:

```yaml theme={null}
triggers:
  - kind: heartbeat
    cron: 0 8 * * *
    timezone: America/Los_Angeles
    routing:
      kind: spawn
```

### Fields

<ParamField path="triggers[].kind" type="&#x22;heartbeat&#x22;" required>
  Marks the trigger as a schedule. `event`, `events`, `connection`, `optional`, `endpoint`, and `auth` are all rejected on a heartbeat trigger, and `checks:` is rejected too (checks are legal only on `github.pull_request.*` events).
</ParamField>

<ParamField path="triggers[].cron" type="string" required>
  The cron expression, 1–512 characters. auto passes it verbatim to a Temporal Schedule, so use the classic 5-field form `minute hour day-of-month month day-of-week` — for example `0 9 * * 1-5` for 9:00 on weekdays.
</ParamField>

<ParamField path="triggers[].timezone" type="string" default="UTC">
  IANA timezone name (e.g. `America/Los_Angeles`) the cron expression is evaluated in.
</ParamField>

`message`, `where`, `fallback`, and `routing` work exactly as on event triggers.

### How ticks run

Applying the agent creates one durable schedule per heartbeat trigger, reconciled on every apply — editing the cron updates the schedule, deleting the trigger (or the agent) deletes it. Each trigger fires under a synthetic event key of the form `heartbeat.<agentResourceId>.<ordinal>`, which only that trigger can match — one agent's heartbeat never wakes another agent.

Operational behavior worth knowing:

* **No overlap**: if a tick's dispatch workflow is still running when the next tick is due, the new tick is skipped rather than stacked.
* **Catch-up window**: ticks missed during a short outage are replayed within a 1-minute window; older missed ticks are dropped. Each tick is deduplicated on `<scheduleId>:<scheduledAt>`, so a replay never fires twice.
* **Self-cleaning**: a tick whose agent no longer exists deletes its own schedule instead of erroring forever.

### Payload and placeholders

Every tick delivers this payload:

```json theme={null}
{
  "trigger": "heartbeat",
  "heartbeat": {
    "scheduleId": "heartbeat-<agentResourceId>-<ordinal>",
    "agentResourceId": "…",
    "triggerOrdinal": 0,
    "scheduledAt": "2026-07-15T15:00:00.000Z"
  }
}
```

`{{heartbeat.scheduledAt}}` is the placeholder that matters: it is the tick's *scheduled* time (not the delivery time), so use it as the anchor for reporting windows — "the 24 hours ending at `{{heartbeat.scheduledAt}}`" stays exact even if delivery lags.

### Routing patterns

* **`routing: { kind: spawn }`** — one fresh session per tick. The right default for reports and digests.
* **`routing: { kind: deliver, onUnmatched: spawn }`** — for a standing agent with `concurrency: 1`: the tick is delivered into the agent's one live session, and spawns it if none is running. A plain `deliver` with no `routeBy` is only legal on a `concurrency: 1` agent (or when `onUnmatched: spawn` can claim the slot).

### Example: a daily digest

Adapted from the [daily digest example](/examples/daily-digest):

```yaml .auto/agents/ship-digest.yaml theme={null}
name: ship-digest
systemPrompt: |
  You are a read-only code analyst. You read code, history, and CI
  state, and you write reports; you never change anything.
initialPrompt: |
  Produce the daily shipped-code digest.

  This run was scheduled at {{heartbeat.scheduledAt}}. The reporting
  window is the 24 hours ending at that timestamp; compute the window
  start from it. Post the digest to Slack #dev with chat.send.
tools:
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
triggers:
  - kind: heartbeat
    cron: 0 8 * * *
    timezone: America/Los_Angeles
    routing:
      kind: spawn
```

## Custom webhook endpoints

A trigger that listens on a `webhook.*` event declares its own HTTP endpoint inline: `endpoint:` names it, `auth:` says how callers authenticate.

```yaml theme={null}
triggers:
  - event: webhook.incident.opened
    endpoint: incident-webhook
    auth:
      kind: bearer_token
      secretRef: incident-webhook-secret
    routing:
      kind: spawn
```

### Provisioning

Applying the agent provisions the endpoint and returns a receipt per webhook trigger — `{ event, endpoint, ingestUrl, status: "ready" }` — with the public ingest URL:

```text theme={null}
POST https://www.auto.sh/api/v1/webhook-endpoints/{slug}/events
```

The slug is stable, so the URL can be pasted into the external system once. The endpoint name scopes trigger selection: several triggers (even on different agents) can share one endpoint, and an inbound event is matched only against the triggers bound to that endpoint.

### Authentication

<ParamField path="triggers[].auth.kind" type="&#x22;hmac_sha256&#x22; | &#x22;bearer_token&#x22; | &#x22;none&#x22;">
  How inbound requests authenticate. `hmac_sha256` and `bearer_token` require `secretRef`. `auth` is required on the apply that first creates the endpoint; a later trigger binding an existing endpoint may omit it and inherit the endpoint's auth, and declaring different auth for an existing endpoint fails apply.
</ParamField>

<ParamField path="triggers[].auth.secretRef" type="string">
  The name of a project [secret](/reference/secrets) holding the shared credential. The plaintext is resolved server-side at delivery time; rotating the secret rotates the endpoint's credential without re-applying.
</ParamField>

| Kind           | Caller sends                                                                                                           |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `hmac_sha256`  | `x-auto-signature-256: <hex HMAC-SHA256 of the raw request body>` — an optional `sha256=` prefix is accepted.          |
| `bearer_token` | `Authorization: Bearer <secret>`                                                                                       |
| `none`         | Nothing. The URL is the only gate, and the slug is derived from the endpoint name — treat a `none` endpoint as public. |

Both credentialed kinds compare in constant time. CORS is wide open on the ingest route (auth is header-based, never cookie-based), so a browser page can POST to a bearer endpoint directly.

### Request contract

The body must be a JSON object. Two optional top-level fields have meaning to auto; everything else is your payload:

<ParamField path="event" type="string">
  Names the event key: a body with `"event": "incident.opened"` routes as `webhook.incident.opened` (a value already starting with `webhook.` is kept verbatim). A body **without** `event` routes as the fixed fallback key `webhook.received`.
</ParamField>

<ParamField path="dedupKey" type="string">
  Idempotency key. A repeated `dedupKey` returns the original event record and does not re-route. Without it, every delivery is a fresh event.
</ParamField>

Responses: `202` with `{ eventRecordId, created: true, routerStatus }` on first delivery; `200` with `created: false` on a dedup hit; `401` on bad credentials; `400` on a non-object or unparseable body.

```bash theme={null}
curl -X POST "https://www.auto.sh/api/v1/webhook-endpoints/{slug}/events" \
  -H "Authorization: Bearer $WEBHOOK_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "incident.opened",
    "dedupKey": "incident-4187",
    "title": "Checkout latency spike",
    "severity": "sev2",
    "service": "checkout",
    "link": "https://alerts.example.com/4187"
  }'
```

### Payload, placeholders, and filters

The trigger payload is the raw request body — there is no normalization. Template placeholders and `where` paths address whatever the caller posts:

```yaml theme={null}
    message: |
      A production alert arrived.

      - Title: {{title}}
      - Severity: {{severity}}
      - Service: {{service}}
      - Link: {{link}}
    where:
      $.severity:
        in: [sev1, sev2]
```

### Catching unshaped providers

Most third-party webhook senders do not put an `event` field at the top level. Point them at the endpoint anyway and author a `fallback: true` trigger on `webhook.received`; it fires only when no normal trigger on the endpoint matched, and `where` filters on the provider's own body discriminate from there:

```yaml theme={null}
triggers:
  - event: webhook.received
    endpoint: alerts
    auth:
      kind: hmac_sha256
      secretRef: alerts-webhook-secret
    fallback: true
    where:
      $.alert.status: firing
    routing:
      kind: spawn
```

### Diagnostics

Sessions inspect endpoints with the [`auto.webhooks.list` and `auto.webhooks.get` tools](/runtime/auto-tools), which report each endpoint's ingest URL, auth mode, secret presence, attached triggers, and problems: a credentialed endpoint with no `secretRef`, or a `secretRef` naming a nonexistent secret, is an error; an endpoint with no active triggers or a not-yet-promoted reservation is a warning.

### Example: incident response

Adapted from the [incident response example](/examples/incident-response):

```yaml .auto/agents/incident-response.yaml theme={null}
name: incident-response
systemPrompt: |
  You are the incident response agent. When an alert arrives, perform
  fast, evidence-based triage and post it to Slack #incidents.
initialPrompt: |
  A production alert arrived.

  Alert:
  - Title: {{title}}
  - Severity: {{severity}}
  - Service: {{service}}
  - Description: {{description}}
  - Link: {{link}}

  Investigate, then post the triage to Slack #incidents.
tools:
  auto:
    kind: local
    implementation: auto
  chat:
    kind: local
    implementation: chat
    auth:
      kind: connection
      provider: slack
      connection: slack
triggers:
  - event: webhook.incident.opened
    endpoint: incident-webhook
    auth:
      kind: bearer_token
      secretRef: incident-webhook-secret
    routing:
      kind: spawn
```

Create the secret before applying, then send the recorded value as the bearer token from the alerting tool.

## See also

* [Triggers reference](/reference/triggers) — routing kinds, filter grammar, fallback semantics
* [Secrets](/reference/secrets) — creating the `secretRef` values webhook auth resolves
* [Lifecycle events](/reference/events/lifecycle) — the internal `auto.*` events that need no endpoint at all
