Skip to main content
A subsequence is a conditional branch. When a lead does something — replies with a keyword, gets tagged as interested, bounces — the branch pulls them off the main follow-up chain and onto its own sequence of steps.
Subsequences are the complement of sequences. Sequences are the MAIN chain every lead walks; subsequences are branches that interrupt it. They are managed by different endpoints, use different trigger vocabularies, and measure waits differently.

subsequences_enabled

Every campaign carries a subsequences_enabled flag, returned by GET /campaigns/{campaign_id} and GET /campaigns/{campaign_id}/flow. It is derived, not a setting — you never write it directly:
The flag gates branches only. It has no effect on the MAIN follow-up chain: a campaign with subsequences_enabled: false runs its sequences exactly as before, and turning it on does not move existing follow-ups onto the graph.
Update, delete and manual-move all require the flag to be true. If it is not, they return 400:
Creating a subsequence does not require it — that is how the flag gets turned on in the first place.

Reading the Graph

GET /api/public/v1/campaigns/{campaign_id}/flow — scope campaigns:read

Node and edge vocabulary

An EVENT edge enters a branch; a TIMEOUT edge is the wait between two branch steps.
variants is populated for SUBSEQUENCE nodes only. A MAIN node returns variants: null because its content is served by the sequences endpoints — returning it in both places would give you two sources of truth for the same step.
This endpoint exposes node and edge IDs that the public API gives you no way to edit directly. That is deliberate: you need entry_from_node_id and target_subsequence_node_id from here, and everything else is managed through the subsequence endpoints below. There is no /flow/nodes or /flow/edges route on the public surface.

Creating a Branch

POST /api/public/v1/campaigns/{campaign_id}/flow/subsequences — scope campaigns:update, returns 201

subsequence_id is the entry edge

Every endpoint below that takes a subsequence_id wants the entry edge’s ID — entry_edge.id in the create response, or the EVENT edge in GET /flow. It is not a node ID.Manual moves are the exception: target_subsequence_node_id wants the branch’s first node ID, which is entry_edge.to_node_id.

Entry triggers

Valid status values are the lead statuses: PENDING, QUEUED, SENT, OPENED, REPLIED, FAILED, BOUNCED, UNSUBSCRIBED, EXTERNAL, BLOCKED. INTENT_TAG also still honours the legacy single-value shape {"tag_name": "Demo Booked"}, and KEYWORD coerces a bare string into a one-element list.
These three are the only valid branch entry triggers. NOT_OPENED, NOT_REPLIED and OPENED_BUT_NOT_REPLIED belong to the MAIN chain and are rejected here with a 422. The two vocabularies do not overlap in either direction.
An entry trigger with missing or empty parameters is accepted by the API and then never fires. The branch is created, subsequences_enabled flips on, and nothing ever enters it — the worker only logs a warning. The same is true of a STATUS trigger naming a value that is not a lead status. Always read the branch back with GET /flow and confirm condition_params is populated.

Steps

Branch steps wait in seconds, not days. campaigns.update sequences use wait_days; this endpoint uses wait_seconds. Three days is 259200, not 3.
At least one step is required — an empty steps array is a 422 on create and a 400 on update.

condition_type between steps

Leave it at the default ALWAYS.
Every other condition is negative — NOT_REPLIED, NOT_OPENED, OPENED_BUT_NOT_REPLIED. A branch is entered because something happened, usually a reply, so email_replied is already true for every lead the branch holds and a negative condition can never be satisfied. Steps gated on one are silently discarded at send time — the lead simply stops advancing, with no error anywhere.

Variants

Variant field names differ between this endpoint and campaigns.update:
variant_name defaults to A, B, C… by position when omitted, is uppercased and truncated to 10 characters. weight is clamped to 0–100. variant_label and content_type are also accepted.
Unrecognised keys are dropped silently, not rejected. Sending subject_template here instead of subject produces a step with no subject and no error. Read the branch back with GET /flow and check variants after writing.
A step with no variants at all enrolls and schedules but has nothing to render.

priority

The entry edge’s evaluation order among the campaign’s EVENT edges. Higher is evaluated first.
Priority must be unique across the campaign’s EVENT edges. Reusing one returns 400:

entry_from_node_id

The MAIN node the branch hangs off. Omit it unless you have a reason not to — the server resolves the campaign’s root MAIN node and creates one if the campaign predates the graph. Supplying a node that is not MAIN returns 400.

Updating a Branch

PUT /api/public/v1/campaigns/{campaign_id}/flow/subsequences/{subsequence_id} — scope campaigns:update
Partial by design: any field you omit is left alone. You can retarget the trigger without touching the steps, or replace the steps without touching the trigger.
When you do send steps, it replaces the whole chain — not one step. Send every step you want to keep, in order.
The replacement is reconciled positionally against the persisted nodes rather than by delete-and-recreate, so node and enrollment IDs the worker has already recorded survive an edit. Leads currently in the branch stay in it. Sending an empty steps array is a 400. Updatable fields: label, trigger_type, trigger_params, priority, is_active, steps. Setting is_active: false parks the branch without deleting it.

Deleting a Branch

DELETE /api/public/v1/campaigns/{campaign_id}/flow/subsequences/{subsequence_id} — scope campaigns:update
Passing a TIMEOUT edge instead of an entry edge returns 400:
Deleting the campaign’s last branch flips subsequences_enabled back to false.

Moving Leads by Hand

POST /api/public/v1/campaigns/{campaign_id}/leads/move-to-subsequence — scope leads:update Note the path: this lives under /leads, not /flow.
A manual move:
  • enrolls each lead in the target branch;
  • invalidates their pending MAIN follow-ups, so they stop receiving the main chain;
  • skips leads already enrolled, reporting them in skipped_leads;
  • emits a subsequence_entered webhook event per new enrollment.
entry_edge_id is null for a manual move — no edge fired. That, plus the trigger_reason, is how you tell a hand move from an automatic entry downstream.
Lead membership is validated all-or-nothing. If one ID in a batch of 400 belongs to a different campaign, the entire request fails with 404 and the message names only the count — {"detail": "3 lead(s) do not belong to this campaign"} — not which ones. Validate IDs against GET /campaigns/{campaign_id}/leads first.

Per-Branch Analytics

GET /api/public/v1/analytics/campaigns/{campaign_id}/subsequences — scope campaigns:read
A campaign belonging to another team is a 404, indistinguishable from one that does not exist.

Lead Journey

GET /api/public/v1/campaigns/{campaign_id}/leads/{lead_id}/journey — scope leads:read The whole of one lead’s path through the campaign graph — main chain and branches together, in send order.
node_key is the node ID for branch steps and step:{step_number} for main-chain steps, which is what lets one timeline mix both. This is the fastest way to answer “why did this lead stop getting emails” — read trigger_invalidated down the steps and check enrollments.

Error Codes

Flow errors return a plain string in detail, not an object. Failed mutations roll back — a 400 mid-create leaves no partial branch behind.

Next Steps

Follow-up Sequences

The MAIN chain that branches interrupt.

Leads

Find the lead IDs a manual move needs.

Webhook Events

subsequence_entered and subsequence_completed.

Analytics

Campaign-level metrics alongside per-branch numbers.