Documentation

DocsCore ConceptsWorkflows & Enrollments

Workflows & Enrollments

Build multi-step email sequences with conditional branching, delays, and event triggers. Powered by Temporal for durable execution.

Workflow Structure

A workflow is a directed graph of nodes. Each node performs an action and points to the next node.

WorkflowDefinition
{
  id: string,
  name: string,
  version: number,
  status: "active" | "draft" | "paused" | "failed" | "archived",
  trigger: "manual" | "api" | "event" | "scheduled",
  nodes: WorkflowNode[],
  exitCriteria?: ExitCriterion[],
  optOutMode?: "link" | "reply" | "header_only",  // default: "reply"
  schedule?: {                   // for "scheduled" trigger
    cronExpression: string,      // e.g. "0 9 * * 1" (every Monday 9am)
    contactFilter?: {
      segmentId?: string         // only enroll contacts in this segment
    }
  },
  pausedNodeIds?: string[],      // nodes currently paused via node-level pause
  createdAt: string,
  updatedAt: string
}

Node Types

WorkflowNode
{
  id: string,
  type: "delay" | "send_email" | "send_webhook" | "wait_for_event" | "branch" | "exit" | "goto" | "ab_test",
  label: string,
  nextNodeId: string | null,

  // delay nodes
  delayAmount?: number,          // duration amount
  delayUnit?: "minutes" | "hours" | "days",
  delayMinutes?: number,         // legacy, prefer delayAmount + delayUnit

  // send_email nodes
  templateId?: string,

  // send_webhook nodes
  webhookUrl?: string,           // POST target URL
  webhookBody?: string,          // Liquid-templated JSON body
  webhookAuthHeader?: string,    // e.g. "Bearer sk-xxx"
  webhookResponseBranches?: [{   // branch on webhook response
    condition: { field: string, operator: string, value?: string },
    nextNodeId: string
  }],

  // wait_for_event nodes
  waitForEvent?: string,

  // branch nodes
  branches?: [{
    condition: {
      field: string,             // e.g. "contact.email", "engagement.opened", "contact.attributes.company_size"
      operator: "equals" | "contains" | "exists" | "not_equals" | "is_true" | "is_false",
      value?: string
    },
    nextNodeId: string
  }],

  // goto nodes (loops)
  targetNodeId?: string,         // jump destination node ID
  maxIterations?: number,        // safety guard, default 10

  // ab_test nodes
  variants?: [{
    id: string,
    templateId: string,
    weight: number               // relative weight for random selection
  }]
}
  • delay — Pause for delayAmount + delayUnit before continuing (legacy delayMinutes also supported).
  • send_email — Send the template specified by templateId.
  • wait_for_event — Pause until the named event occurs (e.g., message.received).
  • branch — Evaluate conditions and route to different next nodes. Supports contact.attributes.* for custom data.
  • exit — End the workflow.
  • send_webhook — POST to webhookUrl with a Liquid-templated JSON body. Supports optional auth header and response-based branching.
  • goto — Jump to targetNodeId, creating loops. Capped by maxIterations (default 10) to prevent infinite loops. When exceeded, falls through to nextNodeId.
  • ab_test — Randomly select a variant by weight and send that template. The selected variantId is stored on the enrollment for analytics.

Exit Criteria

Define conditions that automatically remove contacts from a workflow. When an exit criterion is triggered, the enrollment status changes to "exited" with a reason.

ExitCriterion
{
  trigger: "reply" | "bounce" | "unsubscribe" | "manual" | "custom_event",
  eventName?: string,                  // required for custom_event
  replyClassifications?: string[]      // filter which reply types trigger exit (e.g. ["not_interested"])
}

Add exit criteria to the workflow definition:

WorkflowDefinition with exitCriteria
{
  name: "Outbound Drip",
  trigger: "api",
  nodes: [...],
  exitCriteria: [
    { trigger: "reply" },
    { trigger: "bounce" },
    { trigger: "unsubscribe" },
    { trigger: "custom_event", eventName: "deal.closed" }
  ]
}

Engagement-Based Branching

Branch nodes can evaluate engagement signals using the engagement scope with is_true / is_false operators:

Engagement Branch Example
{
  id: "n3", type: "branch", label: "Opened email?",
  branches: [
    {
      condition: { field: "engagement.opened", operator: "is_true" },
      nextNodeId: "n4_opened"
    }
  ],
  nextNodeId: "n4_not_opened"  // default path
}

Available engagement fields: engagement.opened, engagement.clicked, engagement.replied.

Workflow Lifecycle

Status Transitions
draft → active (publish) → paused (pause) → active (resume)
                          → failed (error)
TypeScript
// Create, publish, then pause
const wf = await client.createWorkflow({
  name: "Onboarding",
  trigger: "api",
  nodes: [
    { id: "n1", type: "send_email", label: "Welcome", templateId: "tmpl_1", nextNodeId: "n2" },
    { id: "n2", type: "delay", label: "Wait 2 days", delayAmount: 2, delayUnit: "days", nextNodeId: "n3" },
    { id: "n3", type: "exit", label: "Done", nextNodeId: null }
  ]
});
await client.publishWorkflow(wf.id);  // draft → active
await client.pauseWorkflow(wf.id);    // active → paused
await client.resumeWorkflow(wf.id);   // paused → active
curl
# Publish a workflow
curl -X POST https://api.outreachagent.dev/v1/workflows/wf_abc/publish \
  -H "Authorization: Bearer $OUTREACHAGENT_API_KEY"

# Pause a workflow
curl -X POST https://api.outreachagent.dev/v1/workflows/wf_abc/pause \
  -H "Authorization: Bearer $OUTREACHAGENT_API_KEY"

Enrollments

Enrolling a contact in a workflow starts execution. Track progress via the enrollment status and execution logs.

Enrollment
{
  id: string,
  workflowId: string,
  contactId: string,
  status: "active" | "completed" | "paused" | "failed" | "exited",
  exitReason: string | null,
  currentNodeId: string | null,
  variantId: string | null,      // set by ab_test nodes
  variables: Record<string, unknown>,  // per-enrollment template overrides ({{ vars.<key> }})
  createdAt: string
}
ExecutionLog
{
  id: string,
  enrollmentId: string,
  nodeId: string,
  status: "scheduled" | "running" | "completed" | "failed" | "skipped",
  message: string,
  createdAt: string
}
TypeScript
// Enroll a contact with per-enrollment variables
const enrollment = await client.createEnrollment({
  workflowId: "wf_abc",
  contactId: "con_xyz",
  variables: { hook: "your recent product launch", offer: "15% discount" }
});

// Check execution logs
const logs = await client.listEnrollmentLogs(enrollment.id);
curl
# Enroll a contact with variables
curl -X POST https://api.outreachagent.dev/v1/enrollments \
  -H "Authorization: Bearer $OUTREACHAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflowId": "wf_abc", "contactId": "con_xyz", "variables": {"hook": "your recent product launch"}}'

# Get execution logs
curl https://api.outreachagent.dev/v1/enrollments/enr_123/logs \
  -H "Authorization: Bearer $OUTREACHAGENT_API_KEY"