Cadences

Get an enrollment + its step-run history

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 its condition didn't apply (e.g. after_accept step on an unaccepted invitation, or never_replied step when the lead replied). Normal cadence flow — render as a grey checkmark or skip-icon, NOT as an error.
  • failed — runner tried and got back ok:false from the downstream call. errorCode is 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.sessionId was set manually but that session is revoked / not-active / capped. Customer should reconnect or repin.
  • NO_TARGET — leadLinkedinUrl (for LinkedIn steps) or leadEmail (for email steps) was missing at enrollment.
  • NO_INBOX — email step but no inboxId on the enrollment or cadence.
  • DRAFT_WEBHOOK_FAILED — step has draftStrategy: "webhook" and the customer's draft handler timed out (>30s), 5xx'd, returned non-JSON, returned an invalid defer, or returned no body. errorMessage carries 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 enrollment state: "paused" or unenroll.
  • AI_STRATEGY_REMOVED — pre-existing step still on draftStrategy: "ai" (removed 2026-05-19). Update the step to static (with staticBody) or webhook (with the cadence's webhookUrl set to your draft handler).
  • send_failed — downstream LinkedIn / SMTP returned an error. errorMessage carries 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
get/enrollments/{id}

Request

  • Base URL: https://anjal.app
  • URL: https://anjal.app/enrollments/{id}
  • Auth: API key in header X-API-Key

Path parameters

idstring uuid required

Response

Enrollment + step runs.

okboolean
enrollmentobject

Same shape as items in /cadences/{id}/enrollments (minus the stepsCompleted / lastError summary — drill into stepRuns[] below instead).

Changes

No recorded changes to this operation across all 1 revision of this API.