Next-Best-Action (NBA) — Engine & Playbooks
The NBA engine watches the deal book and gets a ready-to-review next action in front of the owning rep — a drafted opening outreach, a chase on a stalled deal, or a completed draft the moment an audit lands — without the rep having to ask. It is a hybrid: deterministic code decides WHAT to do (action type, gates, priority, due-hours); the AI advisor decides HOW the draft reads (an autonomous advisor run over the deal brief, in the rep’s own voice). Sales leads shape that voice through natural-language playbooks they edit themselves on the dashboard.
Status (2026-07-02): the precompute (pull) side is LIVE for pilot owners (Barry Byrne; Angel Dimitrov on a scoped cohort). The sweep (push) side is gated OFF (
NBA_ENABLED=0, dry-run default) pending the HubSpottasks.writescope go-live. All flags live inworker/wrangler.tomlwith inline docs.
The two halves
Section titled “The two halves”1. Precompute (pull) — LIVE
Section titled “1. Precompute (pull) — LIVE”Rides the deal-brief refresh lifecycle. After a brief composes for an open deal owned by a pilot rep, one background advisor run precomputes a rich “what’s this deal about” overview plus a next-action draft, and persists both to KV (deal-nba:<id>, 35-day TTL). dashboard-get-deal-brief merges them, so the HubSpot card renders the glance + draft instantly on open — the tools ran once, in advance, not per-view.
NBA_PRECOMPUTE=1enables;NBA_PRECOMPUTE_OWNER_IDSscopes the pilot.NBA_PRECOMPUTE_COHORTScan restrict an owner to a sub-cohort — e.g. Angel is scoped to Inbound deals in early stages (Opportunity / Planned / Reached Out), excluding “Enhanced LM (WC)” (playbook pending). Owners absent from the map get their whole open book.- The precompute recipe follows the deal source’s audit type (Local Rank Check vs Digital Footprint). Where the recipe runs a DF, client-facing copy keeps the “Digital Footprint audit” name.
2. Sweep (push) — built, gated off
Section titled “2. Sweep (push) — built, gated off”The 07:15 UTC cron (next-best-action.js) scans hubspot_deals for new deals in a lookback window (default 36h — detection is a periodic sweep, not a webhook; the table is n8n-reloaded twice daily). For each unactioned deal it asks the playbook what to do, claims the deal in a durable BigQuery ledger (fcr_operations.nba_action_log — the authoritative dedupe and audit trail), and enqueues an autonomous advisor run that reads the deal brief, drafts the opening outreach in the owning rep’s voice, creates a HubSpot review task on the deal, and DMs the rep in Ro.am.
NBA_ENABLEDgates the cron (currently"0");NBA_DRY_RUN(default on) drafts + DMs without creating the task, for eyeballing before go-live;NBA_REP_ALLOWLISTlimits the pilot (currently Barry).- Prerequisite for live tasks: the HubSpot private app needs the
crm.objects.tasks.writescope —create_task_on_dealfails loud on 403 without it.
Two companion sweeps ride the same machinery:
- Stall sweep — open pilot deals with no movement in
NBA_STALL_DAYS(default 7) get an NBA re-run that flips the action to chase/close. - DF-completion re-draft sweep (2026-07-01) — when a Digital Footprint audit finishes for a deal with a pending NBA, the sweep auto-completes the draft from the finished report (
draft_from_reportmode — no re-scan), keyed on the report’ssynced_at+ score.
The playbook — deterministic WHAT
Section titled “The playbook — deterministic WHAT”nba-playbook.js is pure and data-driven: decide() maps (event type, deal state, channel) → an action type. No I/O, trivially unit-testable. Two domain rules are load-bearing:
- Early-stage deals carry no qualified pitch (CLAUDE.md rule): a new deal’s
amount/what_products_pitched/forecast_categoryare creation artifacts, so the engine reads none of them, and the advisor never narrates intent off them. deal_sourceis the one exception, on a different axis: it reliably records the channel / relationship class the deal entered on. The playbook routes on it so a cold-prospect frame is never proposed to an existing client — the advisor still derives the actual angle from the prospect’s real facts in the brief.
Channels are current_customer / inbound / outbound (winback is folded under current_customer — a returning client is still a customer relationship). Per-deal channel resolution precedence:
- Sales override —
nba_source_channelrow for the deal source (a deliberate correction); - HubSpot’s own
hubspot_deals.channel(the primary, per-deal signal); - In-code default (
classifySource) when HubSpot is blank — falling through tooutbound, the most conservative frame.
Playbooks editor — sales-owned HOW
Section titled “Playbooks editor — sales-owned HOW”Named sales leads (Barry Byrne, Oisín MacTigue, Graham Sommers) edit the natural-language playbooks on the dashboard (PlaybooksView tab). A playbook is a rendered prompt block injected into the advisor’s system prompt — the relationship frame + forbidden moves, keyed by channel + deal source (a source without its own playbook inherits the channel baseline). Editors shape tone, never control flow.
Governance mirrors the marketing content carve-out: source of truth is BigQuery (nba_source_channel, nba_playbooks); every write re-renders the KV blob the engine reads (nba:playbook-frames), writes an audit row, and requires a cryptographically-verified editor identity (the signed dashboard JWT — the client-set x-user-name header is never trusted). The in-code defaults in nba-playbook.js are the fallback if KV is empty. Full design: docs/nba-playbooks-editor.md (repo).
Surface
Section titled “Surface”| Route | Purpose |
|---|---|
dashboard-nba-run | The new-deal sweep (cron-driven; HTTP-exposed for manual/forced runs) |
dashboard-nba-stall-run | The idle-deal stall sweep |
dashboard-nba-draft-completion-run | The DF-completion re-draft sweep |
dashboard-nba-precompute | Precompute one deal’s glance + next-action draft into deal-nba:<id> |
dashboard-nba-playbooks | The governed playbooks editor API (list / coverage / assign_channel / upsert_playbook / delete_playbook / render / seed) |
Key files: worker/src/handlers/next-best-action.js (sweep + dispatch), worker/src/handlers/nba-playbook.js (deterministic decide + defaults), worker/src/handlers/nba-playbooks.js (editor API), worker/src/lib/nba-playbook-store.js (BQ+KV store), worker/src/lib/nba-precompute-cohort.js (pilot cohorts). Ledger: fcr_operations.nba_action_log.