# Automations

A workflow that runs per contact. An event enrols someone at the trigger, and the graph carries them through sends, delays and branches until it runs out.

## Lifecycle

An automation is an identity plus immutable *versions*. Creating one writes version 1. A `PATCH` carrying `steps` and `connections` writes the next one and points the automation at it — that `PATCH` is the publish. A run already in flight keeps the version it started on, so publishing never rewrites what someone is halfway through.

`status` gates enrolment and nothing else: `disabled` stops new runs starting and leaves live ones alone. `POST /automations/{automation_id}/stop` is the one that disables *and* cancels every run still going; deleting does the same and answers `404` from that moment.

## Steps and connections

A graph is a list of `steps`, each with a `key` unique within the version, and a list of `connections` between those keys. Exactly one step is the `trigger`, every other step is reachable from it, and the whole thing is acyclic. At most 150 steps.

| Step type | Does |
| --- | --- |
| `trigger` | The one entry point. `config.event_name` is a custom event of yours, or a built-in one under the `rasket:` prefix. |
| `send_email` | Sends a published template through the ordinary send transaction: quota, suppressions, unsubscribe headers and `email.*` events all behave as they do for `POST /emails`. |
| `delay` | Parks the run until a moment: `{ duration: "3 days" }` or `{ until: "2026-10-01T09:00:00Z" }`, at most 30 days out. |
| `condition` | A rule tree over the contact. Its two outgoing edges are `condition_met` and `condition_not_met`; it takes no `default`. |
| `contact_update` | Sets first name, last name, `unsubscribed` or declared properties to literal values. |
| `add_to_segment` | Adds the contact to one of your segments. |
| `topic_update` | Opts the contact in or out of one topic. |

A connection is `{ from, to, type }`, where `type` is `default`, `condition_met` or `condition_not_met` and defaults to `default`. Every step but a `condition` has at most one outgoing edge; a `condition` has at most one of each branch.

The whole graph is checked at once, against your own team: every template, segment, topic, contact property and custom event it names has to resolve. A failure is `422 validation_error` with one entry in `errors[]` per offending step key, so an editor can put each message on its own node rather than fixing one problem per round trip.

Two step types in the schema are not available in this release — `wait_for_event` and `contact_delete` — along with the `timeout` and `event_received` connections that belong to the first. A graph naming any of them is refused with that reason.

## Runs

- One run per contact per enrolling event. Two sends of the same event are two runs; one send redelivered internally is still one.
- A run parked on a `delay` reports `running`, and `?status=running` returns it. A delay is a timestamp the run wakes at, not a process holding a thread — so a worker restart mid-workflow loses nothing, and the public status enum needs no fifth word for it.
- `?status=` on the run list takes a comma-separated list of `running`, `completed`, `failed` and `cancelled`. A member outside those four is `422 invalid_parameter` naming it.
- A single run's `steps[]` come back in graph order — a walk from the trigger of the version that run pinned, taking `condition_met` before `condition_not_met`.

## Endpoints

### `POST /automations`

The workflow and version 1 of its graph, in one call.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | A name for the dashboard. |
| `status` | string | `enabled` or `disabled`. Defaults to `disabled`, so nothing enrols yet. |
| `steps` (required) | object[] | The steps of the workflow, at most 150. Each is `{ key, type, config }` with a `key` unique within the graph. Exactly one step must be a `trigger`. |
| `connections` (required) | object[] | The edges between steps: `{ from, to, type }`, where `from` and `to` are step keys and `type` is `default`, `condition_met` or `condition_not_met`. Omitting `type` means `default`. |

Create an automation:

```sh
curl -X POST "https://api.rasket.com/automations" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Trial nudge",
  "status": "disabled",
  "steps": [
    {
      "key": "start",
      "type": "trigger",
      "config": {
        "event_name": "trial.started"
      }
    },
    {
      "key": "welcome",
      "type": "send_email",
      "config": {
        "template": {
          "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e05",
          "variables": {
            "plan": "pro"
          }
        }
      }
    },
    {
      "key": "wait",
      "type": "delay",
      "config": {
        "duration": "3 days"
      }
    },
    {
      "key": "check",
      "type": "condition",
      "config": {
        "type": "rule",
        "field": "properties.activated",
        "operator": "eq",
        "value": true
      }
    },
    {
      "key": "tag",
      "type": "contact_update",
      "config": {
        "properties": {
          "lifecycle": "activated"
        }
      }
    },
    {
      "key": "onboard",
      "type": "add_to_segment",
      "config": {
        "segment_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e04"
      }
    }
  ],
  "connections": [
    {
      "from": "start",
      "to": "welcome"
    },
    {
      "from": "welcome",
      "to": "wait"
    },
    {
      "from": "wait",
      "to": "check"
    },
    {
      "from": "check",
      "to": "tag",
      "type": "condition_met"
    },
    {
      "from": "check",
      "to": "onboard",
      "type": "condition_not_met"
    }
  ]
}'
```

