Skip to Content
Living documentation — last reviewed 2026-05-28
RunbooksSales workspace operations

Sales workspace operations

The Sales workspace is on for every organization; there is no per-organization switch. This runbook covers setting an organization up to work leads in it, bringing its older contact history into the attempt facts Sales reads, and the email setup it depends on. It does not create production data, apply production migrations, or change configured automation rules.

Setting an organization up

  1. Run scripts/audit-sales-workspace.ts --org <uuid> with the target read-only database connection. It reports counts/IDs rather than lead contact information. Resolve ambiguous dual-subject tasks and terminal work with the owner. Existing backfill tooling must remain dry-run until its proposed changes are reviewed.
  2. Record the organization’s timezone, working weekdays/hours, active Sales staff, chosen callbacks, configured automation filters, enrollment counts and the automation kill-switch state. Do not reinterpret existing UTC automation windows as local time or auto-enable rules.
  3. In Sales → Manage, owner/admin give coaches own or team Sales access (optionally as Sales managers) and set working hours and the no-answer rule. Sales is staff-only: anyone who should work leads needs the coach role first, because a member (customer) cannot be granted Sales access. Do not grant organization admin solely to enable Sales.
  4. If the organization logged lead contact before attempt times were recorded, backfill them: see Backfill attempt times. Dry run first, apply only after review.
  5. After any approved bulk backfill/import, confirm PostgreSQL statistics are current (normal autovacuum/ANALYZE maintenance); stale fixture statistics produced a slow first request in local rehearsal. Compare queue counts and a known report period with fixtures/manual evidence. Daily effort, intake cohorts and settlement-period revenue use different denominators. Old unspecified calls and unprovable salesperson credit cannot be reconstructed from current owner or mutable timestamps.
  6. Exercise a callback, no answer, pending trial, confirmed no-show, inbound email while composing, opt-out, reassignment and conversion. Verify the General Tasks and member inbox surfaces are separated from lead work.

Legacy Leads routes

/dashboard/leads/list, /inbox, /pipeline and pipeline-id URLs redirect into the Sales workspace (Work queue, Needs reply view, Board); old links carrying ?leadId= open that lead’s sheet. Sales offers CSV import (owner), bulk Tag and Delete (owner/admin) and Edit details (name, contact, note), but not consent switches, bulk stage moves, pipeline membership edits, select-all-matching or restore from archive. Member conversations, including member email threads, stay on Messages; lead threads are answered from the lead sheet.

Backfill attempt times

Sales reads a lead’s attempts from organization_leads.first_attempt_at and last_attempt_at. Contact logged before those writers recorded attempts never set them: legacy touches (call_logged), completed contact tasks and replies from the legacy inbox. That work shows no result on the queue row and no attempts on the lead sheet, and First response counts those leads as never contacted. Every one of those paths now records attempts itself; the backfill fills in the history.

scripts/backfill-sales-attempts.ts reads three kinds of evidence: call_logged activity, human inbox messages the provider accepted, and last_contacted_at. For each organization lead (every status; archived leads included and counted separately), it sets first_attempt_at where empty and last_attempt_at where empty or earlier, never earlier than the lead’s intake (created_at). It bumps the version only on rows it changes. It writes no activity and leaves statuses, stages and result facts as they are.

Run it once per organization whose contact history predates attempt recording. scripts/audit-sales-workspace.ts reports how many leads still need it (legacy_attempt_time_unknown). The default is a read-only dry run; always run it first and have the owner review the counts, the sample lead ids and the First response estimate. Only after that review does an operator run it again with --apply, which writes one transaction per batch. An agent never runs --apply against production. A second --apply changes nothing.

TSX_TSCONFIG_PATH=tsconfig.base.json DATABASE_URL=<connection> \ pnpm exec tsx scripts/backfill-sales-attempts.ts --org <uuid> [--org <uuid> …] # after review: the same command with --apply

The backfill cannot recover outcomes. Legacy contact never recorded whether anyone answered, so the backfilled attempts carry no result: whether that earlier work connected (first successful contact) stays unknown, and the daily effort counts leave it out. Leads still new keep their status; the dry run reports how many. After --apply, scripts/audit-sales-workspace.ts should report legacy_attempt_time_unknown: 0.

Email setup and operational limits

The existing email transport and inbound route are reused. External WhatsApp links remain manual: opening a link is not evidence of a sent message, unread state or delivery. No WhatsApp provider is configured by the Sales workspace.

To receive delivery failure updates, configure Resend’s signed events for POST /webhooks/email/delivery and supply EMAIL_DELIVERY_WEBHOOK_SECRET with its signing secret. Enable email.delivered, email.bounced, email.failed, and email.suppressed. Verification uses the original raw request body and Svix headers. Unsigned requests fail closed. Keep the provider webhook configuration in place: without it, delivery facts for sent Sales messages are lost. Provider event documentation .

An email accepted by the provider is not proof of delivery. A matching failure reopens the reply obligation unless a later response/acknowledgment superseded it. A retry uses its original operation/provider key. An unresolved send older than the provider deduplication window requires checking provider history before creating a new send; do not blindly resend.

Validation scope

Local integration fixtures use a disposable database ending in _test. They stub external email/payment providers; no real message, refund or membership invitation is issued. Staffing targets and sample-month financial reconciliation remain acceptance checks per organization, not claims proven by local tests.

See architecture, ADR-0023 and the implementation verification record.