Spec — Sequences page (NEW)

Automated multi-step outreach ("drip cadences tied to pipeline stages") — the Follow Up Boss replacement. Prototype: panel-sequences.

UX

  • Left list: sequences, each with name, 4 steps · 7 running · 31 sent, and a progress bar (--pct). Active item = accent-tinted.
  • Right detail:
    • Trigger line: Triggers when: pipeline.stage = "new".
    • Stats row: Active runs, Open rate, Reply rate, Moved to (9 / 31).
    • Timeline of steps (vertical line + circles, .fired = sent). Each step: delay (T+0h, T+24h, T+72h, T+7d), channel dot (Email / SMS / call / task), status (sent / queued / scheduled), template subject, body preview with {{first_name}} {{street}} {{agent}} merge vars, and counters (Opens / Clicks / Replies).
  • + New sequence and + New step editors.

Engagement model

A sequence is a template (steps). Enrolling a lead creates an enrollment that walks the steps on a delay schedule, materializing one message per step. Messages carry status + engagement (open/click/reply). Sequence-level stats are rollups over its enrollments' messages.

D1 schema (new migration)

  • sequences(id, user_id, name, description, trigger_stage[new|contacted|offer-sent|under-contract|assigned], status[active|paused|archived] DEFAULT active, created_at, updated_at) — idx (user_id, status, updated_at).
  • sequence_steps(id, sequence_id→sequences ON DELETE CASCADE, step_order, channel[email|sms|call|task], delay_minutes, template_name, template_subject, template_body /* {{vars}} */, condition?, created_at) — idx (sequence_id, step_order).
  • sequence_enrollments(id, sequence_id, lead_id→leads, contact_id?, enrolled_at, enrolled_by['trigger'|'manual'], status[active|paused|completed|unsubscribed] DEFAULT active, current_step_index, completed_at, unsubscribe_reason?) — idx (sequence_id, status, enrolled_at), (lead_id, status).
  • sequence_messages(id, enrollment_id→sequence_enrollments ON DELETE CASCADE, step_id→sequence_steps, channel, recipient, message_body /* rendered */, status[scheduled|queued|sent|failed|opened|clicked|replied] DEFAULT scheduled, scheduled_for, sent_at, failed_reason?, engagement_data(JSON: {opened_at,clicked_at,clicked_links,replied_at,reply_body}), created_at) — idx (enrollment_id, status, scheduled_for), (step_id, sent_at).
  • Optional later: sequence_contacts (multi-contact leads: owner + attorney), sequence_templates (reusable), sequence_analytics (denormalized rollup for dashboard speed).

API (/api/v1)

CRUD: GET/POST /sequences, GET/PUT/DELETE /sequences/:id (DELETE = archive), POST /sequences/:id/steps, PUT/DELETE /sequences/:id/steps/:stepId. Enrollment: POST /sequences/:id/enroll {lead_ids[]}, GET /sequences/:id/enrollments, PUT …/enrollments/:eid (pause/resume/unsubscribe). Messages: GET /sequences/:id/messages, POST …/messages/:mid/resend. Dashboard: GET /sequences/stats.

Internal (cron-driven, not UI):

  • POST /sequences/trigger-check — find leads whose pipeline stage matches an active sequence's trigger_stage and auto-enroll them. Driven by the Pipeline stage-change event and/or a cron sweep.
  • POST /sequences/message-dispatch — pick scheduled messages with scheduled_for <= now, send via provider, set sent/sent_at. Cron every 60s.

Sending

Reuse @velli/email-relay (Resend) for email. SMS/call are stubs initially (mark sent and record intent). Anti-spam: respect unsubscribed, dedupe per enrollment+step.

Build notes

  • Ship the read/visual slice first (list + detail timeline from seeded sequences/steps/enrollments/messages) — fully verifiable offline.
  • Then the engine slice: enroll → schedule → cron dispatch → engagement status, wired to the pipeline stage-change event.