Sequence rules

Every Motion's Sequence document is checked whole, on every write that carries it, and offline by curviate motion lint: both give the same verdict. An error (R1 to R24) refuses the write with 422 SEQUENCE_INVALID, and the Motion stays as it was; a warning (W1 to W13) never blocks and appears in the Motion's warnings[].

Each entry in errors[] and warnings[] reads like a compiler error:

{
  "path": "invite.with.withdraw_after",
  "rule": "must be a duration between 14d and 180d: a number then m, h or d, like 30d",
  "code": "R21",
  "message": "must be a duration between 14d and 180d: a number then m, h or d, like 30d",
  "pointer": "/steps/5/with/withdraw_after",
  "hint": "Set withdraw_after between 14d and 180d, for example 21d.",
  "docs_url": "https://docs.curviate.com/reference/sequence-rules#r21"
}
  • path names the step by its id, then the field. pointer is a JSON Pointer (RFC 6901) into the document you sent, so a tool finds the line without parsing path; a missing key points at the object that lacks it.
  • message equals rule. hint is one sentence saying what to do; it never holds a Contact's data.
  • Every error comes back at once, in step order, so one round of fixes covers them all.

curviate motion lint --file motion.json prints the same entries, one per line: the file, line and column, error or warning, the code, the path, the message, the hint and the docs URL.

Deprecated forms. Until 2027-01-19 these are still accepted with warning W13: more than 2 variants on a step, until signal.<name> (now until event.<name>), {{motion.<key>}} and settings.variables (write the text in place, or use a custom field).

Delays are about their duration. A wait or an until timeout always gets a small random spread added, never subtracted, so no step lands on the dot. The document keeps the duration you wrote.

Rules and warnings

R1: Top-level keys and $schema

Keep only $schema, on_reply and steps at the top level; $schema, when present, is https://docs.curviate.com/schemas/sequence/v1.json.

R2: Step count and document size

Keep the document to 1 to 100 steps and 256 KiB; split a longer flow into two Motions.

R3: Step ids

Give each step a unique id: lowercase letters, digits and _, starting with a letter, at most 40 characters, not end.

R4: Exactly one kind per step

Give the step exactly one of do, wait, until, if, emit or tag.

R5: Keys a step kind takes

Remove the key: each step kind takes only its own keys, and the sending Account is chosen with settings.assign.

R6: Targets exist

Point the exit at a step id of this document, or at "end".

R7: then on until and if

Add then: the step to go to when the event happens or the condition is true.

R8: No cycle

Break the loop: point one exit of the cycle at a later step or at "end".

R9: No unreachable step

Lead an exit of another step here, or delete the step.

R10: Explicit endings

End the last step explicitly: set its exits to a step id or "end".

R11: on_reply

Set on_reply to a step id or "end".

R12: Durations and their bounds

Use a duration between 1m and 365d, for example 1d.

R13: Actions and their parameters

Use an action from the list and only the parameters it takes, with values of their type.

R14: Events and timeout

Wait for invite.accepted, message.replied, message.read or event.<name>, and give a timeout.

R15: Predicates

Use one predicate key per object with its typed operand; chain if steps instead of all or any. account.* and message.* work in if steps only.

R16: Variable syntax and names

Write a variable as {{first_name}}, {{my_first_name}} or a custom field {{opener}}; the long forms {{contact.first_name}}, {{contact.extra.opener}} and {{account.first_name}} work too.

R17: No variable defaults or filters

Remove the part after | and add a variable-free fallback on the step instead.

R18: No single-brace placeholder

Write a variable with double braces, like {{first_name}}, or remove the braces.

R19: Fallbacks

Add a variable-free fallback: with.fallback, with.note_fallback (or "drop") on an invitation, "skip" on a comment.

R20: A comment uses a Contact variable

Use a Contact variable in the comment, for example a custom field {{comment}} your agent writes per Contact.

R21: withdraw_after

Set withdraw_after between 14d and 180d, for example 21d.

R22: Text lengths and variable count

Shorten the text to the limit (variables are not counted) or use fewer variables.

R23: Variants (A/B)

Give the step exactly 2 variants (A/B) with ids unique in the document, or one text in with.

R24: Event and Tag names

Name the event with lowercase letters, digits, _ and ., starting with a letter, like reply.received; a Tag is 1 to 64 characters without , or ;.

W1: Merge point

If the merge is intended, keep it; otherwise give the jumping step its own continuation.

W2: InMail without Sales Navigator or Recruiter

Add a sending Account with Sales Navigator or Recruiter, or remove the InMail step.

W3: Invitation note on a basic Account

Set note_fallback to "drop" so invitations go on without a note once the noted ones are used up.

W4: Read receipts on Recruiter

Wait for message.replied instead, or add a sending Account that does not use Recruiter.

W5: Follow after an invitation

Remove the follow step: the invitation already follows the person.

W6: Acceptance without an invitation

Send an invitation on this path before waiting for its acceptance.

W7: Comment personalised by name only

Write the comment per Contact, for example in a custom field {{comment}}.

W8: Link in the first message

Keep the link for a later message.

W9: Motion variable checked by the server only

Motion variables are deprecated: write the text in place, or use a custom field.

W10: on_reply unused

Set on_reply to "end", or turn stop_on_engagement off.

W11: Custom field that looks like a typo

Did you mean first_name?

W12: Seen check before any message

Send a message on this path before checking whether it was seen.

W13: Deprecated form accepted for now

Use the form the warning names instead.