```ts
const response = await fetch("https://api.rasket.com/automations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Trial nudge",
    status: "disabled",
    steps: [
      {
        key: "start",
        type: "trigger",
        config: {
          event_name: "trial.started"
        }
      },
      {
        key: "welcome",
        type: "send_email",
        config: {
          template: {
            id: "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e05",
            variables: {
              plan: "pro"
            }
          }
        }
      },
      {
        key: "wait",
        type: "delay",
        config: {
          duration: "3 days"
        }
      },
      {
        key: "check",
        type: "condition",
        config: {
          type: "rule",
          field: "properties.activated",
          operator: "eq",
          value: true
        }
      },
      {
        key: "tag",
        type: "contact_update",
        config: {
          properties: {
            lifecycle: "activated"
          }
        }
      },
      {
        key: "onboard",
        type: "add_to_segment",
        config: {
          segment_id: "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e04"
        }
      }
    ],
    connections: [
      {
        from: "start",
        to: "welcome"
      },
      {
        from: "welcome",
        to: "wait"
      },
      {
        from: "wait",
        to: "check"
      },
      {
        from: "check",
        to: "tag",
        type: "condition_met"
      },
      {
        from: "check",
        to: "onboard",
        type: "condition_not_met"
      }
    ]
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/automations",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Trial nudge",
    "status": "disabled",
    "steps": [
      {
        "key": "start",
        "type": "trigger",
        "config": {
          "event_name": "trial.started"
        }
      },
      {
        "key": "welcome",
        "type": "send_email",
        "config": {
          "template": {
            "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e05",
            "variables": {
              "plan": "pro"
            }
          }
        }
      },
      {
        "key": "wait",
        "type": "delay",
        "config": {
          "duration": "3 days"
        }
      },
      {
        "key": "check",
        "type": "condition",
        "config": {
          "type": "rule",
          "field": "properties.activated",
          "operator": "eq",
          "value": True
        }
      },
      {
        "key": "tag",
        "type": "contact_update",
        "config": {
          "properties": {
            "lifecycle": "activated"
          }
        }
      },
      {
        "key": "onboard",
        "type": "add_to_segment",
        "config": {
          "segment_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e04"
        }
      }
    ],
    "connections": [
      {
        "from": "start",
        "to": "welcome"
      },
      {
        "from": "welcome",
        "to": "wait"
      },
      {
        "from": "wait",
        "to": "check"
      },
      {
        "from": "check",
        "to": "tag",
        "type": "condition_met"
      },
      {
        "from": "check",
        "to": "onboard",
        "type": "condition_not_met"
      }
    ]
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01"
}
```

- The whole graph is validated at once. A failure is `422 validation_error` with one entry in `errors[]` per offending step key, not the first problem found.
- Every template, segment, topic, contact property and custom event the graph names must already exist in your team.
- `wait_for_event` and `contact_delete` are parsed and refused in this release: a graph naming either is `422 validation_error`.

### `GET /automations`

Every automation, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | `enabled` or `disabled`. Anything else is `422 invalid_parameter`. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `search` | string | A case-insensitive substring of the automation's name, at most 200 characters. `%` and `_` match literally; a blank term is `422 invalid_parameter`. |

List automations:

