Topolo Docs Topolo Platform
Open workspace

Get enrollment

Read a retained enrollment’s pinned sequence version, progress and stop reason.

Outcome

Read a retained enrollment’s pinned sequence version, progress and stop reason.

Before you start

  • Verify the environment, credential, authorized organization and the published action permission workspace:read.
  • Discover an accessible Engage workspace and bind resourceType=workspace and its resourceId separately from action input. Fixture identifiers grant no access.

Discover the target

  • Select the authorized Engage workspace and discover enrollment id through enrollments.list or retain the id from an earlier approved enrollments.create. Read enrollments.get before acting on its status.
topolo whoami --json
topolo actions get app_topolo_engage.enrollments.get --json
topolo workspaces list topolo-engage --json

Input fields

FieldRequiredMeaning and constraints
idYesRequired enrollment identifier returned by enrollments.create or enrollments.list in the selected workspace, trimmed non-empty string up to 128 characters. This is not a campaign, contact or message id.

Validate, plan and execute

Set ENGAGE_WORKSPACE_ID to the identifier of your selected workspace. Prepare payload.json from the current input schema and discovered identifiers. Use the same payload for validation, planning and execution.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions validate app_topolo_engage.enrollments.get --data @payload.json --json
topolo actions plan app_topolo_engage.enrollments.get --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --json

Execute the unchanged validated read in the selected workspace.

: "${ENGAGE_WORKSPACE_ID:?Set ENGAGE_WORKSPACE_ID to your selected workspace identifier}"
topolo actions call app_topolo_engage.enrollments.get --resource-type workspace --resource-id "$ENGAGE_WORKSPACE_ID" --data @payload.json --json

For MCP, use topolo_get_action, topolo_validate_action, topolo_plan_action and topolo_call_action. Pass the selected resource context separately; confirm:true is required for mutations.

Worked requests and responses

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": 2,
    "nextSendAt": "2026-10-08T08:30:00.000Z",
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a WhatsApp template step > skips the step (not the enrollment) without a phone, without an opt-in, after a revoke and with an unresolved variable. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": 1,
    "nextSendAt": "2026-10-06T08:30:00.000Z",
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a task step > creates one task for the campaign owner, waits for it, and moves on when it is done. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": 2,
    "nextSendAt": "2026-10-10T08:30:00.000Z",
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runDueSends > renders, reserves, sends, consumes and schedules the threaded follow-up. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "replied",
    "stopReason": "reply",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a task step > replied stops the enrollment as a reply. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "replied",
    "stopReason": "not_interested",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Hello Ada",
    "outcome": "replied_negative",
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-13T08:00:00.000Z"
  }
}

Source test: the runner on a task step > not_interested stops the enrollment, records the negative outcome and a suppression ledger event. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "bounced",
    "stopReason": "hard_bounce",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: Notify webhook receiver > applies a hard bounce once: a redelivery of the same event id changes nothing. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": 2,
    "nextSendAt": "2026-10-09T08:30:00.000Z",
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a task step > creates one task for the campaign owner, waits for it, and moves on when it is done. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": 3,
    "nextSendAt": "2026-10-15T07:00:00.000Z",
    "firstMessageId": "email_stub_8ed9c21ca5faba2963419f3e20cccb29",
    "firstSubject": "Hello Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-13T07:00:00.000Z"
  }
}

Source test: the runner on a task step > creates one task for the campaign owner, waits for it, and moves on when it is done. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": null,
    "nextSendAt": "2026-10-27T07:00:00.000Z",
    "firstMessageId": "email_stub_8ed9c21ca5faba2963419f3e20cccb29",
    "firstSubject": "Hello Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-15T07:00:00.000Z"
  }
}

Source test: the runner on a task step > creates one task for the campaign owner, waits for it, and moves on when it is done. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": null,
    "nextSendAt": "2026-10-25T07:00:00.000Z",
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-13T07:00:00.000Z"
  }
}

Source test: runDueSends > renders, reserves, sends, consumes and schedules the threaded follow-up. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "completed",
    "stopReason": null,
    "nextStepPosition": null,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-25T07:00:00.000Z"
  }
}

Source test: runDueSends > renders, reserves, sends, consumes and schedules the threaded follow-up. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "manual_block",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a task step > stops a suppressed contact before any task exists, and ends an undeliverable one as bounced. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "active",
    "stopReason": null,
    "nextStepPosition": null,
    "nextSendAt": "2026-10-18T08:30:00.000Z",
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a WhatsApp template step > the last step skipped starts the closing period. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "replied",
    "stopReason": "reply",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: Notify webhook receiver > a reply stops the enrollment and feeds the engagement fact; delivery and soft events do not. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "bounced",
    "stopReason": "contact_point_undeliverable",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a task step > stops a suppressed contact before any task exists, and ends an undeliverable one as bounced. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "idempotency_key_reused",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runner with the Nexus transport contract > idempotency_key_reused fails the message and stops the enrollment. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "manual_block",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T09:00:00.000Z"
  }
}

Source test: runDueSends > hold order > then Engage eligibility: time-bound refusals hold, suppressions end the enrollment. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "unsubscribed",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-13T07:00:00.000Z"
  }
}

