Get an enrollment + its step-run history
https://myagentmail.com/enrollments/{id}Returns the enrollment row plus its stepRuns[] history (last 50,
newest-first). Use this when you need per-step status — completed,
skipped, failed — to render a step-by-step ✅ timeline.
Step run statuses
sent— step fired and LinkedIn / SMTP accepted the action. Render as a green checkmark.skipped— step was evaluated but itsconditiondidn't apply (e.g.after_acceptstep on an unaccepted invitation, ornever_repliedstep when the lead replied). Normal cadence flow — render as a grey checkmark or skip-icon, NOT as an error.failed— runner tried and got backok:falsefrom the downstream call.errorCodeis set. The enrollment is deferred 1h and retried on the next tick — this is NOT a terminal state on its own.queued— internal scheduling state. You'll rarely see this in practice; included for completeness.
Error codes you'll see in errorCode
Set by api/src/lib/cadences/runner.ts. The catalogue:
NO_SESSION_AVAILABLE— workspace has zero connected LinkedIn sessions. Customer needs to connect one at /dashboard/linkedin.WORKSPACE_QUOTA_EXHAUSTED— sessions exist but all at the per-action daily cap (e.g. 30 connection_requests/day per session). Resets at midnight UTC, or add more sessions.PINNED_SESSION_UNUSABLE—enrollment.sessionIdwas set manually but that session is revoked / not-active / capped. Customer should reconnect or repin.NO_TARGET—leadLinkedinUrl(for LinkedIn steps) orleadEmail(for email steps) was missing at enrollment.NO_INBOX— email step but noinboxIdon the enrollment or cadence.DRAFT_WEBHOOK_FAILED— step hasdraftStrategy: "webhook"and the customer's draft handler timed out (>30s), 5xx'd, returned non-JSON, returned an invalid defer, or returned nobody.errorMessagecarries the upstream detail (which status code, "no body in response", etc). The step defers 1h and retries — which means a flaky handler will keep getting opportunities to succeed. To stop retries, mark the enrollmentstate: "paused"or unenroll.AI_STRATEGY_REMOVED— pre-existing step still ondraftStrategy: "ai"(removed 2026-05-19). Update the step tostatic(withstaticBody) orwebhook(with the cadence'swebhookUrlset to your draft handler).send_failed— downstream LinkedIn / SMTP returned an error.errorMessagecarries the upstream detail.
Worked example response
{
"ok": true,
"enrollment": {
"id": "01HZ…",
"cadenceId": "01HY…",
"leadExternalId": "delegated:lead:abc-123",
"leadName": "Jane Doe",
"leadLinkedinUrl": "https://www.linkedin.com/in/jane-doe-1a2b3c/",
"leadProfileUrn": "urn:li:fsd_profile:ACoAAA…aaaa",
"sessionId": "01HX…",
"currentStep": 3,
"state": "active",
"nextStepDueAt": "2026-05-21T14:00:00Z",
"lastStepFiredAt": "2026-05-19T14:00:00Z",
"enrolledAt": "2026-05-18T10:30:00Z",
"updatedAt": "2026-05-19T14:00:00Z"
},
"stepRuns": [
{
"id": "01HZ…",
"stepIndex": 2,
"stepKind": "linkedin_message",
"status": "sent",
"channelUrn": "urn:li:msg_message:(…)",
"renderedSubject": null,
"renderedText": "Hey Jane — saw your post about cold outbound…",
"errorMessage": null,
"errorCode": null,
"skipReason": null,
"firedAt": "2026-05-19T14:00:00Z"
},
{
"id": "01HZ…",
"stepIndex": 1,
"stepKind": "wait",
"status": "sent",
"channelUrn": null,
"renderedSubject": null,
"renderedText": null,
"errorMessage": null,
"errorCode": null,
"skipReason": null,
"firedAt": "2026-05-19T13:00:00Z"
},
{
"id": "01HZ…",
"stepIndex": 0,
"stepKind": "linkedin_connect",
"status": "sent",
"channelUrn": "urn:li:fsd_profile:ACoAAA…aaaa",
"renderedSubject": null,
"renderedText": "Hi Jane — common interest in outbound; thought I'd connect.",
"errorMessage": null,
"errorCode": null,
"skipReason": null,
"firedAt": "2026-05-18T10:30:00Z"
}
]
}
Rendering ✅ progress
const r = await mam.getEnrollment({ id });
const sentByIndex = new Map<number, true>();
const skippedByIndex = new Map<number, true>();
let lastFailure: typeof r.stepRuns[number] | null = null;
for (const run of r.stepRuns) {
if (run.status === "sent") sentByIndex.set(run.stepIndex, true);
else if (run.status === "skipped") skippedByIndex.set(run.stepIndex, true);
else if (run.status === "failed" && !lastFailure) lastFailure = run;
}
// For each step in cadence.steps[]:
// if (sentByIndex.has(i)) → ✅ done
// else if (skippedByIndex.has(i)) → ⊘ skipped (condition not met)
// else if (i === enrollment.currentStep && lastFailure) → ⚠ blocked
// else if (i === enrollment.currentStep) → ⏳ pending
// else → ◯ upcoming
Path parameters
idrequired string |
Responses
okboolean | |
enrollmentobject | Same shape as items in `/cadences/{id}/enrollments` (minus the `stepsCompleted` / `lastError` summary — drill into `stepRuns[]` below instead). |
runnerStateobject | Read-only diagnostic — answers "would the runner fire this row right now? if not, why?". Same logic the runner uses to pick enrollments (`pickDueEnrollments` + `evaluateCondition`), replicated read-only so callers can self-diagnose without poking the runner. Use this BEFORE filing a support ticket. If `pickable: true` and the row isn't firing, the runner tick interval is 5 minutes — `maxExpectedWaitSeconds` carries the upper bound. Use `POST /v1/cadences/{id}/run-now` to force a tick. If `pickable: false`, `reasons[]` lists every blocking condition with the exact PATCH curl needed to unblock. `details` exposes the underlying values (state, scheduling, condition evaluation) for deeper inspection. |
pickableboolean | True iff the runner would pick this enrollment on its next tick. |
reasonsstring[] | Empty when pickable=true. Each item is a human-readable explanation of a blocking condition, including the corrective curl when applicable. |
detailsobject | Underlying decision inputs — state, scheduling, condition evaluation, draft strategy sanity. |
statestring | enrollment.state |
cadenceEnabledboolean | |
nextStepDueAtstring | format: date-time |
nextStepDueInSecondsinteger | Negative = overdue, positive = future. |
currentStepinteger | |
stepCountinteger | |
currentStepKindstring | |
currentStepConditionstring | |
currentStepDraftStrategystring | |
conditionOutcome"proceed" | "skip-step" | "wait" | enum: "proceed" | "skip-step" | "wait" |
conditionReasonstring | |
runnerTickIntervalSecondsinteger | How often the runner ticks (currently 300s/5min). |
maxExpectedWaitSecondsinteger | Upper bound on time-to-fire when pickable=true. Null when blocked (no deterministic ETA). |
stepRunsobject[] | Last 50 step runs for this enrollment, newest-first. |
idstring | format: uuid |
stepIndexinteger | 0-based; matches cadence.steps[stepIndex]. |
stepKind"linkedin_connect" | "linkedin_message" | "email" | "wait" | enum: "linkedin_connect" | "linkedin_message" | "email" | "wait" |
status"sent" | "skipped" | "failed" | "queued" | enum: "sent" | "skipped" | "failed" | "queued" |
channelUrnstring | For LinkedIn steps: invitation URN / message URN / profile URN. For email: the message-id. |
renderedSubjectstring | Email subject (email steps only). |
renderedTextstring | The body that was actually sent — AI-generated or static, post-rendering. |
errorMessagestring | |
errorCodestring | See description above for the catalogue. |
skipReasonstring | Why a `skipped`-status run was skipped (e.g. condition_not_met). |
firedAtstring | format: date-time |
Authentication
Send your API key in the X-API-Key header (or Authorization: Bearer <key>). Any prefix accepted by this endpoint — tk_, wk_, ak_, or sa_ — is documented in the key prefix table.