GTM

Lead scoring, ABM, outreach sequences, intent, expansion playbooks

Aexy's GTM module is a full sales-and-marketing stack: lead scoring, account-based marketing, outreach sequences, intent signals, routing & SLAs, customer-health monitoring, expansion playbooks, competitor intelligence, SEO/content analysis, compliance, alerts, webhooks. 19 sub-routers aggregated under one parent prefix; 15 dedicated services; 13 model files; 24+ Temporal activities.

Most everything sits at /api/v1/workspaces/{workspace_id}/gtm/..., aggregated via backend/src/aexy/api/gtm/__init__.py:28-51. Tag: GTM.

Sub-router quick reference#

#Sub-routerPurpose
1providersConfigure data/enrichment providers (Apollo, ZoomInfo, etc.)
2dashboardKPI overview, funnel, recent visitors, pipeline metrics
3icpIdeal Customer Profile templates
4visitorsVisitor session tracking + identification
5complianceConsent, suppression, audit
6scoringLead score CRUD + rescoring
7dedupDuplicate detection + merge
8sequencesMulti-step outreach campaigns
9analyticsPipeline, channels, attribution, trends
10alertsEvent-driven alert configs + logs
11routingLead routing rules + SLA tracking
12healthCustomer-health scoring
13expansionUpsell/cross-sell playbooks
14handoffsCS → Sales handoffs
15intentBuying-signal monitoring
16competitorsCompetitive intel + battle cards
17seoSEO audits + content-gap analysis
18abmTarget lists, tiering, stage progression
19webhooksSubscribe external systems to GTM events

Throughout: path snippets are relative to /workspaces/{workspace_id}/gtm. Admin-marked endpoints require can_manage_gtm or equivalent permission.


1. Providers#

File: api/gtm/providers.py. Service: GTMProviderService.

Configures data/enrichment providers (slot-based — one provider per slot at a time).

GET    /providers/available                       all registered providers
GET    /providers                                 configured providers for this workspace
POST   /providers                                 configure a new provider
GET    /providers/{slot}/{provider_name}
PUT    /providers/{slot}/{provider_name}
DELETE /providers/{slot}/{provider_name}

Schemas: GTMProviderConfigCreate, GTMProviderConfigResponse, GTMAvailableProvider (schemas/gtm).

2. Dashboard#

File: api/gtm/dashboard.py. Service: GTMDashboardService.

GET /dashboard/overview?days=30                   KPI overview
GET /dashboard/funnel                             stage-by-stage funnel data
GET /dashboard/recent-visitors?limit=10           recently identified visitors
GET /dashboard/pipeline-metrics                   scoring / routing / outreach / provider-health rollup

Returns aggregated metrics for the home page — read-only, no LLM.

3. ICP Templates#

File: api/gtm/icp.py. Service: ICPTemplateService.

GET    /icp-templates
POST   /icp-templates                             admin
GET    /icp-templates/{template_id}
PUT    /icp-templates/{template_id}               admin
DELETE /icp-templates/{template_id}               admin

Templates define the workspace's notion of a "good fit" account — industry, employee range, revenue, tech stack signals. Used by scoring and ABM.

4. Visitors#

File: api/gtm/visitors.py. Service: VisitorService.

GET  /visitors?page=...&status=...&utm_source=...&search=...
GET  /visitors/{session_id}                       detail with behavioral events
POST /visitors/{session_id}/identify              manual identify or link
POST /visitors/{session_id}/link                  link to existing record

Sessions arrive via the public event-ingestion SDK; identify_visitor_session (Temporal, 2m timeout) matches them to known accounts. Behavioral events (page views, form fills, CTAs) live alongside.

5. Compliance#

File: api/gtm/compliance.py. Service: GTMComplianceService.

GET    /compliance/check?email=...                "am I allowed to send to this address?"
POST   /compliance/consent                        record consent
GET    /compliance/consent/{email}
DELETE /compliance/consent/{email}                revoke
POST   /compliance/suppression                    add to suppression list
GET    /compliance/audit                          consent + suppression audit log

The single source of truth for "can we email this person?" — every outreach send queries /compliance/check first. GDPR-aligned: consent records carry timestamp, source, version.

6. Lead scoring#

File: api/gtm/scoring.py. Service: GTMScoringService.

