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 R29) refuses the write with 422 SEQUENCE_INVALID, and the Motion stays as it was; a warning (W1 to W17; W10 is retired and W15 unused) 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"
}pathnames the step by its id, then the field.pointeris a JSON Pointer (RFC 6901) into the document you sent, so a tool finds the line without parsingpath; a missing key points at the object that lacks it.messageequalsrule.hintis 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 AI 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 ;.
R25: save and save_to_contact
Put save and save_to_contact on an until event.<name> step, 1 to 20 entries each: save names are lowercase (not id), save_to_contact keys are custom fields or email.R26: Field paths
Write the path with dots, items[0] and ["a.b"], like lead.score, at most 256 characters.
R27: Enrollment values are saved before use
Save the value with save on an until event.<name> step first; a save_to_contact key reads as {{contact.extra.<key>}}.R28: Number comparisons
Use gte or lte with a number, like {"gte": 50}, on enrollment.<key> or contact.extra.<key>.R29: Saved values against the sample
Save a field inside the object or list, or compare that value with eq, neq or exists instead.
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 (retired)
Retired: never emitted. The Sequence's on_reply decides what a reply does.
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.
W14: Saved value never used
Use the value in a message or an if step, or remove it from save if no webhook template sends it.
W16: Saved value read as a Contact field
Use {{enrollment.score}} to read the saved value.W17: Invitation in a Connected only Motion
Set settings.assign to auto, the default: it prefers an Account already connected and invites everyone else. Keep already_connected only for a Motion that writes to existing Connections.