```sh
curl -X GET "https://api.rasket.com/automations?status=enabled&limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations?status=enabled&limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/automations?status=enabled&limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01",
      "name": "Trial nudge",
      "status": "enabled",
      "created_at": "2026-09-11T12:00:00.000Z",
      "updated_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- Items carry no graph. Retrieve one automation for its steps and connections.

### `GET /automations/{automation_id}`

With the active version's graph — the one new enrolments will run.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

Retrieve an automation:

```sh
curl -X GET "https://api.rasket.com/automations/{automation_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/automations/{automation_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01",
  "name": "Trial nudge",
  "status": "enabled",
  "created_at": "2026-09-11T12:00:00.000Z",
  "updated_at": "2026-09-11T12:00:00.000Z",
  "steps": [
    {
      "key": "start",
      "type": "trigger",
      "config": {
        "event_name": "trial.started"
      }
    },
    {
      "key": "welcome",
      "type": "send_email",
      "config": {
        "template": {
          "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e05",
          "variables": {
            "plan": "pro"
          }
        }
      }
    },
    {
      "key": "wait",
      "type": "delay",
      "config": {
        "duration": "3 days"
      }
    },
    {
      "key": "check",
      "type": "condition",
      "config": {
        "type": "rule",
        "field": "properties.activated",
        "operator": "eq",
        "value": true
      }
    },
    {
      "key": "tag",
      "type": "contact_update",
      "config": {
        "properties": {
          "lifecycle": "activated"
        }
      }
    },
    {
      "key": "onboard",
      "type": "add_to_segment",
      "config": {
        "segment_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e04"
      }
    }
  ],
  "connections": [
    {
      "from": "start",
      "to": "welcome",
      "type": "default"
    },
    {
      "from": "welcome",
      "to": "wait",
      "type": "default"
    },
    {
      "from": "wait",
      "to": "check",
      "type": "default"
    },
    {
      "from": "check",
      "to": "tag",
      "type": "condition_met"
    },
    {
      "from": "check",
      "to": "onboard",
      "type": "condition_not_met"
    }
  ]
}
```

- `steps` and `connections` come from the **active** version. A run already in flight keeps the version it started on.
- Every connection carries a `type`, even the ones you created without one.
- An automation that belongs to another team, or that has been deleted, is `404 not_found`.

### `PATCH /automations/{automation_id}`

Rename, enable, disable — or publish a new version of the graph.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | A new name. |
| `status` | string | `enabled` or `disabled`. |
| `steps` | object[] | The steps of the workflow, at most 150. Each is `{ key, type, config }` with a `key` unique within the graph. Exactly one step must be a `trigger`. |
| `connections` | object[] | The edges between steps: `{ from, to, type }`, where `from` and `to` are step keys and `type` is `default`, `condition_met` or `condition_not_met`. Omitting `type` means `default`. |

Update an automation:

```sh
curl -X PATCH "https://api.rasket.com/automations/{automation_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "enabled"
}'
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    status: "enabled"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/automations/{automation_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "status": "enabled"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01"
}
```

- Sending `steps` and `connections` writes the **next version** and points the automation at it. The two must be sent together; one without the other is `422 validation_error`.
- A `PATCH` with only `name` and/or `status` creates no version.
- `status: "disabled"` stops new enrolments and leaves runs already in flight alone. To stop those too, use the stop endpoint.
- An empty body is `422`. Send at least one of `name`, `status`, or the graph pair.

### `DELETE /automations/{automation_id}`

Cancels every live run, then removes it.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

Delete an automation:

```sh
curl -X DELETE "https://api.rasket.com/automations/{automation_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/automations/{automation_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01",
  "deleted": true
}
```

- Soft-deletes: the automation is `404 not_found` from this moment, on every operation.
- Each cancelled run raises `automation.run.failed` with the reason `cancelled`.

### `POST /automations/{automation_id}/duplicate`

A disabled copy of the active version, at version 1.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

Duplicate an automation:

```sh
curl -X POST "https://api.rasket.com/automations/{automation_id}/duplicate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}/duplicate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/automations/{automation_id}/duplicate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e09"
}
```

- The copy is named `<name> (copy)` and is always `disabled`, whatever the source's status.
- The graph is copied verbatim and is not re-validated — what it copies was valid when it was published.
- The source is untouched.

### `POST /automations/{automation_id}/stop`

Disable it and cancel every run still going.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

Stop an automation:

```sh
curl -X POST "https://api.rasket.com/automations/{automation_id}/stop" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}/stop", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/automations/{automation_id}/stop",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "automation",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e01",
  "status": "disabled"
}
```

- This is the difference between `stop` and `PATCH { status: "disabled" }`: the `PATCH` stops new enrolments, `stop` also cancels the runs already in flight.
- Stopping an automation that is already disabled still cancels its live runs, and still answers 200.

### `GET /automations/{automation_id}/runs`

One run per contact per enrolling event, newest first.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `status` | string | A comma-separated list of `running`, `completed`, `failed` and `cancelled`. A member outside those four is `422 invalid_parameter`. |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `start_date` | string | ISO 8601, inclusive. Runs created before this are excluded. After `end_date` is `422 invalid_parameter`. |
| `end_date` | string | ISO 8601, inclusive. Runs created after this are excluded. |

List runs:

```sh
curl -X GET "https://api.rasket.com/automations/{automation_id}/runs?status=running%2Cfailed&limit=20" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}/runs?status=running%2Cfailed&limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/automations/{automation_id}/runs?status=running%2Cfailed&limit=20",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e02",
      "status": "failed",
      "failure_reason": "validation_error",
      "failure_message": "The domain is not verified.",
      "failed_step_key": "welcome",
      "started_at": "2026-09-11T12:00:00.000Z",
      "completed_at": "2026-09-14T12:00:00.000Z",
      "created_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- A run parked on a `delay` is reported as `running`, and `?status=running` returns it: a delay is a timestamp the run wakes at, not a process holding a thread.
