Skip to content

Using sequences with the API

Create a sequence with agent or template steps, test and publish its draft, enroll contacts, change it while it runs, and read what happened, with one sequence carried from start to finish.

This guide builds one sequence over the REST API and follows it from creation to its first replies. Responses are trimmed to the fields each step is about. The Sequence API reference has the full shape of each one.

A sequence has a published version, which is what runs, and at most one open draft, which is where edits happen. Enrolled contacts follow the published version. A draft changes nothing until it is published.

create -> draft -> publish -> DEACTIVATED -> activate -> ACTIVE

Once published, the loop repeats: open a draft, edit it, publish it. The sequence keeps its status and its enrollments through each publish.

Enrollments and messages belong to the published version, so a sequence keeps running while a draft is open.

Writes, including starting a test, need the sequences:update scope, and creating a sequence needs sequences:create. Reads need sequences:read. Listing the variables a template can use also needs contacts:read and accounts:read. See Scopes.

A sequence refers to two other resources by ID, so look them up first: the senders it sends from, and the members who review messages before they go out.

Terminal window
curl "https://api.lightfield.app/v1/senders?channel[equal]=EMAIL&archived[equal]=false" \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"object": "list",
"data": [
{ "id": "snd_cmuyynvgn000d", "channel": "EMAIL", "health": "HEALTHY", "emailAddress": "ada@outbound.example.com" },
{ "id": "snd_cmuyynvgq000f", "channel": "EMAIL", "health": "HEALTHY", "emailAddress": "ben@outbound.example.com" }
],
"totalCount": 2
}

Sender IDs start with snd_. Member IDs, from List members, start with mem_. Contact IDs start with con_.

Create takes a name, the definition (messaging, steps, send window, pacing), and the senders and reviewers. Only name and definition.steps are required.

Terminal window
curl https://api.lightfield.app/v1/sequences \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-H 'Idempotency-Key: q4-outbound-create' \
-d '{
"name": "Q4 outbound",
"definition": {
"description": "Book intro calls with heads of RevOps",
"messaging": {
"persona": "Head of RevOps",
"valueProp": "Clean CRM with no manual entry",
"callToAction": "15-minute intro call"
},
"sendWindow": { "startHour": 8, "endHour": 18, "timezone": "America/New_York" },
"pacing": { "EMAIL": { "dailyLimit": 40 } },
"steps": [
{ "type": "EMAIL", "name": "Intro", "prompt": "Introduce Lightfield in three sentences.", "deliveryMode": "AUTO" },
{ "type": "WAIT", "duration": 2, "unit": "DAY" },
{ "type": "EMAIL", "name": "Follow-up", "prompt": "Short follow-up on the intro.", "deliveryMode": "FOR_REVIEW" }
]
},
"reviewers": ["mem_cmuyynvg60005"],
"senders": ["snd_cmuyynvgn000d", "snd_cmuyynvgq000f"]
}'
{
"sequenceId": "seq_cmuyyy75r0016",
"name": "Q4 outbound",
"version": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDox",
"channels": ["EMAIL"],
"definition": {
"description": "Book intro calls with heads of RevOps",
"messaging": { "persona": "Head of RevOps", "valueProp": "Clean CRM with no manual entry", "trigger": null, "callToAction": "15-minute intro call" },
"sendWindow": { "startHour": 8, "endHour": 18, "daysOfWeek": [1, 2, 3, 4, 5], "timezone": "America/New_York" },
"pacing": { "EMAIL": { "dailyLimit": 40 }, "LINKEDIN_INVITE": { "dailyLimit": null }, "LINKEDIN_MESSAGE": { "dailyLimit": null } },
"priority": "NORMAL",
"steps": [
{ "id": "step-1", "type": "EMAIL", "name": "Intro", "mode": "AGENT", "prompt": "Introduce Lightfield in three sentences.", "deliveryMode": "AUTO" },
{ "id": "step-2", "type": "WAIT", "duration": 2, "unit": "DAY" },
{ "id": "step-3", "type": "EMAIL", "name": "Follow-up", "mode": "AGENT", "prompt": "Short follow-up on the intro.", "deliveryMode": "FOR_REVIEW" }
]
},
"reviewers": [{ "id": "mem_cmuyynvg60005", "objectType": "member" }],
"senders": [
{ "id": "snd_cmuyynvgn000d", "objectType": "sender" },
{ "id": "snd_cmuyynvgq000f", "objectType": "sender" }
],
"publishWarnings": []
}

