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
| Field | Required | Meaning and constraints |
|---|---|---|
id | Yes | Required 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.
Related actions
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.