Sequences & angles
Multi-step LinkedIn campaigns as an ordered steps array, with messaging angles as the A/B-testing primitive.
A sequence is an ordered steps array - typically a connection request, an acceptance
condition, then a few spaced follow-ups. Create with
create_sequence, edit with
update_sequence, read (and get stats) with
get_sequence.
{
"messaging_angles": ["quick_question", "worth_a_look"],
"steps": [
{ "id": "view", "type": "view_profile" },
{ "id": "invite", "type": "send_connection_request",
"message": "Hi {{first_name}} - would love to connect.",
"variants": [
{ "id": "quick_question", "message": "Hi {{first_name}} - quick question." },
{ "id": "worth_a_look", "message": "Hi {{first_name}} - worth a look?" }
] },
{ "id": "c_accept", "type": "condition",
"check": "linkedin.accepted", "monitor": "7d",
"yes": "li_msg", "no": "fallback" },
{ "id": "li_msg", "type": "send_message",
"message": "Thanks for connecting.", "goto": "stop" }
]
}Step types
type | Notes |
|---|---|
view_profile | Sends a visible "viewed your profile" notification (that's the point) |
react_to_recent_post | Targets the most recent post; skips (and advances) if they have none |
send_connection_request | Note capped at 300 chars. Pre-checks the connection - an already-connected lead soft-succeeds and a following condition short-circuits down yes |
send_message | Capped at 8000 chars. If the enrollment already opened a chat, replies in that thread instead of starting a new one |
send_inmail | Body capped at 1900, subject 200. Requires a seat with InMail |
condition | {"check": "linkedin.accepted", "monitor": "7d", "yes": id, "no": id} - timeout takes the no branch |
Steps run top-to-bottom; delay pauses before a step ("10m"–"30d"), goto: "stop"
ends the flow early.
Template variables
{{first_name}}, {{last_name}}, {{full_name}}, {{headline}}, {{company_name}},
{{title}}, {{public_identifier}}, {{profile_url}}, plus {{lead.<field>}} and
{{custom.<key>}}. Unknown variables are rejected at create/update time - they would
otherwise be sent verbatim to the prospect.
Empty text is never sent. Missing values are checked at enroll time (per-person
missing_variables in failed[]), again at send time as a backstop (the entry cancels
instead of going out with a hole), and on every one-off send.
Angles & A/B testing
Angles are the copy-testing primitive. The contract, end to end:
-
Declare on the sequence.
messaging_angles: ["pain-point", "social-proof"], with an optionaldefault_anglethat must be in the list. This list is the single source of truth - every variant id and every enrollment angle is validated against it. -
Carry variants on steps. Any step whose copy should differ per angle gets
variantswhose ids match the declared angle names. One angle per lead, sequence-wide: an enrollment holds exactly oneselected_angle, and every step with a matching variant uses it - reuse the same ids across steps and a lead gets a coherent pitch on the opener and the follow-ups. Variants are message copy only (message,subject,reaction) - nodelayin variants. -
Assign at enrollment.
angleonadd_to_sequence/add_lead_group_to_sequence- one angle for the enrollee(s); an undeclared angle is avalidation_error.angle_splitonadd_lead_group_to_sequence-{"pain-point": 50, "social-proof": 50}splits the batch deterministically (smooth weighted round-robin over enrollment order - the same call lands the same split; 60/40 over 5 people = 3/2 exactly). Weights are proportional, need not sum to 100, and are mutually exclusive withangle. Skipped people don't consume split slots; the result reports per-personangleinenrolled[]plusangle_counts.- No angle at all - everyone gets
default_angle(or the first declared angle). A deterministic fallback, not a split: an angle-less bulk enroll is not an A/B test.
update_lead_messages{angle}re-assigns a live enrollment and re-bakes the queued unsent send. -
Read results per angle.
get_sequence include_stats=truereturnsby_angle:{ angle: { total, active, replied, exited, reply_rate_pct } }. Declared angles always appear (zeros = arm not enrolled yet).
Testing flow or length instead of copy (different delays, 5 vs 3 steps): variants
can't express that - create two sequences and split the group between them (the people
param on add_lead_group_to_sequence narrows to an intersection, the safe way to split
one group), then compare their include_stats.
Enroll-time copy
Copy resolution at send time: message_overrides.by_step → variant for the enrollment's
angle → step default message. Bulk re-enrolls skip people whose prior enrollment ended
in replied (previously_replied) - a reply is never followed by more cold touches from
a routine re-enroll.
Lifecycle & pacing
Sequences are born active - create_sequence never leaves a draft.
update_sequence status="paused" is a real pause: queued sends stop dispatching and wait
timers freeze, both resuming untouched on active.
remove_from_sequence is the permanent
per-person cancel; stop_sequence_and_send
stops and sends a manual message instead.
Enrolling is not an immediate send: the first touch waits out start_delay_minutes
(default 30), and outreach paces at roughly 20–30 sends per day per LinkedIn seat
(product-tier caps, ramp-up multipliers for new accounts, working-hours windows, and
per-action cooldowns all apply). Copy stays editable via update_lead_messages until
each send dispatches. A reply on any channel always stops the enrollment.
Multiple sequences sharing one seat split its caps fairly (least-recently-served
rotation), and each account hosts at most a handful of concurrent sequences - enrolling a
new sequence onto a full account is rejected with a validation_error naming the cap.