Three things to notice in the response:

  • It is a draft, not a sequence. It has a sequenceId and a version, not an id and a status.
  • Defaults were filled in. daysOfWeek is Monday to Friday, priority is NORMAL, every pacing key is present with null for the ones you did not set, and each step has an id. Each send step also has a mode: AGENT for a step written from its prompt, TEMPLATE for one that sends a template.
  • Nothing can send yet. The sequence exists, but it has no published version.

The Idempotency-Key header makes the create safe to retry. A replay with the same key returns the same draft.

A create that fails leaves nothing behind. The response says what to fix:

{
"error": {
"type": "bad_request",
"code": "invalid_configuration",
"message": "The draft has 1 problem. Nothing was saved.",
"issues": [
{ "code": "invalid_value", "path": "definition.sendWindow.startHour", "stepId": null, "message": "startHour must be before endHour." }
]
}
}

Every refused sequence write has this shape. Sequence errors lists the codes.

A sequence can mix email and LinkedIn. LinkedIn has three step types: LINKEDIN_INVITE sends a connection request, with includeNote: true to attach a note; WAIT_FOR_CONNECTION holds for up to its duration until the request is accepted; and LINKEDIN_MESSAGE sends a direct message, which LinkedIn only delivers to a connection. So the shape is invite, wait for connection, then message:

"steps": [
{ "type": "LINKEDIN_INVITE", "name": "Connect", "prompt": "Ask to connect, one line on why.", "deliveryMode": "AUTO", "includeNote": true },
{ "type": "WAIT_FOR_CONNECTION", "duration": 5, "unit": "DAY" },
{ "type": "LINKEDIN_MESSAGE", "name": "Hello", "prompt": "Thank them for connecting and ask for 15 minutes.", "deliveryMode": "AUTO" },
{ "type": "WAIT", "duration": 3, "unit": "DAY" },
{ "type": "EMAIL", "name": "Follow-up", "prompt": "Short follow-up by email.", "deliveryMode": "AUTO" }
]

If the wait runs out before they accept, later LinkedIn messages are skipped and email steps still run. LinkedIn steps need a LinkedIn sender in senders and a plan that includes LinkedIn; without the plan, the save is refused with a not_entitled issue. The ordering rules and the note budget are covered in Building sequences.

A send step either has the agent write each message from a prompt, or sends copy you write as a template, with variables filled in from each contact’s record. One sequence can mix both. Send a template instead of a prompt and the step is a template step; mode is optional, because the content decides it.

"steps": [
{
"type": "EMAIL",
"name": "Intro",
"deliveryMode": "AUTO",
"template": {
"subject": "{{ coalesce({rel:account.col:name}, \"Your team\") }} and clean CRM data",
"body": "Hi {{ coalesce({col:firstName}, \"there\") }},\n\nLightfield keeps your CRM up to date with **no manual entry**. Worth a [15-minute intro](https://lightfield.app/demo)?"
}
},
{ "type": "WAIT", "duration": 3, "unit": "DAY" },
{
"type": "EMAIL",
"name": "Follow-up",
"deliveryMode": "FOR_REVIEW",
"template": {
"subject": "Following up",
"body": "Hi {{ coalesce({col:firstName}, \"there\") }}, any thoughts on my note? Happy to send a 2-minute video instead."
}
}
]

The template’s shape depends on the step type:

Step typetemplate
EMAILsubject, plain text, and body, in Markdown. Paragraphs, bold, italics, links and lists keep their formatting. Leave out the sign-off and signature; they are added when the message is written.
LINKEDIN_MESSAGEbody, plain text.
LINKEDIN_INVITEbody, plain text: the connection note, sent only when includeNote is true.

