Skip to main content

Nox Funnel

Behaviour-derived lead stages: cold → intermediate → warm → booked, plus a manual terminal won and a very-warm priority flag (not a stage). backend owns the engine (derivation, jobs, GHL writes); ms-ai only relays it to Nox via tools and brief blocks; frontend renders the board (Dashboard → Funnel tab). Limited beta.

signal sources engine (backend)
GHL webhooks (opens/clicks/tags) ─┐
kk-click / web visits ─┤
FunnelSweepJob (sessions/calls/ ─┼─► FunnelStageService.recordSignal()
chats, every 10 min) ─┤ dedupe → $inc counter → derive stage
ms-ai conversation outcomes ─┘ (upward-only) → very-warm flag

stage change ──► GHL stage-tag mirror (`nextlevel stage:<stage>`)
very-warm flip ──► NoxFunnelInterceptService (in-app + push, capped)
nightly ──► FunnelDormancyJob (step-down demotion)
NoxFunnelReconcileJob (behaviour vs mapped GHL pipeline)

Stages

StageHow it's earned
coldentered the funnel (Agent K push, feed tag, form, opportunity seed)
intermediatefirst visit/open/click, opens ≥ email_open_threshold (3), answered call, reply, chat, score_bands.intermediate
warmvisits ≥ 2, session ≥ 120s, high-intent pages, re-open days ≥ 2, multi-link clicks, score_bands.warm; instant-warm signals (instant_warm_signals, default booking_start)
bookedany booking
wonmanual/terminal onlymoveToWon; the auto engine never derives or demotes it

very_warm is a flag with expires_at (demotion.very_warm_hold_days, 14d, re-extended by engagement). OR-rules in FunnelStageService.deriveVeryWarm: same-day visits ≥ 2, one campaign opened ≥ 4×, contact shared, positive conversation, multi-channel within 24h, spike vs baseline, score band. Promotion is upward-only; demotion belongs exclusively to the dormancy job (step-down one stage at a time, with a stage_baseline snapshot the contact must re-earn against). conversation_negative demotes one step immediately.

Data model (backend)

CollectionModelNotes
funnel_contactsapp/models/FunnelContactModel.jsone doc per (company, contact), keyed by ghl_contact_id OR person_id (partial-unique each; merged on stitch). $inc signal counters, recent_signals[] window, stage_history[], very_warm{}, won_details{}
nox_funnel_audit_logsNoxFunnelAuditLogModel.jsevery decision (dedupes, gates, flips, intercepts) — TTL 120 days (live deployments need a manual collMod)
funnel_daily_statsFunnelDailyStatsModel.jsper (company, local day) census/transitions/dwell — the math engine's input
funnel_reconciliation_itemsNoxFunnelReconItemModel.jsbehaviour stage vs CRM layer mismatches, deduped review queue
brief_offersBriefOfferModel.jsexecutable brief offers, valid_until ≈ 48h (read-time gate, no TTL delete)
lead_lifecycle_eventsLeadLifecycleEventModel.jsappend-only lifecycle (stage_changed, showed, closed_won, …)

Config lives in company.nox_funnel.* (strict subdoc — undeclared keys drop silently): thresholds.*, score_bands, demotion.* (90/60/14/14 days), feeding.{tags, tag_rules[{tag,stage}], forms_enabled, opportunities_enabled}, high_intent_pages, instant_warm_signals, stage_tag_prefix (default nextlevel stage:, blank = off), goals.monthly_booked_target, pipeline_mapping.pipelines[{pipeline_id,stage_id,layer}], actions.{workflow_trigger_mode, frequency_cap, standing_rules}, intercept.enabled, learned.{thresholds,history} (self-learning).

Jobs (backend SchedulerService)

JobScheduleDoes
FunnelSweepJobevery 10 minfolds session analyses / calls / chats into recordSignal (20-min lookback, dedupe-safe)
FunnelDailyStatsJobhourly (once per local day)yesterday's census rollup into funnel_daily_stats
KnoxGhlSyncJob03:00 UTC(pre-existing) GHL sync the funnel jobs depend on
FunnelDormancyJob04:00 UTCstep-down demotion, very-warm hold expiry, reactivation flagging
NoxFunnelReconcileJob04:30 UTCmapped-pipeline scan → recon queue; feed-tag scan; closed-won/lost polling
FunnelBackfillJob05:00 UTC + manualidempotent history backfill after gating a tenant (kk_tokens → lifecycle replay → bookings)
NoxFunnelLearningJobMon 05:00 UTCdemote-only self-learning: repeated rejections (n≥5, ≥60%) raise a threshold one notch into nox_funnel.learned
DailyBriefEmailJobevery 10 min ticksends the brief once/local-day at knox.daily_brief.send_hour (default 9; funnel tenants set 5)

