OpenSequence
DocsAPI referenceSDKs and CLIReference
Sequences

Conditions

The field registry, the operators, how missing data behaves, and how every fact is recorded on the run.

Edit on GitHub
"check": {
  "type": "condition",
  "params": {
    "mode": "all",
    "rules": [
      { "field": "contact.has_phone", "op": "is_true" },
      { "field": "history.last_task_outcome", "op": "is", "value": "voicemail" },
      { "field": "attrs.deal.stage", "op": "is_not", "value": "won" }
    ]
  },
  "next": { "yes": "sms_follow", "no": "email_2" }
}
curl
curl $SEQ/v1/schema/fields -H "Authorization: Bearer $KEY"
# {"sources":[{"prefix":"contact","label":"Contact","open":false,"fields":[{"field":"contact.country","label":"Contact country","type":"text"},…]},…],"attrs":[{"field":"attrs.deal.stage","label":"Deal stage","type":"enum","values":[…]}]}

A condition is a list of { field, op, value } combined with all or any (1 to 20 rules). The registry of fields is closed and served at GET /v1/schema/fields, so your picker, the validator and the evaluator cannot disagree. The generated table of every field and operator is in the reference.

Fields

PrefixFieldsSource
contact.country, timezone (text), has_email, has_phone, has_linkedin, sms_consent (bool); also first_name, last_name, email are readablethe contact row
attrs.<path>anything you pushed, read as textthe contact's attrs
sender.timezone (text), has_sms, has_linkedin (bool); also namethe sender and its active channels
history.touches, days_since_last_touch, emails_sent, days_since_last_task, tasks_last_14d (number), replied_since_enrollment (bool), last_reply_channel (enum: email, sms, linkedin, call), last_task_outcome, last_task_kind (text)the enrollment, runs, signals and tasks: one indexed query each
linkedin.connection_stateenum: none, pending, connected, declined_or_ignored, withdrawnreserved for LinkedIn automation
crm.person.exists, company.exists, deal.exists, deal.count_open and any person.<slug>, company.<slug>, deal.<slug>one live Attio lookup per contact per step (CRM conditions)

Operators

TypeOperators
textis, is_not, contains, is_empty, is_not_empty (case-insensitive)
enumis, is_not (the value must be one of the field's values)
numbereq, ne, gt, gte, lt, lte
boolis_true, is_false
datebefore, after (ISO dates)

The validator refuses an operator the field's type does not support (BAD_OPERATOR), a missing value where one is needed, a non-number for a number field, and a value outside an enum (BAD_VALUE). Fields the registry does not know are UNKNOWN_FIELD.

Missing data never matches

A fact that is null (the contact has no country, no task was ever completed, the CRM lookup found nothing or failed) fails every operator, including is_not and ne. So in all mode a missing fact sends the contact to no; in any mode it is simply not a match. Design for it: put the "has the data" rule first (contact.has_phone is_true), and treat no as the safe path.

Evaluation is lazy and recorded

Rules are read in order and the evaluation stops at the first decisive one (all stops at the first false, any at the first true). Each read is stored on the run as result.facts:

"facts": [
  { "field": "contact.has_phone", "label": "Contact has a phone", "value": true, "rule": "is true", "matched": true },
  { "field": "history.last_task_outcome", "label": "Last task outcome", "value": "connected", "rule": "is voicemail", "matched": false }
],
"mode": "all", "branch": "no", "note": "1 of 3 rules matched, so the contact took No."

A read that failed (Attio down) also carries error. This is what the journey shows, so "why did they take No" is answered from what the decision saw.

Keeping facts fresh

attrs.* is what you last pushed; keep it current with PATCH /v1/contacts/{id}. history.* is maintained by the service. crm.* is read at decision time and needs no sync.

On this page