Every EMAIL template needs a subject. Follow-up emails reply in the first email’s thread, so only the first email’s subject is sent. Template steps send exactly what you write: no quality checks are applied to the copy.

Write every variable with a fallback, the text a contact missing that value gets:

{{ coalesce(<variable>, "fallback") }}

An empty fallback, "", is allowed and inserts nothing, so write the text around it to read well without the value: Following up, {{ coalesce({col:firstName}, "") }}. sends Following up, . to a contact with no first name. These are the variables a template can read:

VariableReads
{col:firstName}, {col:lastName}The contact’s first or last name
{attr:ad_…}A field of the contact
{rel:account.col:name}The name of the contact’s first account
{rel:account.ad_…}A field of the contact’s first account
{rel:sender.col:name}, {rel:sender.col:email}The name or email address of the sender the message goes out from

A field variable names the field by the ad_ ID that List contact field definitions and List account field definitions return. The field must be of type TEXT, NUMBER, CURRENCY, DATETIME, SINGLE_SELECT, FULL_NAME, EMAIL or URL. A SINGLE_SELECT field prints its option’s label, written {attr:ad_….label} or {rel:account.ad_….label}.

Rather than assembling the references yourself, List template variables returns every variable this workspace can use:

Terminal window
curl https://api.lightfield.app/v1/sequences/template/variables \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"object": "list",
"data": [
{ "label": "First name", "group": "contact", "ref": "{{ {col:firstName} }}", "field": null },
{ "label": "Job title", "group": "contact", "ref": "{{ {attr:ad_cmuyynvh2000h} }}", "field": { "id": "ad_cmuyynvh2000h", "valueType": "TEXT" } },
{ "label": "Name", "group": "account", "ref": "{{ {rel:account.col:name} }}", "field": null },
{ "label": "Industry", "group": "account", "ref": "{{ {rel:account.ad_cmuyynvh5000j.label} }}", "field": { "id": "ad_cmuyynvh5000j", "valueType": "SINGLE_SELECT" } },
{ "label": "Name", "group": "sender", "ref": "{{ {rel:sender.col:name} }}", "field": null }
],
"totalCount": 5
}

A ref sent as returned is refused, because it has no fallback. Put the part inside {{ }} into coalesce: {{ {col:firstName} }} is written {{ coalesce({col:firstName}, "there") }}.

A save is refused with invalid_value, and path pointing at the subject or body (definition.steps.0.template.body), when a template has:

  • a variable without a fallback,
  • anything inside {{ }} that is not a variable, since it would be sent as written, or
  • a field that is not one of the types above, or that does not exist.

A variable inside a link address, such as [demo](https://example.com/{{ … }}), is refused too, but as 400 invalid_configuration with only a message, and no issues.

Template steps must be turned on for the workspace. If they are not, a save that adds or changes a template is refused with a not_entitled issue at definition.steps.N.mode; write the step with a prompt instead.

Template steps read back with mode: "TEMPLATE" and their template, and no prompt. Field variables read back with their ad_ IDs, so a template you read can be sent back unchanged.

Retrieve the sequence and its draft side by side.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016 \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"id": "seq_cmuyyy75r0016",
"objectType": "sequence",
"name": "Q4 outbound",
"status": "DRAFT",
"channels": [],
"definition": null,
"reviewers": [],
"senders": [],
"firstActivatedAt": null,
"lastActivatedAt": null
}

The sequence is DRAFT with no definition, no channels and no senders, because none of that is published yet. The content you sent lives on the draft:

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/draft \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"

This returns exactly what create returned. The same split holds for the rest of the API: List sequences includes the new sequence with definition: null, Retrieve sequence performance reports zero counts and no steps, and enrolling a contact returns 400 sequence_not_published.

