OpenSequence
DocsAPI referenceSDKs and CLIReference
Sequences

The definition

One JSON document holds the whole sequence: shape, ids, transitions, settings and exit_on.

Edit on GitHub

A sequence is one JSON document: nodes, transitions, settings and the exit choice. It is what you save into a draft version and what the engine runs once published. Validate one without saving:

curl
curl -X POST $SEQ/v1/sequences:validate -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d "{\"definition\": $(cat definition.json)}"
# {"success":true,"data":{"ok":true,"problems":[{"path":"nodes.email_intro.params.body","code":"NO_UNSUBSCRIBE","message":"…","severity":"warning"}]}}

Shape

{
  "schema_version": 1,
  "entry": "email_intro",
  "settings": {
    "send_window": { "days": ["mon", "tue", "wed", "thu", "fri"], "from": "08:00", "to": "18:00", "tz": "contact" },
    "jitter": "15m",
    "reenroll": "after_90d"
  },
  "exit_on": ["reply", "meeting_booked"],
  "nodes": {
    "email_intro": {
      "type": "outreach", "channel": "email", "action": "send",
      "params": { "name": "Intro email", "subject": "Quick question, {{ contact.first_name | \"there\" }}", "body": "…\n\n{{ unsubscribe_url }}", "reply_in_thread": false },
      "next": { "sent": "wait_2d", "skipped": "wait_2d", "failed": "end_failed" }
    },
    "wait_2d": { "type": "wait", "params": { "duration": "2bd" }, "next": { "then": "check_stage" } },
    "check_stage": {
      "type": "condition",
      "params": { "mode": "all", "rules": [
        { "field": "history.last_task_outcome", "op": "is", "value": "voicemail" },
        { "field": "attrs.deal.stage", "op": "is", "value": "contacting" } ] },
      "next": { "yes": "call_1", "no": "sms_follow" }
    },
    "sms_follow": { "type": "outreach", "channel": "sms", "action": "send", "params": { "body": "Hi {{ contact.first_name | \"there\" }}, did you see my email? Reply STOP to opt out." }, "next": { "sent": "call_1", "skipped": "call_1" } },
    "call_1": {
      "type": "task",
      "params": { "kind": "call", "title": "Call {{ contact.first_name | \"them\" }}", "due": "1bd", "expire": "3bd", "dialer": true },
      "next": { "completed": "end_done", "no_answer": "email_2" },
      "branches_by_outcome": { "connected": "end_connected" }
    },
    "email_2": { "type": "outreach", "channel": "email", "action": "send", "params": { "subject": "Re: Quick question", "body": "…\n\n{{ unsubscribe_url }}", "reply_in_thread": true }, "next": { "sent": "end_done" } },
    "end_done": { "type": "end", "params": { "label": "Done" } },
    "end_connected": { "type": "end", "params": { "label": "Connected" } },
    "end_failed": { "type": "end", "params": { "label": "Email failed" } }
  }
}
KeyMeaning
schema_versionalways 1 today. Bumped only for breaking changes; the service keeps reading every version it has ever published
entrythe id of the first node. Most sequences start with an outreach or a start node; start is optional
settings.send_windowdays and hours outside which outreach steps wait, in the contact's zone (tz: "contact", the default, falling back to the sender's when the contact has none) or the sender's ("sender"). Time is HH:MM, 24-hour; the window is [from, to)
settings.jittera random delay added to each outreach step so sends do not burst on the hour, pushed back inside the window if it lands outside
settings.reenrollnever (default: a contact who was ever in this sequence is skipped), always, or after_<n>d (skipped while the last enrollment finished less than n days ago)
exit_onreply, meeting_booked, both, or none (default []). See below
nodesa flat map from id to node. Ids are lower_snake_case, up to 64 characters, chosen by you

A node is { type, name?, channel?, action?, params?, next, branches_by_outcome? }. next maps each transition key the node emits to a node id; a key left unwired ends the enrollment (completed) when taken, except the keys a node type requires (then, yes, no). branches_by_outcome is for task nodes only. The full list of node types, params and transitions: Nodes and the generated catalogue.

Durations

One grammar everywhere, ^\d+(m|h|d|bd)$: m minutes, h hours, d calendar days, bd business days (Monday to Friday in the relevant zone; holidays are not modelled). Waits, timeouts, task due and expiry, jitter.

Exit rules

exit_on is the only exit configuration: Replied (on any channel; a connected call counts) and Meeting booked. Everything else is built in and not configurable:

Always onBehaviourWho detects it
Opt-out (a STOP text, a click on the hosted opt-out link)exits, suppresses the channelthe service
Hard bounce, complaintexits, suppresses emailthe service
Out-of-office auto-replypauses until the stated date or 7 days; never exitsthe service
Opt-out recorded elsewhereexits, suppressesyou, events { type: "opt_out" }
Do not contact, bad number, invalid email, left the companyexitsyou, events { type: "suppressed" }
Contact deleted or mergedexits with deleted and erasesyou, DELETE /v1/contacts/{id}
Sender disabledsteps skip; the enrollment runs onyou, DELETE /v1/senders/{id}
Exit through the APIexits with reason manual and your labelyou

An exit ends the enrollment and emits enrollment.exited. A reply that arrives when exit_on does not list reply is still attributed and can wake a wait_for; it just does not end the flow.

Conventions that keep it safe to edit by hand or by an agent

Human-readable string ids; a flat node map with named transitions; a closed vocabulary (node types, transition keys, event names, field names, operators); one duration grammar; no expressions and no code. The JSON Schema is served at GET /v1/schema and reproduced in the reference; the limits are 200 nodes and 256 KB.

On this page