# Steps

> Steps are the ordered sequence of touches (email, LinkedIn) leads move through, with a delay before each one.

Source: https://warmerly.com/docs/campaigns/steps

Steps are the ordered sequence of touches (email, LinkedIn) leads move through, with a delay before each one.

## List steps

```text
GET /campaigns/{id}/steps
```

`steps` is the sequence in send order. This is the field to read. The response also carries `nodes`, `edges` and `entryStepId`, the same steps as a graph, which the dashboard's visual editor uses; you can ignore them.

A step's `waitDays` / `waitHours` is the delay **before** that step sends, counted from the previous step (the first step's delay is counted from when the lead enters the campaign).

```json
{
  "steps": [
    {
      "id": "c1b2a3d4-1111-4b7a-8e2a-1a2b3c4d5e6f",
      "campaignId": "b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f",
      "stepOrder": 1,
      "kind": "email",
      "subject": "Quick question about {{companyName}}",
      "bodyText": "Hi {{firstName}}, ...",
      "bodyHtml": null,
      "waitDays": 0,
      "waitHours": 0,
      "variants": [],
      "createdAt": "2026-06-01T09:00:00.000Z"
    },
    {
      "id": "c1b2a3d4-3333-4b7a-8e2a-1a2b3c4d5e6f",
      "campaignId": "b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f",
      "stepOrder": 2,
      "kind": "email",
      "subject": "Following up",
      "bodyText": "Hi {{firstName}}, just bumping this...",
      "bodyHtml": null,
      "waitDays": 3,
      "waitHours": 0,
      "variants": [],
      "createdAt": "2026-06-01T09:00:00.000Z"
    }
  ],
  "nodes": ["…the same steps…"],
  "edges": ["…how they connect…"],
  "entryStepId": "c1b2a3d4-1111-4b7a-8e2a-1a2b3c4d5e6f"
}
```

Each step is the stored step row, so it has further fields than shown (for example `aiPersonalize`, `variantMinSampleSize`, and the editor's `positionX` / `positionY`), and `variants` lists any A/B variants. `kind` is one of `email`, `linkedin_connection`, `linkedin_message`.

## Replace steps

```text
PUT /campaigns/{id}/steps
```

Replace-all: send the whole sequence, in order, as `steps` (up to 50 entries). `stepOrder` is assigned from array position, don't send it yourself.

| Field | Type | Required |
| --- | --- | --- |
| `kind` | `"email" \\| "wait" \\| "linkedin_connection" \\| "linkedin_message"` | no (default `"email"`) |
| `subject` | string or `null` | no (needed for an email step to send) |
| `bodyText` | string or `null` | no (needed for an email step to send) |
| `bodyHtml` | string or `null` | no |
| `waitDays` | integer (0-365) | no (default `0`) |
| `waitHours` | integer (0-23) | no (default `0`) |

Two ways to space steps out, and you can mix them:

- put `waitDays` / `waitHours` on the step itself, or
- insert a `{ "kind": "wait", "waitDays": 3 }` entry between two steps.

A `wait` entry is not stored as a step of its own. Its delay is added to the next step, so `GET` returns the example below as **two** steps, the second with `waitDays: 3`. A `wait` at the very end is dropped (there is nothing after it to delay).

Subjects and bodies support merge tags: `{{firstName}}`, `{{lastName}}`, `{{companyName}}`, `{{email}}`, the sender's `{{senderFirstName}}` / `{{senderName}}`, and any `customVars` key you set on the campaign's leads.

**Editing a running campaign:** the Nth email step you send keeps the id of the campaign's existing Nth email step, so rewriting the copy of a live sequence does not move leads off the step they are on. On an active or paused campaign, removing a step that leads are currently waiting at is refused with a `409` (`step_in_use`, with the step ids and lead counts in `error.details`) and nothing is changed.

```bash
curl -X PUT https://app.warmerly.com/api/v1/campaigns/b6a1e7b0-2f2b-4b7a-8e2a-1a2b3c4d5e6f/steps   -H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"   -H "Content-Type: application/json"   -d '{
    "steps": [
      { "kind": "email", "subject": "Quick question about {{companyName}}", "bodyText": "Hi {{firstName}}, ..." },
      { "kind": "wait", "waitDays": 3 },
      { "kind": "email", "subject": "Following up", "bodyText": "Hi {{firstName}}, just bumping this..." }
    ]
  }'
```

Returns the saved sequence in the same shape as `GET /campaigns/{id}/steps`.

| Status | `error.code` | When |
| --- | --- | --- |
| `400` | `bad_request` | The body is not a valid `steps` array (the details name the field) |
| `404` | `not_found` | The campaign doesn't exist or isn't yours |
| `409` | `step_in_use` | You removed a step leads are currently waiting at, on an active or paused campaign |

## More from Campaigns

- [Stats](https://warmerly.com/docs/campaigns/stats): Aggregated counters and rates for the campaign.
- [Senders (email)](https://warmerly.com/docs/campaigns/senders-email): Mailboxes (connected Accounts) that send this campaign's emails, rotated in round-robin to spread volume and…
- [LinkedIn senders](https://warmerly.com/docs/campaigns/linkedin-senders): Connected LinkedIn accounts used for linkedinconnection / linkedinmessage steps.