Saves are partial: send the fields you want to change, with the draft’s current version as expectedVersion.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/draft \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{
"expectedVersion": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDox",
"definition": {
"messaging": { "trigger": "Just raised a Series B" },
"sendWindow": { "startHour": 9 },
"pacing": { "EMAIL": { "dailyLimit": null } }
}
}'
{
"version": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDoy",
"definition": {
"messaging": { "persona": "Head of RevOps", "valueProp": "Clean CRM with no manual entry", "trigger": "Just raised a Series B", "callToAction": "15-minute intro call" },
"sendWindow": { "startHour": 9, "endHour": 18, "daysOfWeek": [1, 2, 3, 4, 5], "timezone": "America/New_York" },
"pacing": { "EMAIL": { "dailyLimit": null }, "LINKEDIN_INVITE": { "dailyLimit": null }, "LINKEDIN_MESSAGE": { "dailyLimit": null } }
}
}

Only the three fields changed, and null cleared the daily limit. The response carries a new version; use it on the next save.

Two fields do not merge. definition.steps replaces the whole step list: include the id of each step you keep, in the same relative order and with the same type, and omit the id for a new step. Reviewers and senders take add and remove lists instead of a full list.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/draft \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{
"expectedVersion": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDoy",
"definition": {
"steps": [
{ "id": "step-1", "type": "EMAIL", "name": "Intro", "prompt": "Introduce Lightfield in two sentences.", "deliveryMode": "AUTO" },
{ "type": "WAIT", "duration": 3, "unit": "DAY" },
{ "id": "step-3", "type": "EMAIL", "name": "Follow-up", "prompt": "Short follow-up on the intro.", "deliveryMode": "FOR_REVIEW" }
]
},
"reviewers": { "add": ["mem_cmuyynvgh000b"] }
}'

The two-day wait is gone and a three-day wait took its place with a new id; step-1 and step-3 kept theirs.

To keep a step’s content, send it back as you read it. Changing a kept step between agent and template follows its content: send a template to make it a template step, or a prompt with mode: "AGENT" to make it an agent step. A kept template step sent with only a prompt is refused, so a template is never dropped by accident.

version changes on every edit, from the API or from someone working on the same draft in Lightfield. A save or publish whose expectedVersion is behind returns 409:

{
"error": {
"type": "conflict",
"message": "The sequence draft changed since it was read. Retrieve the draft and retry with its version."
}
}

Retrieve the draft, look at what changed, and retry with the current version. The draft is never half-saved: a refused write changes nothing.

A test runs the sequence on one real contact without sending anything. Steps run back to back with waits skipped, and each message is composed from the contact’s data, so you can read what an agent step writes and what a template’s variables and fallbacks produce before anyone receives it.

A test is not an enrollment. It does not appear in enrollments, messages or performance, and the contact can still be enrolled. It does need a sender that can send for each channel the steps use, since sender variables and the signature are filled from it.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/tests \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{ "contactId": "con_cmuyyy6zv0007", "expectedVersion": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDoz" }'
{ "id": "sqe_t3stq8m2vk1o", "status": "RUNNING", "target": "DRAFT", "contactId": "con_cmuyyy6zv0007" }

The test runs the open draft, or the published version when no draft is open; target says which. With expectedVersion, it runs exactly the draft you last read, and returns 409 if the draft has changed or is no longer open, for example after a publish or discard. To test the published version, leave expectedVersion out. Editing the draft after a start does not change a test already running.

Starting a test is not idempotent: it does not read Idempotency-Key, so a retry after a timeout starts a second test. Keep the id from the response. It is the only way to read the result, since tests are not listed anywhere.

The request returns right away. Poll the test every few seconds until status is COMPLETED or FAILED, and give up after a few minutes. While it is RUNNING, steps fill in as they finish; act only on the final result.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/tests/sqe_t3stq8m2vk1o \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"id": "sqe_t3stq8m2vk1o",
"status": "COMPLETED",
"target": "DRAFT",
"contactId": "con_cmuyyy6zv0007",
"steps": [
{
"stepId": "step-1",
"name": "Intro",
"type": "EMAIL",
"mode": "AGENT",
"outcome": "COMPOSED",
"message": { "subject": "Clean CRM data at Acme", "body": "Hi Grace,\n\nLightfield keeps your CRM up to date without manual entry. Open to a short intro?" },
"error": null
},
{ "stepId": "step-tf9jgb7g", "name": null, "type": "WAIT", "mode": null, "outcome": "SKIPPED", "message": null, "error": null },
{
"stepId": "step-3",
"name": "Follow-up",
"type": "EMAIL",
"mode": "AGENT",
"outcome": "COMPOSED",
"message": { "subject": "Following up on Lightfield", "body": "Hi Grace, following up on my note from Tuesday." },
"error": null
}
]
}

