PPactDocs
Glossary

Sequence

A multi-step outbound sequence in Pact: how steps, variants, channels, and idempotent deliveries are modeled and rendered.

Sequence

A sequence is an ordered, multi-step outbound cadence attached to enrolled subjects (companies or contacts). Each step declares a channel action — email, SMS, a manual LinkedIn touch, a call, a task, or a wait — and the sequence engine renders and dedupes deliveries as enrollments advance.

Sequences live under the sales module. Every route is gated by require_module("sales") and authenticated with an API key, so a tenant without the sales module gets a clean 403 rather than a partial view.

API surface

The router is mounted at /v1/sequences (api/routes/sequences.py):

MethodPathPurpose
GET/POST/v1/sequenceslist / create sequences
GET/PATCH/DELETE/v1/sequences/{seq_id}fetch / update / delete
GET/POST/v1/sequences/{seq_id}/stepslist / add steps
PATCH/DELETE/v1/sequences/{seq_id}/steps/{step_id}update / delete a step
GET/POST/v1/sequences/{seq_id}/enrollmentslist / enroll a subject

A sequence is persisted as one sequences row plus N sequence_steps rows; enrollments are tracked separately and deliveries land in sequence_deliveries.

Template rendering

Step bodies are rendered by sequence_engine.rendering.render_template. The renderer is deliberately minimal and safe for outbound messaging:

  • Only {{ variable_name }} substitution is supported — no expression evaluation, no attribute traversal, no code execution.
  • A variable that is missing (or resolves to an empty value) is left in place as its original {{var}} token so a human can see what would fail before a real send.
  • Every unresolved token is reported in RenderResult.missing_vars.
python
from sequence_engine.rendering import render_template

render_template("Hi {{ first_name }} at {{ company }}", {"first_name": "Dana"})
# RenderResult(text="Hi Dana at {{ company }}", missing_vars=("company",))

Channels

sequence_engine.outbox.canonical_channel maps a step's action type to a stable delivery channel slug. Channels split into two sets:

  • SENDABLE_CHANNELS = {"email", "sms"} — Pact can dispatch these itself.
  • MANUAL_CHANNELS = {"linkedin", "call", "task", "wait", "wait_reply", "branch"} — surfaced to a human or handled as control flow rather than an automated send.

A/B variants and idempotency

Two pure helpers in sequence_engine.outbox keep sends reproducible and exactly-once:

  • choose_variant(...) deterministically buckets an enrollment+step into variant "A" or "B" by hashing sha256("{company_sequence_id}:{step_id}:variant") against the configured split percentage. If no Variant B body exists it always returns "A", so retries never reassign copy.
  • build_idempotency_key(...) produces a SHA-256 dedupe key over "{tenant_id}|{company_sequence_id}|{sequence_step_id}|{variant}". The delivery writer checks this key against sequence_deliveries before inserting, so a re-run of the same enrollment-step-variant never double-sends.

Enrollment == company_sequence_id

In the delivery layer, an enrollment is identified by company_sequence_id. That is the value threaded through both the variant hash and the idempotency key, which is why re-processing an enrollment is stable across retries.