Sales reps and follow-up tasks

SalesRep is a workspace-scoped identity for a human seller. It is deliberately separate from Profile: a rep must exist in the system — assignable, syncable to HubSpot and Nooks — before, or even without, having a login.

SalesRep

Field Meaning

name, email

Identity; unique on (subscriptionId, email)

profileId

Optional link to a Profile once the rep has a Supabase account

hubspotOwnerId

HubSpot owner, used to assign contacts and tasks

nooksUserId

Nooks user, used to attribute Nooks calls and assign sequences

activeListId

HubSpot active list backing the rep’s sequences

hubspotCallSegmentId

HubSpot call segment for attributing calls

isActive

Only active reps are assignable

The record is workspace-scoped so the same person can carry different CRM ids in different workspaces.

Endpoints: GET|POST /api/sales-reps, GET|PATCH|DELETE /api/sales-reps/{id}, GET /api/sales-reps/{id}/calls, GET /api/sales-reps/{id}/tasks, and GET /api/sales-reps/profile-match?email= which resolves the signed-in user to their rep record.

Rep resolution

Context Resolution order

Nooks call attribution

callData.userData.userId → SalesRep.nooksUserId (workspace-scoped, then global); fallback callData.userData.email → SalesRep.email, case-insensitive

Sequence call_task assignment

Contact’s hubspotOwnerId → an active rep with that hubspotOwnerId; fallback to the oldest active rep in the workspace

Dashboard "my tasks"

GET /api/sales-reps/profile-match?email=<signed-in email>

SalesRep.nooksUserId must be populated for the primary Nooks match to work; email is only a fallback and breaks when a rep uses a different address in Nooks.

FollowUpTask

A task assigned to a rep about a specific lead, optionally mirrored into HubSpot and/or Nooks.

Field Meaning

salesRepId

Assignee

contactId

The lead

campaignTargetId, contactSequenceId

Optional links to what created the task

type

FollowUpTaskType — call, email, sequence_enrollment

status

FollowUpTaskStatus — pending, in_progress, completed, skipped, failed

hubspotTaskId, nooksTaskId, nooksSequenceStateId

External identifiers

hubspotSyncStatus, nooksSyncStatus

pending, synced, failed per platform

escalationStage

0 → 1 → 2 → 3 = failed

inngestRunId

Stored so an escalation run can be cancelled programmatically

lastEscalatedAt

Last escalation timestamp

dueAt, completedAt

Scheduling

lastHubspotSyncAt, lastNooksSyncAt

Per-platform sync timestamps

notes

Free text

Both external ids are nullable because a workspace may have only one of the two integrations active. Storing inngestRunId on the row is what allows a completed task to cancel its own pending escalation instead of letting it fire against work already done.

Escalation

escalationStage advances when a task is not actioned within its window, and reaching stage 3 marks the task failed. Each advance stamps lastEscalatedAt. The escalation ladder is what turns Contact.heatStatus from hot to warming: a failed rep task hands the lead back to an automated email track.

Endpoints

GET /api/follow-up-tasks filters by search, status, type, sales_rep_id, due_filter, sort, created_from, created_to, with pagination. The response includes a stats block alongside data/total/page/pageSize, so the task board can render its counters from one request.

POST /api/follow-up-tasks creates a task; GET|PATCH /api/follow-up-tasks/{id} reads and updates one. The board is at /tasks, and a rep-scoped view at /sales-reps/{id}/tasks.

Nooks-driven outcomes

Calls made by reps in Nooks arrive at POST /api/webhooks/nooks and are stored as Call rows with channelType = "nooks_call", campaignId = null and targetId = null — they are not campaign activity. Two disposition-gated flows follow:

Flow Trigger

AI analysis

Any disposition not in NOOKS_ANALYSIS_SKIP_DISPOSITIONS ("No Answer", "Voicemail", "Busy") fires nooks/call.logged → nooksCallAnalysis

CRM closed-lost

A disposition in NOOKS_CLOSED_LOST_DISPOSITIONS ("Not Interested", "Do Not Call", "Marketplace not supported", "No longer selling/shut down") publishes Crm.DealUpdate with deal_stage: "closedlost"

See Nooks events for payloads and contact-id resolution.