Each step has one outcome:

OutcomeMeaning
COMPOSEDmessage holds exactly what this contact would get: subject and a Markdown body for email, body for LinkedIn, with template variables and fallbacks filled in.
SKIPPEDThe step ran and produced nothing. A wait is always SKIPPED in a test, and so is a send step with nothing to send, such as an invite another step already sent.
FAILEDerror says why: CONFIGURATION_ERROR when the sequence or the contact needs a change, such as a template variable that cannot be filled, or SYSTEM_ERROR when Lightfield failed.
PENDINGThe test has not reached the step, including a step a finished test never reached.

A step that fails ends the test as FAILED, and the steps after it stay PENDING. So check each step’s outcome, not only the test’s status. A follow-up email’s subject is the one its step composes; it is sent as a reply in the first email’s thread.

A test of template steps shows the copy with its variables and fallbacks filled in. Here the contact has no first name, so the first email used the fallback, and the follow-up read a field that was deleted after the template was written:

{
"id": "sqe_t3stz4q9hb2m",
"status": "FAILED",
"target": "DRAFT",
"contactId": "con_cmuyyy72u0009",
"steps": [
{
"stepId": "step-1",
"name": "Intro",
"type": "EMAIL",
"mode": "TEMPLATE",
"outcome": "COMPOSED",
"message": { "subject": "Acme and clean CRM data", "body": "Hi there,\n\nLightfield keeps your CRM up to date with **no manual entry**. Worth a [15-minute intro](https://lightfield.app/demo)?" },
"error": null
},
{ "stepId": "step-2", "name": null, "type": "WAIT", "mode": null, "outcome": "SKIPPED", "message": null, "error": null },
{
"stepId": "step-3",
"name": "Follow-up",
"type": "EMAIL",
"mode": "TEMPLATE",
"outcome": "FAILED",
"message": null,
"error": { "code": "CONFIGURATION_ERROR", "message": "The template reads a field that no longer exists: Job title. Edit the step to remove it." }
}
]
}

error.message is plain language and says what to fix.

A test never sends anything, whatever the steps’ delivery modes, and nothing is added to For review. Agent steps use model credits, as they do when the sequence runs.

A start is refused with:

  • 400 invalid_configuration and invalidContacts when the contact does not exist or lacks what a step needs (an email address, a LinkedIn URL), or with issues when the draft cannot run;
  • 400 no_selectable_sender when a channel the steps use has no sender that can send, even though nothing is sent;
  • 400 permissions_not_granted when an agent step built in Lightfield requests permissions nobody has granted;
  • 409 when expectedVersion is not the open draft’s version, or no draft is open;
  • 429 too_many_requests when the workspace already has 10 tests running (retry once one is COMPLETED or FAILED), or has started more than 5 tests a minute (retry after the Retry-After header’s seconds). message names the limit, and no test is started;
  • 403 when the API key does not have sequences:update.

Reading a test returns 404 for a test that does not exist or belongs to another sequence, and for an enrollment ID; read enrollments with Retrieve a sequence enrollment.

Publishing turns the draft into the published version.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/draft/publish \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{ "expectedVersion": "Y211eXl5NzV3MDAxOGl0OTVyMGluZGxtcDoz" }'
{
"id": "seq_cmuyyy75r0016",
"status": "DEACTIVATED",
"channels": ["EMAIL"],
"definition": { "steps": [ { "id": "step-1", "type": "EMAIL" }, { "id": "step-tf9jgb7g", "type": "WAIT" }, { "id": "step-3", "type": "EMAIL" } ] },
"reviewers": [{ "id": "mem_cmuyynvg60005", "objectType": "member" }, { "id": "mem_cmuyynvgh000b", "objectType": "member" }],
"senders": [{ "id": "snd_cmuyynvgn000d", "objectType": "sender" }, { "id": "snd_cmuyynvgq000f", "objectType": "sender" }]
}