Very-warm intercept

app/services/NoxFunnelInterceptService.js — fired in-process on a very-warm flip: in-app + push notification + nox-funnel-very-warm socket. Redis-capped 1/contact/day and 5/company/hour (code constants; fails open without Redis). Throttles are audited (intercept_throttled).

GHL integration

  • Feeding — contacts carrying a feeding.tag_rules tag enter at that stage (upward-only against an earned stage); legacy flat feeding.tags reads as all-cold. Forms and mapped-pipeline opportunities can also seed members (forms_enabled / opportunities_enabled).
  • Stage-tag mirroring — every stage change writes <stage_tag_prefix><stage> and removes stale stage tags (fire-and-forget).
  • Pipeline mappingpipeline_mapping maps GHL pipeline/stage → funnel layer; the reconcile job compares behaviour stage vs mapped location and queues mismatches for owner approval (knox_funnel_reconcile in chat, or the dashboard Reconciliation panel). movePipeline falls back to a tag (mode: 'tag_fallback') when GHL scopes refuse the write.
  • Outbound sends go through NoxGhlActionService.checkOutboundAllowed (actions.frequency_cap, default 2 sends / 7 days / channel).

Gating

Super-admin company.nox_funnel_beta_enabled (toggle: PUT /company/:_id/nox-funnel-beta, also drops knox.daily_brief.send_hour 9→5) outranks tenant nox_funnel.enabled. FunnelStageService.getConfig() returns null without the beta flag — fully dark, no writes, no audit.

Endpoints

  • Tenant (app/routes/noxFunnelRoutes.js): GET /knox/funnel/overview, /contacts, /call-prep, /contacts/:id/journey, /contacts/:id/audit, /reconciliation; POST /contacts/mark-won, /contacts/move-stage, /reconciliation/apply, /reconciliation/:id/dismiss.
  • Settings (CompanyRoutes.jsNoxFunnelSettingsController): GET/PATCH /company/:id/nox-funnel, POST …/nox-funnel/sync, GET …/nox-funnel/ghl-options.
  • Admin (adminNoxFunnelRoutes.js): GET /admin/nox-funnel/audit-logs, /contacts/trace; POST /backfill, /brief-send-hour-backfill, /reason-text-backfill.
  • Internal ms-ai → backend (internalKnoxRoutes.js): GET /internal/knox/funnel/{contact,summary,stats,priorities,reconciliation}, POST …/actions/{tag,move,trigger-workflow,book-meeting,mark-customer}, …/reconciliation/apply, …/conversation-outcome, …/suggestion-decisions; brief: GET /internal/knox/brief/{snapshot-data,sections,offers}, POST …/offers/:id/resolve.

ms-ai side

  • knox_funnel read tool (tools/knox-funnel.tool.ts) — kinds summary | priorities | stats | contact; the source of truth for lead-state questions on funnel tenants. Honesty-gated: not_enabled → fall back to knox_stats and don't mention the funnel.
  • Action tools (tools/knox-funnel-actions.tool.ts): knox_tag_contact, knox_move_pipeline (auto), knox_trigger_workflow (confirm per workflow_trigger_mode), knox_book_meeting (hard confirm), knox_funnel_reconcile (list/apply), knox_mark_customer (→ won). All honesty-gated — ok:false means it did NOT happen.
  • Brief: deterministic funnel blocks from services/knox-funnel-insights.ts (10 blocks, min-sample guards) assembled by knox-brief-snapshot.service.ts; knox_brief_offers grounds "yes, do the first one" against persisted brief_offers and executes via the mapped real tool. Conversation outcomes are classified in the fact-extraction pass (confidence ≥ 0.7) and posted to /internal/knox/funnel/conversation-outcome.
  • Math/priorities are computed in backend: NoxFunnelMathService.js (deterministic rates/velocity/bottleneck/pacing over funnel_daily_stats, rates suppressed under min-sample — never faked) and NoxFunnelPriorityService.js (explainable scoring, every component emits a human reason; no ML/LLM).

Frontend

  • Board: views/funnel/Index.vue + views/funnel/components/ (drag-to-move FunnelBoard.vue, CallPrepPanel.vue, ReconciliationPanel.vue); /funnel redirects to /dashboard?tab=funnel.
  • Tenant settings: views/settings/nox-funnel/Index.vue (/settings/nox-funnel).
  • Admin: beta toggle in views/admin/companies/Detail.vue; audit browser views/admin/nox-funnel/{AuditLogs,ContactTrace}.vue.