Source test: runDueSends > hold order > an unsubscribe ends the enrollment and counts as an opt-out of the previous message. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "completed",
    "stopReason": "max_touches_reached",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-13T07:00:00.000Z"
  }
}

Source test: message approval > maxTouches ends the enrollment before a later step. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "unresolved_merge_field:company",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runDueSends > skips a message with an unresolved merge field and never sends it empty. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "message_rejected",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: message approval > reject cancels the message, stops its enrollment and is audited. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "send_failed",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-08T08:30:00.000Z"
  }
}

Source test: the runner on a WhatsApp template step > counts transport errors and fails the message after three. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "manual_stop",
    "nextStepPosition": 1,
    "nextSendAt": "2026-10-06T08:30:00.000Z",
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: tasks.* actions > records the outcome without touching an enrollment that already ended. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "provider_suppressed",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runner with the Nexus transport contract > recipient_suppressed records a manual block (provider_suppressed) and stops the enrollment. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "completed",
    "stopReason": "max_touches_reached",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Hello Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: the runner on a WhatsApp template step > respects the emergency stop, a paused campaign, approval and the touch limit. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "send_failed",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-07T07:00:00.000Z"
  }
}

Source test: runDueSends > retries transport errors, then fails the message and stops the enrollment. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "campaign_closed",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T09:30:00.000Z"
  }
}

Source test: runDueSends > ending enrollments the runner cannot continue > holds while the campaign is paused and stops when it is completed. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "contact_unavailable",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runDueSends > ending enrollments the runner cannot continue > stops for an archived contact, a deleted seat, or a missing step. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "seat_missing",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runDueSends > ending enrollments the runner cannot continue > stops for an archived contact, a deleted seat, or a missing step. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "step_missing",
    "nextStepPosition": 1,
    "nextSendAt": null,
    "firstMessageId": null,
    "firstSubject": null,
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: runDueSends > ending enrollments the runner cannot continue > stops for an archived contact, a deleted seat, or a missing step. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "replied",
    "stopReason": "reply",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": "meeting_booked",
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T09:00:00.000Z"
  }
}

Source test: outcomes.record > classifies a reply, stops an active enrollment as replied, replaces a classification and is audited. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Captured enrollments.get · HTTP 200

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "enrollment": {
    "id": "eng_enr_1",
    "campaignId": "eng_cmp_1",
    "sequenceVersionId": "eng_seqv_1",
    "contactId": "contact_1",
    "seatId": "eng_seat_1",
    "status": "stopped",
    "stopReason": "complaint",
    "nextStepPosition": 2,
    "nextSendAt": null,
    "firstMessageId": "email_stub_2707f2dfbb8a394b00969d941f11089b",
    "firstSubject": "Quick question, Ada",
    "outcome": null,
    "enrolledAt": "2026-10-06T08:30:00.000Z",
    "updatedAt": "2026-10-06T08:30:00.000Z"
  }
}

Source test: transport events > a complaint records complaint and stops the enrollment; delivery after it never softens the outcome. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Effects and response meaning

  • Returns enrollment, including campaignId, contactId, seatId, pinned sequenceVersionId, status, stopReason, nextStepPosition, nextSendAt, first-message references and outcome. Organization/workspace scope columns are excluded from the response.
  • This is current state, not proof of external delivery or permission to resume stopped work. A pinned version may differ from the sequence’s latest published version.

Verify the result

  • Compare enrollment.id to the requested id and inspect status/stopReason. Read enrollments.messages.list for rendered messages and transport state; inspect the exact retained sequence version when reviewing copy.

Recover

Retry this read after a transient failure using the same authorized workspace and discovered identifier. Missing data does not authorize creation, re-enrollment or a different resource context.

Refused responses

Captured enrollments.get · HTTP 404

Action input

{
  "id": "eng_enr_1"
}

Actual local response

{
  "error": {
    "code": "not_found",
    "message": "Enrollment was not found in this workspace."
  }
}

Source test: enrollment routes > keeps enrollments inside the workspace and enforces permissions. Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

Error recovery

  • HTTP 403 permission_denied: Stop and inspect the permission required by this action in the current credential. Do not broaden permissions or switch organization automatically.
  • HTTP 422 invalid_action_input: Validate the current strict input schema. Creation requires campaignId, seatId and exactly one of contactIds or listId; do not provide both or neither.
  • HTTP 404 not_found: Rediscover the exact campaign, seat, list or enrollment in the same authorized workspace. Inspect the refusal message to identify which lookup failed; do not substitute another organization or guessed resource id.

Evidence and limits

  • contracts/sending.mjs

  • apps/api/src/enrollments/enrollments-handlers.ts

  • apps/api/src/enrollments/enrollments-repository.ts

  • apps/api/src/enrollments/enrollments-domain.ts

  • apps/api/src/enrollments/messages-repository.ts

  • apps/api/src/audience/lists-repository.ts

  • apps/api/src/tasks/tasks-repository.ts

  • apps/api/src/sending/outreach-eligibility.ts

  • Actual route handlers with SQLite repositories; synthetic identity/workspace grants and provider ports.

  • Widget/bootstrap use actual app handler but mocked worker runtime and app identity.

  • No gateway execution, live permissions, external delivery or deployed proof.

The examples are local route captures with synthetic context and simulated providers. They do not prove live authorization, CLI/MCP gateway execution, production state or delivery.