GET  /scoring/overview
GET  /scoring/leads?min_score=...&max_score=...&lifecycle_stage=...&sort=...
GET  /scoring/leads/{record_id}                   score detail with factor breakdown
POST /scoring/rescore/{record_id}                 manual rescore

Scoring runs as the score_lead activity (5m timeout) for individual records; bulk re-score uses batch_score_leads (30m + 5m heartbeat). Scores attach to CRM records as JSONB and feed routing, alerts, and the dashboard funnel.

7. Deduplication#

File: api/gtm/dedup.py. Service: DedupService.

GET  /dedup/scan?limit=...&record_id=...          find duplicates
POST /dedup/merge                                 merge two records (admin)
GET  /dedup/stats                                 dedup statistics

Match candidates returned with confidence + reason. Merge is destructive — the source row is archived, references rewritten.

8. Outreach sequences#

File: api/gtm/sequences.py. Service: OutreachSequenceService.

POST   /sequences                                 admin
GET    /sequences?status=...                      paginated
GET    /sequences/{sequence_id}
PUT    /sequences/{sequence_id}                   admin
DELETE /sequences/{sequence_id}                   admin
POST   /sequences/{sequence_id}/enroll            single contact
POST   /sequences/{sequence_id}/bulk-enroll
POST   /sequences/{sequence_id}/reply-classify    LLM reply classification

Sequence steps are typed (SequenceAction enum): send_email, linkedin_view, linkedin_connect, linkedin_message, send_sms, wait. Each step execution is execute_outreach_step (5m). Reply classification runs classify_outreach_reply (LLM_RETRY, 2m).

SequenceStatus: draft/active/paused/archived. EnrollmentStatus: active/paused/completed/replied/bounced/unsubscribed/exited/failed. StepExecutionStatus: pending/sent/delivered/opened/clicked/replied/bounced/failed/skipped.

For the distinction between this and CRMSequence, see crm.md.

9. Analytics#

File: api/gtm/analytics.py. Service: GTMAnalyticsService.

GET /analytics/pipeline                           lifecycle stage distribution + conversions
GET /analytics/channels                           email / LinkedIn / SMS metrics
GET /analytics/attribution?model=...              first_touch / last_touch / linear / u_shaped / time_decay
GET /analytics/sequences                          sequence performance comparison
GET /analytics/trends?period=...                  time-series

All five attribution models supported. No LLM.

10. Alerts#

File: api/gtm/alerts.py. Service: GTMAlertService.

GET    /alerts/configs
POST   /alerts/configs                            admin
PUT    /alerts/configs/{alert_id}                 admin
DELETE /alerts/configs/{alert_id}                 admin
GET    /alerts/logs                               delivery history

Alerts trigger on events (visitor identified, score crossed threshold, deal stage changed, …) with optional conditions (JSONB AND/OR). Channel: slack / email / webhook. Delivery dispatches send_gtm_alert (2m).

11. Routing & SLA#

File: api/gtm/routing.py. Service: LeadRoutingService.

GET    /routing/rules
POST   /routing/rules                             admin
PUT    /routing/rules/{rule_id}                   admin
DELETE /routing/rules/{rule_id}                   admin
POST   /routing/route/{record_id}                 apply rules → assign
GET    /routing/assignments?status=...
POST   /routing/assignments/{assignment_id}/reassign
GET    /routing/sla-dashboard                     breach overview

Strategies: round_robin, availability, custom. SLA tracked per assignment via sla_first_response_minutes and sla_follow_up_minutes. check_sla_breaches (5m, scheduled) flags violations.

12. Customer health#

File: api/gtm/health.py. Service: HealthScoringService.

GET  /health/dashboard                            tier distribution + at-risk
GET  /health/scores?health_status=...
GET  /health/scores/{record_id}
POST /health/scores/{record_id}/rescore
POST /health/batch-score                          batch

Score is a weighted sum of five sub-scores: engagement (default 25%), usage (30%), support (20%), NPS (15%), payment (10%). Thresholds: healthy ≥70, at_risk 40-69, critical <20. Trend is derived from score_history JSONB.

Individual scoring: score_customer_health (5m). Bulk: batch_score_customer_health (30m + 5m heartbeat). Drops: detect_health_drops (5m, scheduled).

13. Expansion playbooks#

File: api/gtm/expansion.py. Service: ExpansionPlaybookService.

