Skip to main content

Agent K

Agent K sources prospects and pushes them into the tenant's GoHighLevel — contact + custom fields + why-now note + trigger tags — and the tenant's own GHL workflows send the sequences. Knock Knock never drafts or sends Agent K email (Amendment 02); LinkedIn keeps an approval queue. Backend owns the pipeline; ms-ai does drafting-adjacent LLM work (ICP parsing, previews, replies) and all pricing/billing. Limited beta.

feeders queue & pacing
inbound KnockID (realtime, ─────────► (bypasses queue → push)
PersonResolverService)
AgentKOutboundJob (hourly): ─┐
Apify SoS registrations + ├─► agent_k_prospects ─► AgentKTrickleJob
Apollo search→match reveal │ (pending/drafted/ (5 min tick, jittered
+ PredictLeads why-now ─┘ skipped ledger) 6–20 min gap, burst ≤3,
AgentKReactivationJob (hourly, daily cap)
dormant GHL contacts) │

AgentKPushService.pushLead → GHL contact
kk_token + kk_why_now fields · why-now note
`nextlevel *` trigger tags (tag failure = push failure)

tenant's GHL workflows send email …?kk={{contact.kk_token}}

POST /user-session/kk-click ─► identity stitch + `nextlevel returned-to-site`

Feeders

FeederTriggerFiles
Inbound KnockIDrealtime on verified identify — PersonResolverService._fireAgentKInbound → ms-ai POST /internal/knox/agent-k/inboundapp/services/PersonResolverService.js, ms-ai/src/agent-k/
Outbound net-newAgentKOutboundJob (hourly; once/day per tenant via ai_agent_k.last_outbound_scan_day claim). Apify SoS filings (per-state actors in app/configs/apify.js; the shared pull runs as company_id: 'platform') + Apollo mixed_people/api_searchpeople/match reveal (1 credit, ≤15/scan) + PredictLeads signalsapp/jobs/AgentKOutboundJob.js, app/services/AgentKOutboundService.js, app/lib/apifySos.js
CRM reactivationAgentKReactivationJob (hourly; once/day per tenant), requires re_engage.revive_crm + GHLapp/jobs/AgentKReactivationJob.js, app/services/ReactivationService.js

Why-now rule: outbound discovery requires a signal (hiring → funding → expansion → modernizing, per enabled ai_agent_k.watch_for.*); ICP fit alone is a cold list — the prospect is ledgered skipped with reason no_why_now. New-business registrations carry an unconditional why-now.

Queue & pacing

agent_k_prospects (app/models/AgentKProspectModel.js) — status pending | drafted | skipped, unique (company, source, external_id) doubles as the dedupe ledger. AgentKTrickleJob (every 5 min) releases with a jittered 6–20 min gap, burst 1–3, ranked by AgentKSourceWeightService.scoreProspect. Daily cap = ai_agent_k.daily_limit (default 10), counted from lead_lifecycle_events with stage pushed + legacy drafted since local midnight.

GHL push (app/services/AgentKPushService.js)