- A run that failed or was cancelled says why: `failure_reason` is a stable code, the same one the `automation.run.failed` webhook carries; `failure_message` is that ending in a sentence, for a person; `failed_step_key` is the step it stopped at. All three are null on any other run.
- Items carry no steps. Retrieve one run for its step log.

### `GET /automations/{automation_id}/runs/{run_id}`

One contact's journey, step by step, in graph order.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |
| `run_id` (required) | string | The run's ID. |

Retrieve a run:

```sh
curl -X GET "https://api.rasket.com/automations/{automation_id}/runs/{run_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}/runs/{run_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/automations/{automation_id}/runs/{run_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "automation_run",
  "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e02",
  "status": "completed",
  "failure_reason": null,
  "failure_message": null,
  "failed_step_key": null,
  "started_at": "2026-09-11T12:00:00.000Z",
  "completed_at": "2026-09-14T12:00:00.000Z",
  "created_at": "2026-09-11T12:00:00.000Z",
  "steps": [
    {
      "key": "start",
      "type": "trigger",
      "status": "completed",
      "started_at": "2026-09-11T12:00:00.000Z",
      "completed_at": "2026-09-11T12:00:00.000Z",
      "output": {
        "enrolled": true
      },
      "error": null,
      "created_at": "2026-09-11T12:00:00.000Z"
    },
    {
      "key": "welcome",
      "type": "send_email",
      "status": "completed",
      "started_at": "2026-09-11T12:00:00.000Z",
      "completed_at": "2026-09-11T12:00:00.000Z",
      "output": {
        "email_id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e0a"
      },
      "error": null,
      "created_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- `steps` is a breadth-first walk from the trigger of the version this run pinned, taking `condition_met` before `condition_not_met` — the order the editor draws them.
- `output` and `error` are whatever the step produced; their shape depends on the step type. A failed step's `error` is `{ reason, message }`, the same pair the run's own `failure_reason` and `failure_message` carry.
- On a run that failed or was cancelled, `failed_step_key` names the step it stopped at and matches one of `steps[].key`.
- A run of a different automation, or of another team, is `404 not_found`.

### `GET /automations/{automation_id}/versions`

Every published graph, newest first, and which one new enrolments run.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `automation_id` (required) | string | The automation's ID. |

List an automation's versions:

```sh
curl -X GET "https://api.rasket.com/automations/{automation_id}/versions" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/automations/{automation_id}/versions", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/automations/{automation_id}/versions",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "0199d2f1-4c4e-7a20-9c31-6f2b8a0e5e06",
      "version_number": 2,
      "active": true,
      "trigger_event_name": "trial.started",
      "trigger_segment_id": null,
      "steps": [],
      "connections": [],
      "created_by": {
        "type": "api_key",
        "id": "a4d2f0c8-5b31-4e7a-9c62-8f0b1d4e6a75"
      },
      "created_at": "2026-09-11T12:00:00.000Z"
    }
  ]
}
```

- `steps` and `connections` are the graph as it was published; they are elided here.
- To run an earlier version again, `PATCH /automations/{automation_id}` with its `steps` and `connections`. That publishes it as the next version.