GET    /expansion/playbooks
POST   /expansion/playbooks                       admin
GET    /expansion/playbooks/{playbook_id}
PUT    /expansion/playbooks/{playbook_id}         admin
DELETE /expansion/playbooks/{playbook_id}         admin
POST   /expansion/playbooks/{playbook_id}/enroll  enroll an account
GET    /expansion/enrollments?status=...
POST   /expansion/enrollments/{enrollment_id}/outcome   converted/lost
GET    /expansion/analytics

Playbooks are step graphs — eligibility check, multi-channel touches, outcome reporting. Triggered by evaluate_expansion_triggers (5m); steps advance via advance_expansion_step (2m).

14. Handoffs#

File: api/gtm/handoffs.py. Service: HandoffService.

POST /handoffs                                    create CS → Sales handoff
GET  /handoffs?status=...&assigned_to=...
GET  /handoffs/{handoff_id}
POST /handoffs/{handoff_id}/accept
POST /handoffs/{handoff_id}/decline
POST /handoffs/{handoff_id}/convert               → deal
GET  /handoffs/analytics                          acceptance + conversion rates

handoff_type: expansion / upsell / cross_sell. Status flow: pending → accepted/declined → in_progress → converted/lost. SLA on acceptance: sla_accept_minutes with sla_breached boolean.

15. Intent signals#

File: api/gtm/intent.py. Service: IntentSignalService.

GET    /intent/signals?signal_type=...&intent_strength=...
GET    /intent/signals/{signal_id}
POST   /intent/signals                            admin (manual insert)
POST   /intent/signals/{signal_id}/dismiss        admin
GET    /intent/config
PUT    /intent/config                             admin
GET    /intent/summary

Config holds monitored_domains, job_title_keywords, tech_keywords, competitor_names, plus per-signal-type signal_weights (JSONB). Strengths: weak/medium/strong.

Collection: collect_intent_signals (30m + 5m heartbeat, scheduled) pulls third-party data; match_intent_signals_to_records (10m) joins signals to known accounts.

16. Competitors#

File: api/gtm/competitors.py. Service: CompetitorIntelService.

GET    /competitors
POST   /competitors                                          admin
GET    /competitors/{competitor_id}
PUT    /competitors/{competitor_id}                          admin
DELETE /competitors/{competitor_id}                          admin
GET    /competitors/changes
POST   /competitors/changes/{change_id}/acknowledge          admin
GET    /competitors/{competitor_id}/battle-card              get or generate
PUT    /competitors/{competitor_id}/battle-card              admin

check_competitor_changes (30m + 5m heartbeat, scheduled) scrapes competitor pages for pricing/feature/messaging changes. generate_battle_card (LLM_RETRY, 10m) synthesizes a one-pager comparing your product vs the competitor.

17. SEO & content gap#

File: api/gtm/seo.py. Service: SEOAuditService / ContentAnalysisService.

# SEO audits
POST /seo/audits                                  admin → Temporal dispatch
GET  /seo/audits
GET  /seo/audits/{audit_id}
GET  /seo/audits/{audit_id}/pages
GET  /seo/audits/{audit_id}/history

# Content gap analysis
POST /content/analysis                            admin → Temporal dispatch
GET  /content/analysis
GET  /content/analysis/{analysis_id}

run_seo_audit (15m + 30s heartbeat) runs the audit. run_content_gap_analysis (LLM_RETRY, 30m + 5m heartbeat) compares your content footprint to keyword gaps vs competitors.

18. ABM (Account-Based Marketing)#

File: api/gtm/abm.py. Service: ABMService.

GET    /abm/lists
POST   /abm/lists                                            admin
GET    /abm/lists/{list_id}
PUT    /abm/lists/{list_id}                                  admin
DELETE /abm/lists/{list_id}                                  admin
GET    /abm/overview                                         total accounts + tier distribution + engagement
GET    /abm/accounts?target_list_id=...&tier=...&stage=...
GET    /abm/accounts/{account_id}                            contact + engagement metrics
POST   /abm/accounts/{account_id}/stage                      admin
POST   /abm/accounts/{account_id}/assign-campaign            admin

Lists can be is_dynamic=true — criteria-based, re-evaluated by refresh_dynamic_abm_lists (10m, scheduled). Account tiers: tier_1/tier_2/tier_3. Stages: unaware/aware/engaged/...

Engagement recompute: recalculate_abm_engagement (30m + 5m heartbeat).

19. Outbound webhooks#

