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
| Feeder | Trigger | Files |
|---|---|---|
| Inbound KnockID | realtime on verified identify — PersonResolverService._fireAgentKInbound → ms-ai POST /internal/knox/agent-k/inbound | app/services/PersonResolverService.js, ms-ai/src/agent-k/ |
| Outbound net-new | AgentKOutboundJob (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_search → people/match reveal (1 credit, ≤15/scan) + PredictLeads signals | app/jobs/AgentKOutboundJob.js, app/services/AgentKOutboundService.js, app/lib/apifySos.js |
| CRM reactivation | AgentKReactivationJob (hourly; once/day per tenant), requires re_engage.revive_crm + GHL | app/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 tags → NoxActionModel 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_levelis a strict subdoc — cache keys must be schema'd or writes drop silently).kk_token/kk_why_noware fixed;linkedin_url/job_titlenames are tenant-configurable viaai_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>, plusnextlevel returned-to-sitewritten 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.resolvestitches the identity onto the session → writesnextlevel returned-to-site→ funnellink_click/web_visitsignals → auto-engage after 8s (once/session).- Backfill:
POST/GET /knox/agent-k/kk-backfillstampskk_tokenonto existing GHL contacts (requires the field to already exist; progress persisted atai_agent_k.kk_backfill, socketkk-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$inconprovider_usageper (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-aiPOST /api/cost-tracking/internal/provider-usage, idempotent onexternal_id agentk:<co>:<provider>:<day>; theemittedwatermark makes retries safe.- ms-ai prices via
src/cost-tracking/agent-k-provider-pricing.ts(USD/call:apollo_match0.05,predictleads0.02,unipile0.01,apollo_searchfree; JSON env overrideAGENT_K_PROVIDER_USD, no deploy) into ausage_charge. Those credits flow into the existingtoken-usagesum and the hourlyAITokensCreditJobdeducts 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): asignal_typewith ≥5 outcomes and ≥60% skips is demoted — unless it ever converted (reply/booking). Demote-only; never adds volume. Fed byGET /internal/knox/agent-k/signal-stats. knox_agent_k_weightsNox tool (ms-ai) →POST /internal/knox/actions/agent-k-weights→ai_agent_k.source_weight_overrides(multiplier clamped 0.25–3) consumed byAgentKSourceWeightService— rank-only, never volume.AgentKWeeklyDigestJob(hourly tick; sends onai_agent_k.digest.dayin company tz, once per ISO week viadigest_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, onboardingAgentKStep.vue); beta gate picks the command-center dashboard (views/Dashboard/Index.vue); admin toggle inviews/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).