pushLead() order: suppression check (NoxGhlActionService, 72h per-contact cooldown) → mint kk_token → write custom fields → why-now note (Agent K — why now: …) → trigger tagsNoxActionModel status pushed + lifecycle + funnel enterFunnel.

  • Custom fields write by field ID — resolved once per name and cached in go_high_level.kk_field_ids (Mixed map; go_high_level is a strict subdoc — cache keys must be schema'd or writes drop silently). kk_token / kk_why_now are fixed; linkedin_url / job_title names are tenant-configurable via ai_agent_k.ghl_fields (blank = skip). Native GHL fields (company/website/city/state/country) fill directly. Field and note writes are best-effort.
  • Tags are the workflow trigger — all prefixed nextlevel (with space): nextlevel agent-k-lead, nextlevel signal:<type>, nextlevel source:<src>, plus nextlevel returned-to-site written on kk-click. A tag write failure fails the push (reason: 'tag_failed'); everything else is best-effort.

kk_token & click stitching

  • kk_tokens (app/models/KkTokenModel.js) — one token per (company, GHL contact), or per email for external campaigns; denormalized email/phone/name so click resolution needs no GHL call. Every GHL template link appends ?kk={{contact.kk_token}} — the token lives on the contact.
  • POST /user-session/kk-click (UserSessionController@kkClick): bot filter (client flag → scanner-UA regex → IP check) → token lookup → PersonResolverService.resolve stitches the identity onto the session → writes nextlevel returned-to-site → funnel link_click/web_visit signals → auto-engage after 8s (once/session).
  • Backfill: POST/GET /knox/agent-k/kk-backfill stamps kk_token onto existing GHL contacts (requires the field to already exist; progress persisted at ai_agent_k.kk_backfill, socket kk-backfill-progress).

Campaign tokens & ManyReach

For external senders: POST /knox/agent-k/campaign-tokens mints kk_tokens rows per email (no GHL contact). ManyReach push (app/services/ManyReachService.js; routes POST /knox/agent-k/manyreach/{connect,token-field,push}, GET …/campaigns) writes the token into a ManyReach custom field (ai_agent_k.manyreach.token_field, default custom20); templates link with ?kk={{<token_field>}}.

LinkedIn (Unipile)

app/services/AgentKLinkedInService.js + AgentKLinkedInJob (every 30 min); sequences in agent_k_linkedin_sequences. Tenant's own connected account id in ai_agent_k.channels.linkedin.account_id. Warm-up like → 12–48h wait → connect request lands in the approval queue (a NoxAction kind linkedin). Caps enforced at send, not draft: 20 connects / 50 likes / 50 DMs per account/day; acceptance breaker pauses connects under 25% acceptance over ≥10 invites in 7 days. Webhooks only, never polling (POST /unipile/webhook, POST /unipile/hosted-auth). Frontend UI is behind LINKEDIN_CHANNEL_UI = false in views/settings/ai/agents/AgentK/index.vue.

Provider usage → billing (backend meters, ms-ai prices)

  • trackProviderUsage() (app/lib/providerUsage.js) — fire-and-forget $inc on provider_usage per (company, provider, day).
  • AgentKUsageEmitJob (hourly) forwards closed-day rows (day < today, emitted != true, company_id != 'platform' — the shared Apify SoS pull is never billed) to ms-ai POST /api/cost-tracking/internal/provider-usage, idempotent on external_id agentk:<co>:<provider>:<day>; the emitted watermark makes retries safe.
  • ms-ai prices via src/cost-tracking/agent-k-provider-pricing.ts (USD/call: apollo_match 0.05, predictleads 0.02, unipile 0.01, apollo_search free; JSON env override AGENT_K_PROVIDER_USD, no deploy) into a usage_charge. Those credits flow into the existing token-usage sum and the hourly AITokensCreditJob deducts them — never reinvent pricing in the backend. Reconciliation/statements: ms-ai cost-tracking admin endpoints (see Cost Tracking).

Learning & steering

  • Skip-demotion (ms-ai/src/agent-k/agent-k-learning.service.ts): a signal_type with ≥5 outcomes and ≥60% skips is demoted — unless it ever converted (reply/booking). Demote-only; never adds volume. Fed by GET /internal/knox/agent-k/signal-stats.
  • knox_agent_k_weights Nox tool (ms-ai) → POST /internal/knox/actions/agent-k-weightsai_agent_k.source_weight_overrides (multiplier clamped 0.25–3) consumed by AgentKSourceWeightService — rank-only, never volume.
  • AgentKWeeklyDigestJob (hourly tick; sends on ai_agent_k.digest.day in company tz, once per ISO week via digest_last_sent_week), agency-branded; empty weeks send nothing.

Flags & config

Super-admin company.agent_k_beta_enabled outranks tenant ai_agent_k.enabled; both are required by every job and by NoxActionUserController._resolveAgentKCompany (403 otherwise). Toggle: PUT /company/:_id/agent-k-beta. Tenant config under ai_agent_k.*: icp_description/icp_filter (parsed by ms-ai "Let Agent K read this"), watch_for.*, re_engage.*, skip_self_reachouts, daily_limit, ghl_fields, manyreach, template.*, channels.*, sending_mode, source_weight_overrides.

Endpoints & surfaces

  • Tenant (app/routes/noxActionRoutes.js): /knox/agent-k/parse-icp, suggest-reply, preview-message, lifecycle-summary, dormant-count, linkedin/{connect-link,disconnect}, kk-backfill, campaign-tokens, manyreach/*, reactivate; plus the shared /knox/actions/* review queue.
  • Internal ms-ai → backend (internalKnoxRoutes.js): POST /internal/knox/agent-k/push, GET …/agent-k/signal-stats, GET …/provider-usage, POST …/actions/agent-k-weights.
  • ms-ai (src/agent-k/agent-k.controller.ts): POST /internal/knox/agent-k/{inbound,suggest-reply,draft-reengage,draft-outbound,preview-message,parse-icp}.
  • Frontend: config page views/settings/ai/agents/AgentK/index.vue (+ AgentKCard.vue, onboarding AgentKStep.vue); beta gate picks the command-center dashboard (views/Dashboard/Index.vue); admin toggle in views/admin/companies/Detail.vue.

Living plan: AGENT_K_PLAN.md (repo root, untracked) — code is authoritative where they disagree (tag prefix, field-id cache location, retired email drafting).