File: api/gtm/webhooks.py. Service: GTMWebhookService.

GET    /webhooks?is_active=...
POST   /webhooks                                  with event subscriptions
GET    /webhooks/{webhook_id}
PUT    /webhooks/{webhook_id}
DELETE /webhooks/{webhook_id}
GET    /webhooks/{webhook_id}/deliveries
POST   /webhooks/{webhook_id}/test                send sample payload

Delivery uses the shared deliver_webhook activity with WEBHOOK_RETRY (6 attempts, 1m → 1h backoff). See webhooks.md for HMAC signing protocol.


Models (13 files)#

All under backend/src/aexy/models/gtm_*.py. Key shapes:

gtm_outreach.py#

ModelHighlights
OutreachSequencestatus, steps JSONB array, settings (send window)
OutreachEnrollmentrecord_id, sequence_id, status, lifecycle timestamps
OutreachStepExecutionstep_index, status, per-step metadata

Enums above (Sequences).

gtm_intent.py#

ModelHighlights
IntentSignalsignal_type, intent_strength, confidence_score, signal_data JSONB, is_dismissed
IntentSignalConfigmonitored_domains, job_title_keywords, tech_keywords, competitor_names, signal_weights JSONB

gtm_alerts.py#

ModelHighlights
GTMAlertConfigevent_type, conditions JSONB, channel_type, channel_config, message_template, is_active
GTMAlertLogdelivery_status, error_message, sent_at

gtm_routing.py#

ModelHighlights
GTMRoutingRulepriority, is_active, conditions JSONB, strategy, assignee_pool, sla_first_response_minutes, sla_follow_up_minutes
GTMLeadAssignmentrecord_id, assignee_id, assigned_at, first_response_at, sla_breached, sla_breach_at

gtm_health.py#

ModelHighlights
GTMHealthScoretotal_score, five sub-scores (0-100), health_status, trend, scoring_factors JSONB, score_history JSONB
GTMHealthConfigweights (defaults engagement 25, usage 30, support 20, NPS 15, payment 10), thresholds

gtm_abm.py#

ModelHighlights
ABMTargetListname, criteria JSONB (industries, employee/revenue ranges), is_dynamic, is_active, account_count
ABMAccounttier, stage, engagement_score, contact counts, activity metrics (emails, meetings, deals)

gtm_handoff.py#

ModelHighlights
GTMHandoffhandoff_type, title, context, estimated_value, products JSONB, signals JSONB, status, sla_accept_minutes, sla_breached

Plus#

  • gtm_webhook.py — webhook configs + deliveries
  • gtm_seo.py — audit results + per-page scores
  • gtm_competitor.py — competitor records + tracked changes
  • gtm_compliance.py — consent records + suppression lists
  • gtm_expansion.py — playbook templates + enrollments
  • gtm_content.py — content-gap analysis records

Services (15 files)#

Under backend/src/aexy/services/:

ServiceDomain
gtm_service.pyProvider config, dashboard aggregation, ICP CRUD
gtm_analytics_service.pyPipeline/channel/attribution/trends
gtm_alert_service.pyAlert configs, event matching, dispatch
gtm_compliance_service.pyConsent, suppression
gtm_webhook_service.pyWebhook CRUD + event publishing
lead_routing_service.pyRouting rules, assignment, SLA
health_scoring_service.pyScore computation, trend detection
intent_signal_service.pyIntent CRUD, signal-to-record matching
competitor_intel_service.pyCompetitor CRUD, change detection, battle cards
seo_audit_service.pySEO lifecycle, per-page scoring
outreach_sequence_service.pySequence CRUD, enrollment, step execution, reply classification
outreach_personalization_service.pyLLM personalization for outreach content
expansion_playbook_service.pyPlaybook CRUD, enrollment, stage advance
handoff_service.pyHandoff lifecycle
abm_service.pyTarget lists + account engagement

Temporal activities#

All registered in dispatch.py:109-164:

