Skip to content

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 HubSpot tasks.write scope go-live. All flags live in worker/wrangler.toml with inline docs.

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=1 enables; NBA_PRECOMPUTE_OWNER_IDS scopes the pilot.
  • NBA_PRECOMPUTE_COHORTS can 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.

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_ENABLED gates the cron (currently "0"); NBA_DRY_RUN (default on) drafts + DMs without creating the task, for eyeballing before go-live; NBA_REP_ALLOWLIST limits the pilot (currently Barry).
  • Prerequisite for live tasks: the HubSpot private app needs the crm.objects.tasks.write scope — create_task_on_deal fails 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_report mode — no re-scan), keyed on the report’s synced_at + score.

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_category are creation artifacts, so the engine reads none of them, and the advisor never narrates intent off them.
  • deal_source is 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:

  1. Sales overridenba_source_channel row for the deal source (a deliberate correction);
  2. HubSpot’s own hubspot_deals.channel (the primary, per-deal signal);
  3. In-code default (classifySource) when HubSpot is blank — falling through to outbound, the most conservative frame.

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).

RoutePurpose
dashboard-nba-runThe new-deal sweep (cron-driven; HTTP-exposed for manual/forced runs)
dashboard-nba-stall-runThe idle-deal stall sweep
dashboard-nba-draft-completion-runThe DF-completion re-draft sweep
dashboard-nba-precomputePrecompute one deal’s glance + next-action draft into deal-nba:<id>
dashboard-nba-playbooksThe 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.

Ask the docsRAG over this site
Ask anything about the FCR Dashboard platform — architecture, BigQuery, the worker routes, billing rules, the LRC stack, scoring… Answers are grounded in this documentation, with source links.
How does the deal-brief refresh work? Which routes are Worker vs n8n? How is account health scored?