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).
- Trigger line:
+ New sequenceand+ New stepeditors.
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'strigger_stageand auto-enroll them. Driven by the Pipeline stage-change event and/or a cron sweep.POST /sequences/message-dispatch— pickscheduledmessages withscheduled_for <= now, send via provider, setsent/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.