URL: /code/apps/sequences/enroll · Id: `page:/code/apps/sequences/enroll`

# Contact.enroll

> Start one person on a Sequence that sends from your inbox and stops on reply.

`POST https://api.sb/{your-domain}/contacts/enroll`

Start one person on a [Sequence](https://startupbuilder.dev/business/nouns/sequence) that sends from your inbox and stops on reply.

- **Action class:** `send`
- **Approval:** Sends on its own
- **Emits:** `Contact.enrolled`
- **Sites:** Gmail, LinkedIn, Outlook
- **Price:** Free

```text title="Run it · Ask"
Put Maya Chen at Cedar Hollow Rentals on the personal first touch, the first of today’s 12.
```

```ts title="Run it · SDK"
import { $ } from 'startup-builder'

const result = await $.try.Contact.enroll({ contact: 'contact_Mc7yH2kD9s', sequence: 'sequence_Ft2mR6hW8c' })
```

```json title="Run it · Response"
{
  "api": "https://api.sb/leaseline.app",
  "contact": {
    "$id": "https://api.sb/leaseline.app/contacts/contact_Mc7yH2kD9s",
    "$type": "Contact",
    "name": "Maya Chen",
    "email": "maya@cedar-hollow-rentals.example"
  },
  "enrollment": {
    "$type": "Enrollment",
    "sequence": "https://api.sb/leaseline.app/sequences/sequence_Ft2mR6hW8c",
    "status": "active",
    "step": "email-1",
    "nextAt": "2026-10-27T14:12:00Z",
    "branches": [],
    "exitReason": null
  },
  "firstStep": {
    "from": "dana@tryleaseline.app",
    "subject": "64 new units at Cedar Hollow",
    "body": "Maya, saw Cedar Hollow added 64 units this month. Are renewal notices for the new leases still tracked by hand? Leaseline sends each one 60 days out, per owner. Worth a look?"
  },
  "action": {
    "operation": "Contact.enroll",
    "mode": "try",
    "status": "done"
  }
}
```

Starts one person on a Sequence that sends from your Mailbox and stops the moment they reply. [The hub’s definition of `Contact.enroll`](https://startupbuilder.dev/business/nouns/contact/enroll).</HubDefinition>

## Inputs
| Name | Type | Default | Meaning |
| --- | --- | --- | --- |
| `contact` | `Contact` | Required | The Contact to enroll. |
| `sequence` | `Sequence` | Required | The Sequence to run. |

## Outputs
| Field | Type | What `null` means |
| --- | --- | --- |
| `contact` | `Contact` | Never null. |
| `enrollment` | `Enrollment` | Never null. In try mode, the run as it would start; nothing is stored. |
| `enrollment.status` | `string` | Never null: `active` from the first step until the run ends. |
| `enrollment.step` | `string` | Never null: the step the Contact is on, starting at the first. |
| `enrollment.nextAt` | `datetime` | Null while paused or once exited: no step is waiting. |
| `enrollment.exitReason` | `string` | Null while the run is active. |
| `firstStep` | `Message` | Null in send and do. In try mode, the first step rendered for this Contact, so you read it before anything sends. |

## Effects
**Action class**

`send`: an approval rule on `send` can make it wait for your OK

**Modes**

- `try`: Renders the first step for this Contact and when each later step would fire; nothing sends and no Enrollment is stored.
- `send`: Returns the active Enrollment at once; each step sends at its own time.
- `do`: Returns the active Enrollment at once; each step sends at its own time.

**Idempotency**

Only with a key: the same `Idempotency-Key` and input return the first Action, and the same key with other input returns `409`. A Contact already active on the Sequence is never enrolled twice: the repeat returns the open Enrollment.

**Saved or shared**

The Enrollment is saved on the Contact. A reply, a bounce, an unsubscribe or a purchase ends it and records its `exitReason`.

**Emits**

`Contact.enrolled` with `{ contact, enrollment, sequence, nextAt }`

**Performed by**

[Prospector](https://startupbuilder.dev/agents/sales/prospector)

**Price**

Free on your own account: the platform charges nothing for the call.

**Standing approval**

It goes out on its own, inside the Limits of your standing approval: a [`Mandate`](https://startupbuilder.dev/business/nouns/mandate) granted with [`Mandate.authorize`](/code/integrations/calls/authorize). One approval rule makes it wait for your OK.

## On each Venue
| Site | Fields | Formats |
| --- | --- | --- |
| [Gmail](#gmail) | `from`, `to`, `subject`, `body`, `purpose` | None. |
| [LinkedIn](#linkedin) | `body` | 3 |
| [Outlook](#outlook) | `to`, `subject`, `body`, `purpose` | None. |

### Gmail
Through [Gmail](/code/integrations/gmail).

| Our field | Gmail’s field |
| --- | --- |
| `Message.from` | `raw: From header` (Your address, or a send-as alias you set up in Gmail) |
| `Message.to` | `raw: To header` |
| `Message.subject` | `raw: Subject header` (A reply keeps the thread’s subject, so Gmail threads it) |
| `Message.body` | `raw: text/plain and text/html parts` |
| `Message.purpose` | `raw: List-Unsubscribe and List-Unsubscribe-Post headers` (A commercial purpose adds the one-click unsubscribe headers Gmail shows as a link) |

- **Cold sends.** 30 a day per inbox after 14 days of warm-up, held by your standing approval; a Sequence spreads the rest over the next days.
- **Replies.** Gmail pushes each new message within seconds; the platform renews the mailbox watch daily, inside the 7 days Gmail allows.

### LinkedIn
Through [LinkedIn](/code/integrations/linkedin).

| Our field | LinkedIn’s field |
| --- | --- |
| `Message.body` | `invitation note or message` (Typed into LinkedIn’s own screens by the browser agent) |

- **Follow-ups.** One first message and one follow-up per person; any reply stops the rest.
- **Replies.** Read from your LinkedIn inbox every 30 minutes on weekdays, since LinkedIn sends no webhook for member messages.

Feed post Single image ad, 1200 × 628, 1200 × 1200 or 720 × 900 Document ad

### Outlook
Through [Outlook](/code/integrations/outlook).

| Our field | Outlook’s field |
| --- | --- |
| `Message.to` | `message.toRecipients` |
| `Message.subject` | `message.subject` |
| `Message.body` | `message.body` (contentType html or text) |
| `Message.purpose` | `message.internetMessageHeaders` (A commercial purpose adds the one-click unsubscribe headers) |

- **Cold sends.** 30 a day per inbox after 14 days of warm-up, held by your standing approval; a Sequence spreads the rest over the next days.

## What try does
| Account | What try does |
| --- | --- |
| [Gmail](/code/integrations/gmail#try) | Renders the exact email, headers included, and returns it with the time it would send; nothing reaches Gmail and no draft is saved. |
| [LinkedIn](/code/integrations/linkedin#try) | Posts render as LinkedIn will show them and nothing is sent; requests and messages return each person, the note and the day it goes, and no browser session opens. |
| [Outlook](/code/integrations/outlook#try) | Renders the exact email or invite and returns it with the time it would go; nothing reaches Outlook and no draft or event is saved. |

## Refuses when
| Check | Error | Fix | Then call |
| --- | --- | --- | --- |
| A send or Sequence step whose purpose the Contact has given no Consent for | [`no-consent`](/code/refusals#check-no-consent) | Record the Contact’s yes, with where they gave it, before the first send. | [`Consent.create`](https://startupbuilder.dev/business/nouns/consent/create) |
| A send or Sequence step to a Contact on a Suppression, from any Play | [`suppressed-contact`](/code/refusals#check-suppressed-contact) | Leave them out: run the List again, which drops every suppressed Contact. | [`Collection.query`](https://startupbuilder.dev/business/nouns/collection/query) |
| A new-lead send to a Contact with an active Subscription | [`customer-collision`](/code/refusals#check-customer-collision) | Talk to them yourself instead: the Contact leaves the List. | [`Lead.assign`](https://startupbuilder.dev/business/nouns/lead/assign) |
| A first cold email from a Mailbox that has not finished warming | [`mailbox-warm`](/code/refusals#check-mailbox-warm) | Wait for the warm-up to finish, 14 days from day 0; warm intros keep running. | [`Mailbox.warm`](https://startupbuilder.dev/business/nouns/mailbox/warm) |

Every call through an account can also meet [the shared account refusals](/code/refusals#accounts): [`grant-missing`](/code/refusals#grant-missing), [`needs-reauth`](/code/refusals#needs-reauth), [`account-not-connected`](/code/refusals#account-not-connected), [`over-mandate`](/code/refusals#over-mandate), [`quota-reached`](/code/refusals#quota-reached), [`venue-rejected`](/code/refusals#venue-rejected).

```json
{
  "type": "https://startupbuilder.dev/errors/no-consent",
  "title": "No yes on record",
  "status": 422,
  "detail": "A send or Sequence step whose purpose the Contact has given no Consent for.",
  "fix": "Record the Contact’s yes, with where they gave it, before the first send.",
  "next": {
    "verb": "Consent.create",
    "input": {}
  },
  "instance": "https://api.sb/leaseline.app/actions/action_1"
}
```

## When it waits
Returns in one response; nothing waits.

## Example

**Ask**

```text
Put Maya Chen at Cedar Hollow Rentals on the personal first touch, the
first of today’s 12.
```

**API**

```bash
curl -X POST "https://api.sb/leaseline.app/contacts/enroll?mode=try" \
  -H "Authorization: Bearer $SB_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"contact":"contact_Mc7yH2kD9s","sequence":"sequence_Ft2mR6hW8c"}'
```

The response is the one in the Run it window above.

**CLI**

```bash
npx startup-builder do Contact.enroll --try --data '{"contact":"contact_Mc7yH2kD9s","sequence":"sequence_Ft2mR6hW8c"}'
```

**MCP**

```json
{
  "name": "do",
  "arguments": {
    "mode": "try",
    "code": "return await $.Contact.enroll({ contact: 'contact_Mc7yH2kD9s', sequence: 'sequence_Ft2mR6hW8c' })"
  }
}
```

**SDK**

```ts
import { $ } from 'startup-builder'

const result = await $.try.Contact.enroll({
  contact: 'contact_Mc7yH2kD9s',
  sequence: 'sequence_Ft2mR6hW8c',
})
```

To retry, send the same `Idempotency-Key`: the retry returns the first answer for $0. A new key reads again.

A `sk_test_` key answers these the same way every time, at $0 ([every test value](/code/data#test-values)).

| Test value | Always returns | What it is for |
| --- | --- | --- |
| `contact_test_active` | `{"status":"active","step":"email-1","exitReason":null}` | Enrolls and renders the first step without sending, so your agent can read the run it starts. |
| `contact_test_suppressed` | Refuses with [`suppressed-contact`](/code/refusals#check-suppressed-contact) | The address sits on a Suppression, so the enroll refuses before any step is planned. |
| `contact_test_no_consent` | Refuses with [`no-consent`](/code/refusals#check-no-consent) | The Contact gave no Consent for the Sequence’s purpose, so the enroll refuses. |
| `maxCost: 0.0001` | Refuses with [`max-cost`](/code/refusals#max-cost) | Any priced call with a maxCost of 0.0001 is refused before it spends: the price is above the cap. |
| `rate-limited.test` | Refuses with [`rate-limited`](/code/refusals#rate-limited) | Any call naming this hostname answers 429 with Retry-After, so your retry logic runs. |
| `sources-busy.test` | Refuses with [`sources-busy`](/code/refusals#sources-busy) | Any call naming this hostname answers 503 as if every route were busy, and nothing is spent. |

## Used by
- **Agent tasks:** [Personal first touch](/capabilities/reach-out/personal-first-touch)
- **Jobs:** [Reach out](/capabilities/reach-out)
- **Integrations:** [Gmail](/code/integrations/gmail) · [LinkedIn](/code/integrations/linkedin) · [Outlook](/code/integrations/outlook)

## Related
None.