The response is the sequence, now with a definition, channels and senders. The first publish leaves it DEACTIVATED: published, able to take enrollments, not yet sending. The draft is closed, so Retrieve a sequence draft now returns 404 draft_not_found.

A refused publish lists its problems under issues the same way a refused save does. Publish also checks things a save does not: that every reviewer is still an active member, that every sender is still usable, and that the plan covers every channel the steps use.

Activate starts sending to enrolled contacts.

Terminal window
curl -X POST https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/activate \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"id": "seq_cmuyyy75r0016",
"status": "ACTIVE",
"firstActivatedAt": "2026-10-08T03:25:04.208Z",
"lastActivatedAt": "2026-10-08T03:25:04.208Z"
}

Activate needs an available sender for each channel the steps use; without one it returns 400 no_selectable_sender and the sequence stays as it was. Activating an active sequence returns it unchanged, and the same goes for deactivating a paused one.

Deactivate pauses sending. Enrollments keep their place and resume when the sequence is activated again. In both directions the sequence’s status changes at once, while the enrollments’ statuses catch up in the background.

Enrollment takes contact IDs to add, to remove, or both, in one request.

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/enrollments \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{ "add": ["con_cmuyyy6zv0007", "con_cmuyyy72u0009"] }'
{
"results": [
{ "contactId": "con_cmuyyy6zv0007", "outcome": "enrolled", "enrollmentId": "sqe_vk1olqpc1500" },
{ "contactId": "con_cmuyyy72u0009", "outcome": "enrolled", "enrollmentId": "sqe_ca90sfmok1kf" }
]
}

Each contact gets one of four outcomes. enrolled and unenrolled did what they say. already_enrolled means the contact had an open enrollment, which was left alone. no_active_enrollment means a removal found nothing to cancel. Additions are checked as a group: if any contact cannot be enrolled, the request returns 400 invalid_configuration with the contact and reason under invalidContacts, and nobody is enrolled.

Right after enrolling, the enrollment is on the first step:

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/enrollments/sqe_vk1olqpc1500 \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"id": "sqe_vk1olqpc1500",
"contactId": "con_cmuyyy6zv0007",
"status": "ACTIVE",
"currentStepId": "step-1",
"remainingSends": 2,
"enrolledBy": "mem_cmuyyicgi000b",
"enrolledAt": "2026-10-08T03:26:11.902Z",
"lastActivityAt": null,
"lastActivityKind": null,
"repliedAt": null,
"completedAt": null,
"cancelledAt": null
}

currentStepId matches a step in the published definition.steps, and remainingSends counts the email and LinkedIn steps still ahead. You can enroll into a deactivated sequence; the contacts wait and are sent to once it is activated.

To take a contact out, put their ID in remove. Their remaining steps stop, and the enrollment stays readable with status CANCELLED and a cancelledAt. A contact whose enrollment ended, for any reason, can be enrolled again with add.

Editing a published sequence is the same draft loop. A save with no expectedVersion opens a new draft from the published version:

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/draft \
-H 'Content-Type: application/json' \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY" \
-d '{
"definition": {
"steps": [
{ "id": "step-1", "type": "EMAIL", "name": "Intro", "prompt": "Introduce Lightfield in one sentence and ask for 15 minutes.", "deliveryMode": "AUTO" },
{ "id": "step-tf9jgb7g", "type": "WAIT", "duration": 3, "unit": "DAY" },
{ "type": "EMAIL", "name": "Breakup", "prompt": "Polite last note.", "deliveryMode": "AUTO" }
]
}
}'
{
"version": "Y211eXoyM3BzMDAxa2l0OTVxYTFibmtoYjox",
"publishWarnings": [
{
"code": "step_removed",
"severity": "destructive",
"stepIds": ["step-3"],
"message": "\"Follow-up\" was removed. Contacts that have not reached it will skip it, and any message waiting for review at that step is recalled."
},
{
"code": "automatic_send_step_inserted",
"severity": "destructive",
"stepIds": ["step-aus2eesh"],
"message": "\"Breakup\" was added and sends automatically. Contacts that have not passed this point may receive it."
}
]
}

