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

# Automations

> Start agent sessions on a schedule, when a Slack, GitHub, or webhook event arrives, or by hand, with the same agent version, environment, prompt, and credentials every time.

An automation saves everything a session needs: a pinned agent version, an environment, an opening prompt, and run defaults such as credentials and files. Its **triggers** decide when it starts a session, and every start is recorded as a **run**, so you can see why each session began and what it was given.

You can manage automations in the console, under **Automations**, or through the REST API with an API key. Both work on the same automations.

<Note>
  You need an agent version and an environment. Saving checks that both exist. Runs need the environment's setup verified: a run on an unverified environment fails with `environment_not_verified`. See [Agents](/recursion/agents) and [Verify the setup script](/recursion/environments#verify-the-setup-script). Slack and GitHub triggers also need an **event source**: connecting Slack or GitHub on [Integrations](/recursion/integrations) creates one. See [Receive events](#receive-events). Creating automations and starting runs by hand needs create permission, changing, pausing, and resuming them needs update, and deleting needs delete. Organization developers and admins have all of them. See [Organizations and roles](/recursion/organizations-and-roles).
</Note>

## Create an automation

Choose the agent version, environment, and instructions, then add the triggers that should start runs.

<Tabs>
  <Tab title="Console">
    1. In the sidebar, click **Automations**, then click **New automation**. To start from a ready-made automation, click **Start from a template** at the top of the form and choose one; it fills in the name, triggers, and instructions, and you still choose the agent and environment.
    2. Name it, choose the agent, its version, and an environment.
    3. Under **Instructions**, write what the agent should do each time.
    4. Add triggers: **Scheduled**, **Slack**, or **GitHub**. With none, the automation runs only with **Run now**.
    5. Optionally set **Credential access**, **Files**, and, under **Advanced**, **Run metadata**.
    6. Click **Create automation**. It starts paused.
  </Tab>

  <Tab title="cURL">
    ```bash theme={"theme":"css-variables"}
    curl -X POST 'https://api.recursion.labelbox.com/managed-agents/v1/automations' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H 'Idempotency-Key: weekday-triage-2026-09-30' \
      -H 'Content-Type: application/json' \
      -d '{
        "displayName": "Weekday triage",
        "agentId": "5f0c2a1e-8b7d-4c3a-9e21-6d4f0b9a7c55",
        "agentVersionId": "c1a94e07-2f6b-4d18-b3a5-0e7d8c6f4a21",
        "environmentId": "9d3e7b52-1a4c-4f80-b6e9-2c8a5d0f7e13",
        "initialPrompt": {
          "type": "literal",
          "text": "Read the issues opened in acme/api since the last weekday and post a triage summary."
        },
        "runDefaults": { "metadata": { "team": "platform" } },
        "triggers": [
          {
            "type": "schedule",
            "triggerId": "3f6a9c21-7d4e-4b85-a0c3-5e8d2f1b7a64",
            "enabled": true,
            "schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/London" }
          }
        ]
      }'
    ```
  </Tab>
</Tabs>

A `201` response is the full automation, with `status` `paused` and a `revision`. Some fields are left out here.

```json theme={"theme":"css-variables"}
{
  "automationId": "a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53",
  "displayName": "Weekday triage",
  "agentVersionId": "c1a94e07-2f6b-4d18-b3a5-0e7d8c6f4a21",
  "status": "paused",
  "revision": 1,
  "createdAt": "2026-09-30T09:12:04Z",
  "updatedAt": "2026-09-30T09:12:04Z"
}
```

