Outreach Sequences
Overview
Outreach Sequences is the multi-channel sales engagement engine in CRED Commercial. It replaces the legacy MessageSequence / MessageSequenceStep system (email-only, single-variant) with a full-featured orchestration platform supporting Email, LinkedIn, Phone, and Action steps, A/B testing via template variants, configurable sending schedules, automated rulesets, and granular per-channel metrics.
Migration from Legacy System
The previous system lived under src/domain/message-sequence/ and only supported email steps with simple sequential scheduling. The new system under src/domain/sequence/ is a ground-up redesign that retains the core concept of ordered outreach steps while adding multi-channel support, variant-based A/B testing, a dedicated execution engine with channel handlers, and comprehensive metrics tracking.
Core Concepts
Entity Relationship Overview
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Outreach Sequences β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β SequenceTemplate β
β βββ 1:N SequenceTemplateVariant (A/B test branches) β
β β βββ 1:N SequenceStep (ordered steps per variant) β
β βββ 1:N SequenceEnrollment (contacts enrolled in the sequence) β
β β βββ 1:N SequenceStepExecution (one per step per enrollment) β
β βββ N:M SequenceRuleset (via SequenceTemplateRuleset) β
β βββ N:1 SendingSchedule (optional linked schedule) β
β βββ 1:N SequenceMetrics (aggregated performance data) β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Entities
SequenceTemplate
The top-level container that defines a reusable outreach sequence.
| Field | Description |
|---|---|
name |
Display name for the sequence |
description |
Optional long-form description |
isActive |
Whether the sequence accepts new enrollments and processes steps |
defaultTimezone |
Fallback timezone for scheduling |
defaultSendingScheduleId |
FK to the SendingSchedule used by default |
enableAbTesting |
Enables variant-level A/B testing |
pauseOnReply |
Auto-pause enrollment when recipient replies (default: true) |
pauseOnMeetingBooked |
Auto-pause when a meeting is booked (default: true) |
pauseOnOutOfOffice |
Auto-pause when OOO is detected (default: true) |
maxEnrollmentsPerDay |
Throttle for daily enrollment volume |
enableMailboxRotation |
Distribute sends across multiple mailboxes |
scheduledStartAt / startTimezone |
Delayed start for the entire sequence |
sendTimeStrategy |
IMMEDIATELY, SENDER_TIMEZONE, or RECIPIENT_TIMEZONE |
tags |
Freeform tags for organization |
totalEnrollments / activeEnrollments / completedEnrollments |
Denormalized counters |
SequenceTemplateVariant
An A/B testing branch within a template. Each variant holds its own set of steps and tracks independent performance counters.
| Field | Description |
|---|---|
sequenceTemplateId |
Parent template FK |
name |
Variant label (e.g. "Control", "Shorter cadence") |
weight |
Distribution weight (0-100, default 50) for enrollment assignment |
isControl |
Marks the control group in an A/B test |
isActive |
Whether this variant participates in enrollment distribution |
enrollmentCount / sentCount / openedCount / clickedCount / repliedCount / bouncedCount / optedOutCount |
Denormalized performance counters |
Computed rates (openRate, replyRate, clickRate, bounceRate) are calculated on-read from the counters.
SequenceStep
A single action in a variant's cadence. Steps are ordered by stepOrder and can target any supported channel.
| Field | Description |
|---|---|
sequenceTemplateVariantId |
Parent variant FK |
stepOrder |
1-based execution order |
channelType |
EMAIL, LINKEDIN, PHONE, or ACTION |
name |
Step label |
delayInHours |
Hours to wait before executing (from the previous step or from delayFromStepId) |
delayFromStepId |
Optional FK to a specific step to measure delay from |
| Email fields | emailSubject, emailBody, emailBodyHtml |
| LinkedIn fields | linkedinMessage, linkedinConnectionRequest |
| Phone fields | phoneScript, phoneType (CALL or SMS) |
| Action fields | actionDescription, actionType (TASK, REMINDER, CALENDAR) |
dynamicFields |
JSON object for merge fields / personalization tokens |
messageGeneratorId / llmPromptId / llmSettingsId |
Optional LLM-based content generation |
attachmentFileIds |
File attachments (email steps) |
conditions |
Optional conditional logic (JSON) |
trackOpens / trackClicks |
Email tracking toggles (default: true) |
isApproved / approvedAt / approvedByUserId |
Manual approval gate |
SequenceEnrollment
Represents a single contact's journey through a sequence.
| Field | Description |
|---|---|
sequenceTemplateId |
Template FK |
sequenceTemplateVariantId |
Variant FK (assigned based on weight distribution) |
contactId / personId |
The enrolled contact or person |
recipientEmail |
Resolved email for this enrollment |
status |
Current lifecycle state (see state machine below) |
pauseReason |
Why the enrollment was paused |
currentStepId / currentStepOrder |
Pointer to the active step |
enrolledAt / enrolledBy |
Enrollment origin |
startedAt / pausedAt / completedAt / failedAt / optedOutAt |
Lifecycle timestamps |
nextScheduledAt |
When the next step is due |
enrollmentSource |
MANUAL, API, TRIGGER, CAMPAIGN, or IMPORT |
sendingScheduleId / mailboxId |
Per-enrollment overrides |
timezone |
Per-enrollment timezone |
stepsCompleted / stepsFailed / emailsOpened / emailsClicked / emailsReplied / emailsBounced |
Per-enrollment metrics |
SequenceStepExecution
A record of a single step being executed for a single enrollment. One execution per step per enrollment.
| Field | Description |
|---|---|
sequenceEnrollmentId |
Enrollment FK |
sequenceStepId |
Step FK |
stepOrder |
Snapshot of the step's order at execution time |
status |
Execution status (see statuses below) |
channelType |
Channel type for this execution |
scheduledAt / executedAt / completedAt |
Timing |
| Email tracking | emailId, emailOpened, emailOpenedAt, emailClicked, emailClickedAt, emailReplied, emailRepliedAt, emailBounced, emailBouncedAt, bounceReason |
| LinkedIn tracking | linkedinMessageId, linkedinConnectionRequestId, linkedinSent, linkedinSentAt |
| Phone tracking | phoneCallId, phoneCallDurationSeconds, phoneCallStatus (COMPLETED, NO_ANSWER, BUSY, FAILED) |
| Action tracking | taskId, actionCompleted, actionCompletedAt |
retryCount / lastRetryAt |
Retry tracking |
contentSnapshot |
Frozen copy of the content that was sent |
SendingSchedule
Defines when outreach can be sent. Configurable per day of week with start/end times.
| Field | Description |
|---|---|
name / description |
Schedule label |
timezone |
Base timezone for the schedule |
{day}Enabled |
Boolean toggle per day (Mon-Sun). Weekdays default true, weekends default false |
{day}StartTime / {day}EndTime |
HH:MM send window per day (default 09:00-17:00) |
holidays |
Array of dates to skip |
skipHolidays |
Whether to honor the holidays list (default: true) |
minDelayBetweenSendsMinutes |
Throttle between consecutive sends (default: 60) |
useContactTimezone |
Use the contact's timezone instead of the schedule timezone |
SequenceRuleset
Reusable automation rules that can be attached to templates via the SequenceTemplateRuleset junction table.
| Field | Description |
|---|---|
name / description |
Rule label |
ruleType |
ENROLLMENT_TRIGGER, STEP_CONDITION, PAUSE_TRIGGER, RESUME_TRIGGER, or EXIT_TRIGGER |
triggerType |
For enrollment triggers: CONTACT_CREATED, CONTACT_UPDATED, FIELD_CHANGED, TAG_ADDED, LIST_ADDED, OPPORTUNITY_STAGE_CHANGED, CUSTOM_EVENT, API, MANUAL |
config |
JSON object containing rule-specific parameters |
priority |
Evaluation order when multiple rules of the same type exist |
isActive |
Toggle |
isReusable |
Whether the rule can be shared across templates |
SequenceMetrics
Aggregated daily performance data, queryable at the template, variant, or step level, and filterable by channel.
Tracks:
- Enrollment metrics: created, active, paused, completed, failed, opted-out
- Step metrics: sent, failed, skipped
- Email metrics: sent, opened, clicked, replied, bounced (total and unique)
- LinkedIn metrics: messages sent, connections sent, connections accepted
- Phone metrics: calls, completed calls, total duration, SMS sent
- Action metrics: created, completed
- Calculated rates: open, click, reply, bounce, opt-out
- Business metrics: opportunities created, meetings booked, revenue generated
Channel Types
| Channel | Enum Value | Description |
|---|---|---|
EMAIL |
Sends emails via Nylas-connected mailboxes. Supports tracking, threading, attachments, and LLM-generated content. | |
LINKEDIN |
Sends LinkedIn messages or connection requests via Unipile integration. | |
| Phone | PHONE |
Logs phone calls (CALL) or sends SMS (SMS). Tracks call duration and outcome. |
| Action | ACTION |
Creates internal tasks (TASK), reminders (REMINDER), or calendar entries (CALENDAR) for the sales rep. |
Enrollment Lifecycle
βββββββββββ
β PENDING β
ββββββ¬βββββ
β enrollment activated
βΌ
βββββββββββ
βββββ β ACTIVE β βββββββββββββββββββββ
β ββββββ¬βββββ β β
β β β β
pause β β all β recipient β failure
(manual, β steps β opts out β
reply, β done β β
OOO, βΌ βΌ βΌ
bounce, ββββββββββββ ββββββββββββ βββββββββββ
rate βCOMPLETED β β OPTED_OUTβ β FAILED β
limit) ββββββββββββ ββββββββββββ βββββββββββ
β
βΌ
βββββββββββ
β PAUSED β
ββββββ¬βββββ
β resume
β
ββββββββΊ ACTIVE
Pause reasons: MANUAL, REPLIED, OUT_OF_OFFICE, MEETING_BOOKED, BOUNCED, RATE_LIMIT
Enrollment sources: MANUAL, API, TRIGGER, CAMPAIGN, IMPORT
Step Execution Flow
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Execution Engine Flow β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Background job picks up SCHEDULED step executions β
β β β
β βΌ β
β 2. Validate enrollment is ACTIVE β
β β β
β βΌ β
β 3. Check step approval gate β
β βββ Not approved β mark SKIPPED, reschedule next step β
β βββ Approved β continue β
β β β
β βΌ β
β 4. Transition status: SCHEDULED β SENDING β
β β β
β βΌ β
β 5. Route to channel handler β
β βββ EmailChannelHandler (Nylas, mailbox rotation, tracking) β
β βββ LinkedInChannelHandler (Unipile, connection requests) β
β βββ PhoneChannelHandler (call logging, SMS) β
β βββ ActionChannelHandler (task/reminder/calendar creation) β
β β β
β βββββββ΄ββββββ β
β βΌ βΌ β
β Success Failure β
β SENT FAILED β
β β β β
β βΌ βΌ β
β 6. Update enrollment metrics β
β 7. Reschedule next step (respect delayInHours from completion time) β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Step execution statuses: PENDING β SCHEDULED β SENDING β SENT | FAILED | SKIPPED | CANCELLED
Delay Handling
When a step completes (sent or skipped), the engine recalculates the next step's scheduledAt to ensure the configured delayInHours is measured from the actual completion time, not the original schedule. This prevents back-to-back execution after outages or delays.
Approval Gate
Steps with isApproved = false are automatically skipped during execution. The engine marks them as SKIPPED with metadata { skipReason: "STEP_NOT_APPROVED" } and reschedules the following step accordingly.
A/B Testing
When enableAbTesting is turned on for a template:
- Multiple variants are created, each with its own set of steps and a
weight(0-100). - On enrollment, the system assigns the contact to a variant based on weight distribution.
- Each variant independently tracks
enrollmentCount,sentCount,openedCount,clickedCount,repliedCount,bouncedCount, andoptedOutCount. - Computed rates (
openRate,replyRate,clickRate,bounceRate) are derived on-read for real-time comparison. - One variant can be marked
isControlto serve as the baseline.
Sending Schedules
Sending schedules control when outreach is delivered.
- Per-day windows: Each day of the week has an independent enable/disable toggle and start/end time (defaults: Mon-Fri 09:00-17:00, Sat-Sun disabled).
- Timezone awareness: The schedule operates in a configured timezone. Can optionally use the contact's timezone (
useContactTimezone). - Holiday handling: An array of dates can be configured as holidays. When
skipHolidaysistrue(default), sends are deferred past holidays. - Send throttling:
minDelayBetweenSendsMinutes(default: 60) enforces a minimum gap between consecutive sends.
The SendTimeStrategy on the template determines how the schedule is applied:
| Strategy | Behavior |
|---|---|
IMMEDIATELY |
Send as soon as the delay elapses (no schedule gating) |
SENDER_TIMEZONE |
Gate sends to the schedule window in the sender's timezone |
RECIPIENT_TIMEZONE |
Gate sends to the schedule window in the recipient's timezone |
Rulesets & Automation
Rulesets define automated behaviors that can be attached to any template.
| Rule Type | Purpose |
|---|---|
ENROLLMENT_TRIGGER |
Automatically enroll contacts when a condition is met (e.g. contact created, tag added, opportunity stage changed) |
STEP_CONDITION |
Conditional logic to skip or modify a step based on runtime data |
PAUSE_TRIGGER |
Auto-pause enrollment on events (reply, bounce, OOO detection) |
RESUME_TRIGGER |
Auto-resume paused enrollments |
EXIT_TRIGGER |
Remove contact from sequence on events |
Rules are reusable across templates via the SequenceTemplateRuleset junction table and are evaluated in priority order.
Enrollment Trigger Types
| Trigger | Fires When |
|---|---|
CONTACT_CREATED |
A new contact is created |
CONTACT_UPDATED |
A contact is updated |
FIELD_CHANGED |
A specific field value changes |
TAG_ADDED |
A tag is applied to a contact |
LIST_ADDED |
A contact is added to a list |
OPPORTUNITY_STAGE_CHANGED |
A deal moves to a target stage |
CUSTOM_EVENT |
A custom event fires |
API |
Triggered via the API |
MANUAL |
Manually enrolled by a user |
Out-of-Office Detection
The system includes built-in OOO detection with multiple methods:
| Method | Description |
|---|---|
HEADER_AUTO_REPLY |
Checks email headers for auto-reply indicators |
SUBJECT_KEYWORD |
Matches OOO-related keywords in the subject line |
CONTENT_PATTERN |
Pattern matching on email body content |
ML_CLASSIFIER |
ML-based classification of response content |
When OOO is detected and pauseOnOutOfOffice is enabled on the template, the enrollment is automatically paused with reason OUT_OF_OFFICE.
GraphQL API Surface
Queries
| Query | Description |
|---|---|
sequenceTemplate(id) |
Get a single template with variants, steps, and metrics |
sequenceTemplates(filters) |
List templates with filtering and pagination |
sequenceEnrollment(id) |
Get enrollment details with step executions |
sequenceEnrollments(filters) |
List enrollments for a template |
sequenceMetrics(templateId) |
Get aggregated metrics for a template |
sendingSchedule(id) |
Get a sending schedule |
sendingSchedules |
List all sending schedules |
Mutations
Template management:
createSequenceTemplate/updateSequenceTemplateactivateSequenceTemplate/deactivateSequenceTemplatedeleteSequenceTemplate
Variant management:
createTemplateVariant/updateTemplateVariant/deleteTemplateVariant
Step management:
createSequenceStep/createBulkSequenceStepsupdateSequenceStep/deleteSequenceStepreorderSequenceStepsapproveSequenceStep
Enrollment management:
enrollContactInSequence/enrollContactsInSequence/bulkEnrollContactspauseEnrollment/resumeEnrollmentcompleteEnrollment/optOutEnrollmentunenrollContact
Scheduling:
createSendingSchedule/updateSendingSchedule/deleteSendingSchedule
Key Differences from Legacy System
| Aspect | Legacy (MessageSequence) |
New (Sequence) |
|---|---|---|
| Channels | Email only | Email, LinkedIn, Phone, Action |
| A/B Testing | Not supported | Variant-based with weight distribution |
| Enrollment tracking | Implicit (sequence = one contact) | Explicit SequenceEnrollment entity with lifecycle |
| Execution model | Direct send via service | Execution engine with pluggable channel handlers |
| Step execution records | Status on the step itself | Dedicated SequenceStepExecution per enrollment |
| Metrics | Basic step status counts | Dedicated SequenceMetrics entity with daily aggregation per channel |
| Scheduling | Simple delay (hours/days between messages) | SendingSchedule with per-day windows, timezone, holidays |
| Automation | Manual enrollment only | Rulesets with trigger-based auto-enrollment and pause/resume |
| OOO detection | Not supported | Built-in multi-method detection |
| Mailbox rotation | Not supported | Configurable rotation across connected mailboxes |
| Approval workflow | Per-sequence flag | Per-step approval gate with approval tracking |
Codebase Reference
| Layer | Path |
|---|---|
| Entities & Enums | src/domain/sequence/entity/ |
| Repository interfaces | src/domain/sequence/repository/ |
| Repository implementations | src/data/models/sequence/ |
| Use cases β Templates | src/domain/sequence/usecases/template/ |
| Use cases β Variants | src/domain/sequence/usecases/variant/ |
| Use cases β Steps | src/domain/sequence/usecases/step/ |
| Use cases β Enrollments | src/domain/sequence/usecases/enrollment/ |
| Use cases β Scheduling | src/domain/sequence/usecases/schedule/ |
| Use cases β Metrics | src/domain/sequence/usecases/metrics/ |
| Execution engine | src/domain/sequence/engine/ |
| Channel handlers | src/domain/sequence/engine/channel-handlers/ |
| Email services | src/domain/sequence/email/ |
| Scheduling services | src/domain/sequence/scheduling/ |
| OOO detection | src/domain/sequence/ooo/ |
| Metrics aggregation | src/domain/sequence/metrics/ |
| Event handlers | src/domain/sequence-events/ |
| GraphQL resolvers | src/graphql-api/sequence/resolvers/ |
| GraphQL types | src/graphql-api/sequence/types/ |
| GraphQL inputs | src/graphql-api/sequence/resolvers/inputs/ |
| Background jobs | src/worker/queue/sequences-task-queue.ts |
| Migrations | src/data/migrations/20260126* through 20260217* |
Troubleshooting
| Issue | Check | Resolution |
|---|---|---|
| Steps not executing | Enrollment status, template isActive, step isApproved |
Ensure enrollment is ACTIVE, template is active, and step is approved |
| Emails not sending | Nylas account connection, mailbox status | Verify Nylas account is connected and mailbox is ACTIVE (not WARMING_UP or DISABLED) |
| Steps executing back-to-back | delayInHours config, sending schedule |
Engine reschedules from actual completion time; verify schedule windows |
| Enrollment stuck in PAUSED | pauseReason field |
Check reason; resume manually or configure RESUME_TRIGGER ruleset |
| Metrics not updating | Background job status, metrics aggregation | Trigger recalculateSequenceMetrics or check record-daily-metrics job |
| A/B test not distributing | Variant weight and isActive |
Ensure at least two active variants with non-zero weights |
| LinkedIn steps failing | Unipile account, connection status | Verify Unipile account is connected and active |