publishWarnings is the part to read before publishing a live sequence. It describes what the change does to contacts already enrolled; destructive means some of them may skip, repeat or receive a step they would not have. A publish never recalls a message that is already queued or being sent. While the draft is open, the sequence keeps running on the published version, and Retrieve a sequence still shows the old steps.

Publish when you are happy with the warnings. The sequence stays ACTIVE, and enrolled contacts continue on the new version from wherever they are. If you change your mind, Discard a sequence draft with the draft’s version throws the draft away and leaves the published version untouched.

Three reads cover what is happening. Enrollments, filtered by status, show where each contact is:

Terminal window
curl "https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/enrollments?status[equal]=AWAITING_REVIEW" \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"

Messages are every email and LinkedIn message the sequence sent, plus each enrollment’s first reply. Filter by direction to see replies, or by status to see what is still waiting to go out:

Terminal window
curl "https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/messages?direction[equal]=INBOUND" \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"object": "list",
"data": [
{
"id": "sqm_i_cmV3bHk6c3FlX3ZrMW9scXBjMTUwMDpFTUFJTA",
"direction": "INBOUND",
"enrollmentId": "sqe_vk1olqpc1500",
"contactId": "con_cmuyyy6zv0007",
"sequenceStepId": "step-1",
"sequenceStepType": "EMAIL",
"channel": "EMAIL",
"createdAt": "2026-10-09T14:02:51.000Z",
"receivedAt": "2026-10-09T14:02:51.000Z",
"reference": { "objectType": "email", "id": "eml_cmuz1k2p40012" }
}
],
"totalCount": 1
}

List items carry no content. Retrieve one to read it: Retrieve a sequence message adds content with the subject and plain-text body, readable with sequences:read. The reference points at the underlying record in the Emails or Messages API, which keep their own scopes.

Performance totals the sequence’s lifetime engagement by step type and per step:

Terminal window
curl https://api.lightfield.app/v1/sequences/seq_cmuyyy75r0016/performance \
-H 'Lightfield-Version: 2026-03-01' \
-H "Authorization: Bearer $API_KEY"
{
"byStepType": {
"EMAIL": { "sent": 2, "delivered": 2, "opened": 1, "replied": 1, "bounced": 0, "openRate": 0.5, "replyRate": 0.5, "bounceRate": 0, "currentEnrollments": 1 },
"WAIT": { "currentEnrollments": 0 }
},
"perStep": [
{ "sequenceStepId": "step-1", "sequenceStepType": "EMAIL", "sent": 2, "delivered": 2, "opened": 1, "replied": 1, "bounced": 0, "openRate": 0.5, "replyRate": 0.5, "bounceRate": 0, "currentEnrollments": 1 },
{ "sequenceStepId": "step-tf9jgb7g", "sequenceStepType": "WAIT", "currentEnrollments": 0 },
{ "sequenceStepId": "step-aus2eesh", "sequenceStepType": "EMAIL", "sent": 0, "delivered": 0, "opened": 0, "replied": 0, "bounced": 0, "openRate": null, "replyRate": null, "bounceRate": null, "currentEnrollments": 0 }
]
}

currentEnrollments is the live view: how many contacts are on each step right now. The counts are lifetime and include steps that were later removed, as long as they sent something. Rates are null until there is something to divide by.

Discarding a draft needs its version, like any other draft write. On a published sequence it only closes the draft:

{ "sequenceId": "seq_cmuyyy75r0016", "sequenceDeleted": false }

On a sequence that has never been published, the draft is all there is, so discarding it deletes the sequence too, and sequenceDeleted is true. After that the sequence returns 404.

Every sequence write is all-or-nothing. A 400 means the request itself needs to change, and the body says where: issues for a draft, invalidContacts for an enrollment, or message and param otherwise. A 409 means the draft or the sequence moved since you read it; read it again and retry with what you see. Sequence errors has every code and what to do about it.