The `ETag` response header is an opaque validator; send it unchanged as `If-Match` on the next change. Do not construct it from the body's `revision`. New automations start paused: test one with **Run now** from its row's **Actions** menu, then resume it so its triggers can start runs. See [Run, pause, and resume](#run-pause-and-resume).

<Accordion title="Required fields">
  Every request field is required: `displayName`, `agentId`, `agentVersionId`, `environmentId`, `initialPrompt`, `runDefaults` (at least `metadata`, which can be `null`), and `triggers` (an empty list for manual runs only). You choose each `triggerId` yourself, as a UUID, and keep it stable across replacements.
</Accordion>

<Accordion title="Retries, Idempotency-Key, and If-Match">
  | Operation | Needs | Retrying |
  | - | - | - |
  | `createAutomation`, `runAutomation` | `Idempotency-Key` | Resend the same request with the same key; it replays the first response instead of creating a second automation or run. |
  | `replaceAutomation`, `deleteAutomation`, `pauseAutomation`, `resumeAutomation` | `If-Match` with the current returned `ETag` | Not safe to replay blindly. Read the automation again, then retry with its new `ETag`. |
  | `pauseEventSource`, `resumeEventSource` | `If-Match` with the source's current `ETag` | Read the source again, then retry. |
  | Lists, reads, and status | Nothing extra | Safe to retry. |

  Copy the complete returned `ETag`, quotes included. A missing `If-Match` returns `428 precondition_required`; a stale one returns `412 precondition_failed`. A public contract change can invalidate an ETag even when the body's `revision` is unchanged: read the resource again before retrying. See [Idempotent mutations](/recursion/api#idempotent-mutations) and [Errors](/recursion/errors#automations-and-event-sources).
</Accordion>

## Choose triggers

An automation has up to 20 triggers, and a run starts when any enabled one matches. The trigger types are `schedule`, `slack`, `github`, and `webhook`. Turn a trigger off with `enabled: false` to keep it without it starting runs. You can also start a run by hand at any time, even while the automation is paused.

### Schedule

Starts runs on a cron expression read in a time zone, such as the create example's `0 9 * * 1-5` in `Europe/London`: 9:00 on weekdays. By default, an occurrence is skipped while the previous run's session is still going.

<Accordion title="Schedule fields">
  These fields go inside the trigger's `schedule` object. Every one you save is applied to the schedule.

  | Field | Meaning |
  | - | - |
  | `cron` | A five-field cron expression, such as `0 9 * * 1-5` for 9:00 on weekdays. The console also offers **Hourly**, **Daily**, **Weekdays**, and **Weekly**. |
  | `timezone` | The IANA time zone the expression is read in, such as `Europe/London`. |
  | `overlapPolicy` | What happens when the previous run's session is still going. See **Overlap policies**. The default is `skip`. |
  | `catchupWindowSeconds` | How late a missed occurrence can still run, from 10 seconds to a year. The default is 900, 15 minutes. |
  | `jitterSeconds` | Up to this many seconds of delay, from 0 to 540, so many schedules don't all start on the same second. The default is 10; 0 turns it off. |
  | `startAt`, `endAt` | Optional times the schedule starts and stops, inclusive. Before `startAt` and after `endAt`, the schedule starts no runs. |
</Accordion>

<Accordion title="Overlap policies">
  | `overlapPolicy` | Console label | Behavior |
  | - | - | - |
  | `skip` | **Skip the next run** | The occurrence is skipped. |
  | `buffer_one` | **Queue one run** | One occurrence waits and runs when the previous finishes. |
  | `buffer_all` | **Queue every run** | Every occurrence waits its turn. |
  | `allow_all` | **Run in parallel** | Occurrences run at the same time. |
  | `cancel_other` | **Cancel the previous session** | The running session is cancelled, and the new occurrence runs. |
</Accordion>

<Accordion title="Check a schedule's status">
  The console shows each schedule's next and last run, upcoming runs, and occurrences skipped for overlap or missed outside the catch-up window. It also shows whether each saved schedule is in effect. `getAutomationStatus` returns the same values for each schedule trigger:

  ```bash theme={"theme":"css-variables"}
  curl 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53/status' \
    -H "Authorization: Bearer $RECURSION_API_KEY"
  ```

  ```json theme={"theme":"css-variables"}
  {
    "automationId": "a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53",
    "triggers": [
      {
        "triggerId": "3f6a9c21-7d4e-4b85-a0c3-5e8d2f1b7a64",
        "synchronization": "synced",
        "available": true,
        "paused": false,
        "nextRunAt": "2026-10-01T08:00:07Z",
        "lastRunAt": "2026-09-30T08:00:04Z",
        "upcomingRuns": ["2026-10-01T08:00:07Z", "2026-10-02T08:00:03Z"],
        "skippedOverlap": 0,
        "missedCatchupWindow": 0
      }
    ]
  }
  ```
</Accordion>

### Slack

Starts a run for Slack messages. It needs a Slack event source: connect Slack on the [Integrations](/recursion/integrations) page. Choose the `message` or `app_mention` event and the channels to accept. This trigger starts a run for each new top-level post in an incident channel that mentions a severity, and ignores other Slack apps:

```json theme={"theme":"css-variables"}
{
  "type": "slack",
  "triggerId": "9c4f2e81-5a7d-4b36-8e19-2f6c3d0a7b58",
  "enabled": true,
  "eventSourceId": "d2b8f6a4-3e17-4c95-a0d6-8b1e5f9c2a73",
  "events": ["message"],
  "routing": { "workspaceIds": [], "channelIds": ["C0APQ7Z1K2M"] },
  "botEvents": { "mode": "ignore" },
  "filters": { "textContains": ["sev1", "sev2"], "ignoreThreadReplies": true }
}
```

* **Filter `message` triggers by text.** Without a text filter, every new message in the trigger's channels starts its own run and a paid session, with no cap. The editor warns about this but still saves. Add `textContains` or `textMatches` so only the messages you mean start work.
* **One post starts one run.** Slack sends a message that mentions the app as both a `message` and an `app_mention` event. One post starts one run per automation, even when a trigger lists both events or two of its triggers each match.
* **Every Slack event starts a new session.** A reply in the same thread starts another run with its own session; it doesn't continue the earlier one.

<Accordion title="Slack trigger fields">
  | Field | Meaning |
  | - | - |
  | `eventSourceId` | The Slack source. |
  | `events` | `message`, `app_mention`, or both. A post that matches both starts one run per automation. |
  | `routing.channelIds` | Channels to accept. An empty list accepts every channel. The console picks channels the Slack app is a member of. |
  | `routing.workspaceIds` | Workspaces to accept. An empty list accepts every workspace the source receives. |
  | `botEvents.mode` | `ignore` to skip messages from other Slack apps, or `allow_external` to accept them. Messages from the source's own app never start runs. |
  | `allowSharedChannels` | `true` to accept channels shared with other organizations. The default is `false`. |
  | `filters` | Optional `textContains`, `textExcludes`, `textMatches` (a regular expression), `ignoreThreadReplies`, and `fromUserIds`. Text matching ignores case. |
  | `filters.fromUserIds` | The people and apps whose messages may start runs: a person's Slack user id or an app's bot id, as the member catalog lists them. An empty or absent list accepts anyone. An app listed here starts runs even when `botEvents.mode` is `ignore`. An app's message matches only by its bot id, even when the app posts on a listed person's behalf. |

  The console lists up to 1,000 channels the connected Slack app is a member of. Invite the app to a channel to make it selectable. The same list is available from the API for a Slack connection:

  ```bash theme={"theme":"css-variables"}
  curl 'https://api.recursion.labelbox.com/managed-agents/v1/integrations/connections/b4e81f26-9c3d-4a57-8e10-2d6f5a9c7b43/channel-catalog' \
    -H "Authorization: Bearer $RECURSION_API_KEY"
  ```

  The people and apps for `fromUserIds` come from the connection's member catalog, read with the same connection:

  ```bash theme={"theme":"css-variables"}
  curl 'https://api.recursion.labelbox.com/managed-agents/v1/integrations/connections/b4e81f26-9c3d-4a57-8e10-2d6f5a9c7b43/member-catalog' \
    -H "Authorization: Bearer $RECURSION_API_KEY"
  ```
</Accordion>

<Accordion title="Messages that never start runs, and stopping a Slack session">
  * **Only newly posted messages start runs.** Editing or deleting a message never starts one.
  * **Direct messages to the app don't start runs.** Use a channel.
  * **Stopping a Slack-started session posts nothing to its thread.** The console reminds you when you force-stop or interrupt one. Reply in the thread yourself, or message the agent to post an update.
</Accordion>

### GitHub

Starts a run for GitHub webhook events. It needs a GitHub event source: connect GitHub on the [Integrations](/recursion/integrations) page. This trigger matches `pull_request` deliveries in `acme/api` with the action `opened` or `synchronize` on `fix/*` branches, and ignores drafts and bots:

```json theme={"theme":"css-variables"}
{
  "type": "github",
  "triggerId": "8b2e4d71-6c39-4a05-9f18-3d7a5c1e9b42",
  "enabled": true,
  "eventSourceId": "e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07",
  "events": ["pull_request"],
  "filters": {
    "repositories": ["acme/api"],
    "actions": ["opened", "synchronize"],
    "branches": ["fix/*"],
    "excludeDrafts": true,
    "ignoreBots": true
  }
}
```

<Note>
  The event only starts the run. To read or change the repository, the agent needs a GitHub grant, and it works through GitHub's MCP tools. No GitHub credential is placed in the sandbox, so `git` and `gh` there aren't authenticated. See [GitHub access](/recursion/github).
</Note>

Three more common triggers. This one hands a pull request to the agent when someone adds the `agent-ready` label:

```json theme={"theme":"css-variables"}
{
  "type": "github",
  "triggerId": "4a7c9e23-8d15-4f60-b2e8-6c1d3a5f9b07",
  "enabled": true,
  "eventSourceId": "e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07",
  "events": ["pull_request"],
  "filters": {
    "repositories": ["acme/api"],
    "actions": ["labeled"],
    "addedLabels": ["agent-ready"],
    "excludeDrafts": true,
    "ignoreBots": true
  }
}
```

This one starts a run when a pull request comment contains the command `/agent fix`:

```json theme={"theme":"css-variables"}
{
  "type": "github",
  "triggerId": "b3e5d8f1-2c74-4a96-8d0b-5f7e1c9a3d62",
  "enabled": true,
  "eventSourceId": "e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07",
  "events": ["issue_comment"],
  "filters": {
    "repositories": ["acme/api"],
    "actions": ["created"],
    "commentOn": "pull_request",
    "textContains": ["/agent fix"],
    "ignoreBots": true
  }
}
```

This one starts a run when a CI workflow run on `main` finishes with a failure:

```json theme={"theme":"css-variables"}
{
  "type": "github",
  "triggerId": "7f2a6c94-1e58-4d3b-a9c7-0b4e8d2f6a15",
  "enabled": true,
  "eventSourceId": "e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07",
  "events": ["workflow_run"],
  "filters": {
    "repositories": ["acme/api"],
    "actions": ["completed"],
    "conclusions": ["failure"],
    "branches": ["main"]
  }
}
```

<Accordion title="GitHub trigger fields and filters">
  | Field | Meaning |
  | - | - |
  | `eventSourceId` | The GitHub source. |
  | `events` | GitHub event names. The console offers only the events that can start runs. A save that names any other event is refused, and the error lists the accepted names. |
  | `filters` | Optional conditions, all of which must match. |

  | Filter | Matches |
  | - | - |
  | `repositories` | Repository full names (`owner/name`), ignoring case. The console's repository picker lists the repositories of the connection's GitHub installation. |
  | `actions` | Delivery actions, such as `opened`, `labeled`, `submitted`, or `completed`. |
  | `branches` | Globs for the pushed branch, the workflow run's head branch, or the pull request's head branch. |
  | `baseBranches` | Globs for the pull request's base branch. |
  | `labelsAny`, `labelsNone`, `addedLabels` | Labels on the pull request or issue, or the label just added. |
  | `senders`, `ignoreBots` | The GitHub account that caused the delivery. |
  | `excludeDrafts`, `merged` | Draft pull requests, and whether a closed pull request was merged. |
  | `commentOn` | For `issue_comment`: `pull_request` or `issue`. |
  | `reviewStates`, `conclusions` | Submitted review states, and completed workflow run conclusions. |
  | `textContains`, `textExcludes`, `textMatches` | The comment or review body, or else the pull request or issue title and body, ignoring case. `textMatches` is a regular expression. |

  In globs, `*` matches any characters, including `/`, and `?` matches one character.
</Accordion>

### Webhook

Starts a run for deliveries to a custom webhook source. The console offers this trigger when your organization has a custom source. Add `filters.fieldEquals` to start runs only for some deliveries. Each condition names a dot-separated `path` into the JSON body, such as `data.level`, and the `value` it must equal, written as JSON text without quotes.

```json theme={"theme":"css-variables"}
{
  "type": "webhook",
  "triggerId": "5d9c1e37-2a84-4f60-b7c3-1e8a4d6f9b25",
  "enabled": true,
  "eventSourceId": "7a4e2c18-9d53-4b61-8f07-2c6e9a3d5b14",
  "filters": { "fieldEquals": [{ "path": "data.level", "value": "critical" }] }
}
```

To send deliveries to the source, see [Send a custom webhook delivery](#send-a-custom-webhook-delivery).

## Set run defaults

`runDefaults` applies to every run, whether a trigger or a person starts it.

| Field | Meaning |
| - | - |
| `metadata` | String key/value pairs copied onto every session, for example to find them later. `null` for none. Their count and size limits are in [Limits](/recursion/limits#automations). |
| `vaultIds` | Vaults each session can use. Leave it out to use the agent's default vaults. An empty list grants no vault access. |
| `credentialRefs` | Specific credentials within `vaultIds`. Leave it out to grant everything in those vaults. |
| `resources` | [Files](/recursion/files) from your organization, each with a `fileId` and a `mountPath` under the session's files directory, such as `/data/input.csv`. |

## Keep untrusted input in check

Event runs put outside text into the session: Slack messages, pull request and issue titles and bodies, comments, and webhook bodies. Treat that text as untrusted input. Someone who can post in the channel or open an issue can try to steer the agent.

* **Restricted environments drop `web_fetch`.** A session an automation starts, or a session started from a Slack thread, gets no `web_fetch` tool when its environment limits internet access (limited or no access). The environment's network policy is then its only way out.
* **Unrestricted environments keep `web_fetch` and full internet access.** An injected instruction could ask the agent to send data anywhere. Use a limited environment for automations that read outside text, and grant only the credentials and repositories the task needs.
* **Webhook runs record only the sender's headers.** The event in a webhook run keeps the headers the sender sent. Headers added on the way to the event source are never recorded.

See [Environments](/recursion/environments#control-network-access) and [Security](/recursion/security#automations-and-event-sources).

## Run, pause, and resume

Start one run by hand to test the automation, then resume it so its triggers can start runs.

<Tabs>
  <Tab title="Console">
    1. In **Automations**, open the automation row's **Actions** menu.
    2. Click **Run now** to start one run, or **Run with input** to add a **JSON payload**. Both need create permission.
    3. Click **Resume** to let its triggers start runs, or **Pause** to stop them.
  </Tab>

  <Tab title="cURL">
    Set `AUTOMATION_ETAG` to the complete `ETag` header returned by the current automation read. Refresh it from each write response before the next change.

    ```bash theme={"theme":"css-variables"}
    curl -X POST 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53/runs' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H 'Idempotency-Key: triage-rerun-2026-09-30' \
      -H 'Content-Type: application/json' \
      -d '{"payload": {"repository": "acme/api", "since": "2026-09-29"}}'

    curl -X POST 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53/resume' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H "If-Match: $AUTOMATION_ETAG"

    curl -X POST 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53/pause' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H "If-Match: $AUTOMATION_ETAG"
    ```
  </Tab>
</Tabs>

`runAutomation` answers with the run. **What success means:** the run moves from `pending` to `created`, and `sessionId` names the session it started. Follow that session like any other; see [Sessions](/recursion/sessions).

Resume returns the automation with `status` `active`. Pausing stops triggers from starting runs; **Run now** still works. Each change raises the `revision`, so use the new `ETag` for the next one.

<Accordion title="Run input, failed runs, and automatic pauses">
  `payload` is optional and can be any JSON value up to 64 KiB (65,536 bytes) as JSON; a larger one is refused with `400` on `payload`.

  A run that couldn't start a session ends `failed` with an `error` that has a `code`, a `message`, the `field` or `resourceId` responsible when there is one, and `retryable`.

  Resume is refused with `422 automation_reference_invalid` while the automation's agent, agent version, or environment is deleted; the automation stays paused. If a run fails because something the automation uses no longer exists, such as its agent, environment, a vault, a credential, or a file, the automation is paused, and the run's error says what is gone. Fix the automation, then resume it.
</Accordion>

## See run history

In **Automations**, choose **View runs** from the automation row's **Actions** menu. Each run shows its **Trigger**, when it started, and **Session creation**. Filter by status, trigger, or date, and click a run for its **Run detail** and a link to its session.

In the API, list an automation's runs, then read one. `getAutomationRun` returns one run with its frozen snapshots and `effectiveInitialMessage`.

```bash theme={"theme":"css-variables"}
curl 'https://api.recursion.labelbox.com/managed-agents/v1/automation-runs?automationId=a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53&status=failed' \
  -H "Authorization: Bearer $RECURSION_API_KEY"
curl 'https://api.recursion.labelbox.com/managed-agents/v1/automation-runs/6e3b9d14-8a27-4c50-b1f9-0d5c7e2a4b86' \
  -H "Authorization: Bearer $RECURSION_API_KEY"
```

```json theme={"theme":"css-variables"}
{
  "automationRunId": "6e3b9d14-8a27-4c50-b1f9-0d5c7e2a4b86",
  "automationId": "a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53",
  "triggerType": "manual",
  "status": "created",
  "sessionId": "b7e1c9a4-3d62-4f15-8a07-5c2e9f6d1b38",
  "statusUrl": "/v1/automation-runs/6e3b9d14-8a27-4c50-b1f9-0d5c7e2a4b86",
  "createdAt": "2026-09-30T09:20:11Z",
  "updatedAt": "2026-09-30T09:20:13Z"
}
```

<Accordion title="Run filters and paging">
  `listAutomationRuns` returns runs newest first. Filter with `automationId`, `triggerId`, `eventSourceId`, `triggerType`, `status`, `createdAtGte`, and `createdAtLt`, and page with `limit` and `cursor`.
</Accordion>

## Change or delete an automation

Saving a change keeps the automation's current status. Deleting it removes its triggers while preserving records for runs that already started.

<Tabs>
  <Tab title="Console">
    In **Automations**, use the automation row's **Actions** menu to open it for editing or to delete it. The automations list is newest first, and the editor shows the automation's prompt and triggers. Click **Save changes** to save an edit.
  </Tab>

  <Tab title="cURL">
    `listAutomations` returns summaries, and `getAutomation` returns the full automation with its `ETag`. `replaceAutomation` takes the same complete body as create, so send every field you want to keep. Replace and delete need `If-Match`. Set `AUTOMATION_ETAG` to the current returned header and refresh it after a successful replace before deleting.

    ```bash theme={"theme":"css-variables"}
    curl 'https://api.recursion.labelbox.com/managed-agents/v1/automations' \
      -H "Authorization: Bearer $RECURSION_API_KEY"

    curl -X PUT 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H "If-Match: $AUTOMATION_ETAG" \
      -H 'Content-Type: application/json' \
      -d '{
        "displayName": "Weekday triage",
        "agentId": "5f0c2a1e-8b7d-4c3a-9e21-6d4f0b9a7c55",
        "agentVersionId": "d8f25b1c-6e4a-4a93-b7c0-3f1e9d5a2c68",
        "environmentId": "9d3e7b52-1a4c-4f80-b6e9-2c8a5d0f7e13",
        "initialPrompt": { "type": "literal", "text": "Read the issues opened in acme/api since the last weekday and post a triage summary." },
        "runDefaults": { "metadata": { "team": "platform" } },
        "triggers": [
          {
            "type": "schedule",
            "triggerId": "3f6a9c21-7d4e-4b85-a0c3-5e8d2f1b7a64",
            "enabled": true,
            "schedule": { "cron": "0 9 * * 1-5", "timezone": "Europe/London" }
          }
        ]
      }'

    curl -X DELETE 'https://api.recursion.labelbox.com/managed-agents/v1/automations/a2d7e4c9-1b35-4f86-9e02-7c4b8d1f6a53' \
      -H "Authorization: Bearer $RECURSION_API_KEY" \
      -H "If-Match: $AUTOMATION_ETAG"
    ```

    A delete answers `204`.
  </Tab>
</Tabs>

<Accordion title="If someone else saved first">
  If someone else saved the automation while you were editing it, **Save changes** saves nothing and shows **This automation changed elsewhere. Nothing was saved.** Choose **Keep my edits** to replace their version with yours on the next save, or **Load the newer version** to see what they saved.
</Accordion>

### Keep the agent version current

An automation keeps the agent version it was saved with. Later changes to the agent's instructions, tools, and integrations don't apply to its runs until you move it to the new version. In the **Automations** list, an **Older version** label marks automations that run an older version than their agent's latest. When you open one, the drawer names both versions, for example "This automation runs Version 1 of backend-fixer. The agent is now on Version 2", and offers **Use Version 2**. Click it, then **Save changes**. In the API, replace the automation with the new `agentVersionId`.

## Receive events

An **event source** is an endpoint that receives events and checks they're genuine. Automations listen to it through Slack, GitHub, and webhook triggers. One source can serve many automations, up to the enabled-trigger limit in [Limits](/recursion/limits#automations).

| Source type (`type`) | Receives | Comes from |
| - | - | - |
| `slack_events_api` | Slack Events API callbacks. | Connecting Slack on **Integrations**. |
| `github_webhook` | GitHub webhook deliveries. | Connecting GitHub on **Integrations**. |
| `custom_webhook` | JSON from your own services. | Custom sources already in your organization. |

Sources created by connecting an account are **connection-managed**: they start active, and you pause and resume them from the list.

<Accordion title="Signing secrets and disconnecting">
  Slack and GitHub sources check every delivery against a signing secret that the connection creates and manages. Signing secrets never reach an agent sandbox. Disconnecting the account removes its source and stops its triggers. Existing automations keep those trigger settings so you can select a new source after reconnecting.
</Accordion>

### Manage sources

In **Automations**, open the **Event sources** tab. Each source's health and last delivery are columns in the list, and its row's **Actions** menu pauses or resumes deliveries. A healthy source has received a delivery, which does not prove the provider can reach it right now.

A connection-managed source is changed and removed through its connection on **Integrations**. A custom source can be deleted when no automation uses it.

<Accordion title="Source health">
  | `health` | Meaning |
  | - | - |
  | `never_received` | No delivery has arrived yet. |
  | `healthy` | A delivery has arrived. |
  | `paused` | The source is paused. |
  | `connection_unavailable` | The connection behind the source isn't usable. Check or reconnect it on **Integrations**. |
  | `setup_required` | The connected GitHub app needs its event permissions approved. |
</Accordion>

<Accordion title="Manage sources with the API">
  `listEventSources` and `getEventSource` return sources, and `getEventSourceStatus` returns `health`, `triggerCount`, and `lastDelivery`. Pause and resume need `If-Match` with the source's `ETag`. Set `EVENT_SOURCE_ETAG` to the complete header returned by `getEventSource`.

  ```bash theme={"theme":"css-variables"}
  curl 'https://api.recursion.labelbox.com/managed-agents/v1/event-sources' \
    -H "Authorization: Bearer $RECURSION_API_KEY"

  curl 'https://api.recursion.labelbox.com/managed-agents/v1/event-sources/e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07/status' \
    -H "Authorization: Bearer $RECURSION_API_KEY"

  curl -X POST 'https://api.recursion.labelbox.com/managed-agents/v1/event-sources/e1c7a3f9-4b26-4d80-a5e3-9f2b6d8c1a07/pause' \
    -H "Authorization: Bearer $RECURSION_API_KEY" \
    -H "If-Match: $EVENT_SOURCE_ETAG"
  ```
</Accordion>

### Send a custom webhook delivery

A custom source receives deliveries at its **Callback URL**, shown in **Event sources**. The full URL is the console's address followed by the callback path, `https://recursion.labelbox.com/agents/v1/event-sources/<eventSourceId>/events`. Give it to the sender, and resume the source when you're ready to receive events.

Send a JSON body with `POST` to the callback URL. For an `hmac_sha256` source, compute the HMAC-SHA256 of the exact body bytes, keyed with the source's signing secret, and send it as lowercase hexadecimal in the signature header, after the prefix. Sign the bytes you send; re-serializing the JSON after signing breaks the signature.

```http theme={"theme":"css-variables"}
POST /agents/v1/event-sources/7a4e2c18-9d53-4b61-8f07-2c6e9a3d5b14/events HTTP/1.1
Content-Type: application/json
X-Signature-256: sha256=5b1c9e0f3a7d2b8e4c6f1a9d3e7b2c5f8a0d4e6b9c1f3a5d7e2b8c4f6a0d9e1b

{"data":{"level":"critical","service":"billing"}}
```

A delivery that's accepted returns `200` with `{"ok": true}`, and matching triggers on active automations start runs. The same signed body is treated as the same delivery, so a retry doesn't start a second run. For an unsigned source, send an `X-Webhook-Id` or `Idempotency-Key` header to name the delivery.

<Accordion title="Every delivery response">
  | Response | Meaning |
  | - | - |
  | `200` | Accepted. Matching triggers on active automations start runs. |
  | `401 unauthorized` | The signature didn't match. |
  | `400 invalid_request` | The body isn't JSON, or the request is malformed. |
  | `404 not_found` | The source doesn't exist or was deleted. |
  | `413 payload_too_large` | The body is over 1 MiB. |
  | `429 rate_limited` | Too many deliveries. Retry after `Retry-After`. |
  | `503 event_admission_unavailable` | The delivery couldn't be recorded. Retry. |
</Accordion>

## How automations run

```mermaid theme={"theme":"css-variables"}
flowchart TD
  schedule["Schedule"] --> run["Run: pending"]
  event["Slack, GitHub,<br>or webhook event"] --> run
  manual["Run now"] --> run
  run -->|"session created"| created["Run: created"]
  run -->|"could not start"| failed["Run: failed"]
```

* **Every run uses the pinned agent version.** Publishing a new agent version doesn't change the automation. See [Keep the agent version current](#keep-the-agent-version-current).
* **Everything is frozen per run.** A run records the automation, trigger, and event exactly as they were, and the session's opening message. Editing the automation later doesn't change runs already started.
* **The opening message** is your saved prompt. For an event run, it's followed by `Event for this run:` and the event as JSON. For a manual run with input, it's followed by `Input for this run:` and the input. For a scheduled run, it ends with `This is a scheduled run for <time>.`, where the time is the occurrence in UTC, such as `2026-10-01T08:00:00Z`.
* **Sessions show where they came from.** A session an automation started shows **Started by** in its session panel: the automation, the trigger (**Manual**, **Schedule**, **Slack**, **GitHub**, or **Webhook**), and the run, each linked.

## What can go wrong

The most common problems:

* **A run is `failed`.** The session couldn't start. Read the run's `error`. If `retryable` is `true`, run it again; otherwise fix the named `field` or resource.
* **An automation paused on its own.** A run found that something it uses was deleted. Read the failed run's `error`, fix the automation, then resume it.
* **An event starts no runs.** Check the source status and the trigger filters. A Slack message that was edited, deleted, or sent as a direct message never starts a run.
* **Runs still use access you removed from the agent.** The automation runs its pinned agent version. Move it to the new version. See [Keep the agent version current](#keep-the-agent-version-current).

<Accordion title="Every automation error">
  | Code or symptom | Cause | Fix |
  | - | - | - |
  | `422 automation_reference_invalid` on save | The agent version or environment doesn't exist or can't be used. | Choose a live agent version and environment. |
  | `422 automation_event_source_invalid` | A trigger's event source is missing or is the wrong type for the trigger. | Use a Slack source for a Slack trigger, and so on. |
  | `409 conflict` on `triggers` | The save would put an event source over its enabled-trigger limit. See [Limits](/recursion/limits#automations). | Switch off or delete triggers on that source, then save again. |
  | A run is `failed` with `environment_not_verified` | The environment's setup script hasn't been verified, or the environment changed since it was. The automation stays active. | Run the environment's setup; later runs then start. See [Verify the setup script](/recursion/environments#verify-the-setup-script). |
  | `422 automation_reference_invalid` on resume | The automation's agent, agent version, or environment was deleted. | Choose a live agent version and environment, save, then resume. |
  | An automation paused on its own | A run found that something it uses was deleted. | Read the failed run's `error`, fix the automation, then resume it. |
  | **This automation changed elsewhere. Nothing was saved.** | Someone saved the automation while you were editing. | Choose **Keep my edits** or **Load the newer version**, then save. |
  | `428 precondition_required` | A replace, delete, pause, or resume sent no `If-Match`. | Read the resource and send its `ETag` as `If-Match`. |
  | `412 precondition_failed` | The automation or source changed after you read it. | Read it again and retry with its new `ETag`. |
  | `400` on `payload` | The run input is over 64 KiB as JSON. | Send less input, or put large data in a file. |
  | `409 idempotency_conflict` | You reused an `Idempotency-Key` with a different request. | Use a new key for a new request. |
  | A source can't be deleted | Automations still use it, or it's managed by its connection. | Remove it from their triggers first, or disconnect the account on **Integrations**. |
  | A schedule never runs | The automation or trigger is paused or off, or the time is outside `startAt` and `endAt`. | Resume the automation, and check its schedule status. |
  | A run is `failed` | The session couldn't start. | Read the run's `error`. If `retryable` is `true`, run it again; otherwise fix the named `field` or resource. |
  | Runs still use access you removed from the agent | The automation runs its pinned agent version. | Move it to the new version. See [Keep the agent version current](#keep-the-agent-version-current). |
  | No runs from a Slack message | The message was edited, deleted, or sent as a direct message, the channel isn't routed, or the filters didn't match. | Post a new message in a routed channel, and check the filters. |
  | No runs from a GitHub or webhook event | The source is paused, the signature failed, or the filters didn't match. | Check the source status and trigger filters. |
  | The session has no `web_fetch` | The environment limits internet access, and the session was started by an automation or from Slack. | Allow the hosts the task needs in the environment's network policy. |
  | `health` is `setup_required` | The connected GitHub app needs its event permissions approved. | Approve the permission update in GitHub, then check the connection on **Integrations**. |

  For every error code, see [Errors](/recursion/errors).
</Accordion>

## Limits

Limits on triggers, prompts, names, run input, run defaults, schedules, trigger filters, and custom webhook bodies are in [Limits](/recursion/limits). Two more apply here:

* A page of automations or event sources holds 1 to 100 items, 50 by default.
* Name and prompt limits count UTF-8 bytes, not characters: an emoji or a non-Latin letter takes 2 to 4 bytes, so a name in those scripts fits fewer than 256 characters.

## Next steps

<CardGroup cols={2}>
  <Card title="Integrations" href="/recursion/integrations">
    Connect Slack and GitHub to get event sources.
  </Card>

  <Card title="GitHub access" href="/recursion/github">
    Give automated runs access to the repositories they work on.
  </Card>

  <Card title="Files" href="/recursion/files">
    Mount the same files into every run.
  </Card>

  <Card title="Sessions" href="/recursion/sessions">
    Follow the sessions automations start.
  </Card>
</CardGroup>