ActivityRetryTimeoutWhat
identify_visitor_sessionSTANDARD2mMatch visitor to known account
process_visitor_eventsSTANDARD5mBatch event ingestion
verify_email_addressSTANDARD2mEmail validation
score_leadSTANDARD5mSingle lead score
batch_score_leadsSTANDARD30m, 5m heartbeatBulk
execute_outreach_stepSTANDARD5mOne step
finalize_enrollmentSTANDARD2mWrap up enrollment
generate_weekly_gtm_reportSTANDARD10mWeekly digest
classify_outreach_replyLLM2mLLM reply triage
personalize_outreach_batchLLM30m, 5m heartbeatLLM personalization
run_bulk_importSTANDARD30m, 5m heartbeatLead/account import
send_gtm_alertSTANDARD2mAlert delivery
route_new_leadSTANDARD2mApply routing rules
check_sla_breachesSTANDARD5mSLA monitoring (scheduled)
score_customer_healthSTANDARD5mSingle account
batch_score_customer_healthSTANDARD30m, 5m heartbeatBulk
detect_health_dropsSTANDARD5mAt-risk detection
evaluate_expansion_triggersSTANDARD5mPlaybook eligibility
advance_expansion_stepSTANDARD2mStep execution
collect_intent_signalsSTANDARD30m, 5m heartbeatPull third-party intent
match_intent_signals_to_recordsSTANDARD10mJoin signals
check_competitor_changesSTANDARD30m, 5m heartbeatCompetitor scrape
generate_battle_cardLLM10mLLM battle card
run_seo_auditSTANDARD15m, 30s heartbeatSEO audit
run_content_gap_analysisLLM30m, 5m heartbeatContent gap
recalculate_abm_engagementSTANDARD30m, 5m heartbeatABM engagement refresh
refresh_dynamic_abm_listsSTANDARD10mDynamic list eval

Periodic schedules in temporal/schedules.py drive the scheduled ones (compete-changes, intent collect, SLA checks, health drops, expansion triggers, ABM refresh, etc.).

Frontend#

19 pages under frontend/src/app/(app)/gtm/:

RoutePurpose
/gtmDashboard / overview
/gtm/abmAccount-Based Marketing
/gtm/visitorsVisitor tracking & identification
/gtm/providersData provider config
/gtm/healthCustomer health scoring
/gtm/content-gapSEO + content gap
/gtm/complianceConsent + suppression management
/gtm/alertsAlert configs + logs
/gtm/expansionExpansion playbooks
/gtm/intentIntent signal monitoring
/gtm/sequencesOutreach sequence builder
/gtm/scoringLead scoring dashboard
/gtm/routingRouting rules + SLA dashboard
/gtm/seoSEO audit results
/gtm/importBulk import
/gtm/handoffsCS-to-Sales handoffs
/gtm/competitorsCompetitor intel + battle cards
/gtm/analyticsMulti-view analytics

Common pitfalls#

  • Two sequence systems (again — covered in crm.md but worth repeating): CRMSequence for internal nurture, GTM outreach sequence for outbound sales. Don't enroll a record in both — they'll race on email sends.
  • Compliance check is per-call, not cached. Every outreach send queries /compliance/check. If you're dispatching tens of thousands of sends, the compliance service is on the hot path — keep its DB queries indexed (consent_records.email, suppression_list.email).
  • Health-score weights vs thresholds. Weights sum to 100 across the five sub-scores; thresholds partition 0-100 into health tiers. Changing weights doesn't auto-rescore — kick off batch_score_customer_health after weight changes.
  • Dynamic ABM lists drift. Criteria-based lists are re-evaluated only when refresh_dynamic_abm_lists runs. If the user just added a tag to an account, it won't appear in the list until the next refresh.
  • Reply classifier failure mode: when LLM rate-limit kicks in, classification falls back to keyword heuristics with a higher false-positive rate. Watch the gtm.outreach.misclassified count if reply triage looks off.
  • Visitor identification confidence: identify_visitor_session returns confidence ∈ [0, 1]. Acting on low-confidence matches creates duplicate accounts; set a workspace-wide threshold and respect it.
  • Routing rule priority is honored in order — first match wins. Don't add a catch-all rule at priority 1; it'll mask everything else.
  • run_seo_audit heartbeats every 30s because third-party SERP APIs throttle aggressively. Don't loop audits or fan them out without coordinating with the rate limiter.
  • Intent signals are 6-24h stale. collect_intent_signals is batched — don't gate real-time routing on intent freshness; gate on score + visitor data instead.
  • Handoff conversion creates a deal. Calling /handoffs/{id}/convert mutates the CRM — you can't undo it via the handoff endpoints. Use deal-level operations after the fact.
  • Webhook secret rotation is destructive. No overlap window — the next delivery signs with the new secret. Coordinate with consumers.