Seat Growth Signal Detection
Detect team growth signals — invitations sent, collaboration spikes, new user additions — and score accounts for seat expansion readiness
npx gtm-skills add drill/seat-growth-signal-detectionWhat this drill teaches
Seat Growth Signal Detection
This drill builds the detection layer that identifies accounts whose team dynamics signal they are ready to add seats. Unlike generic upgrade prompts, this operates on collaboration-specific signals — the behavioral patterns that precede natural team expansion.
Input
- PostHog tracking active with per-user and per-account event data (minimum 14 days)
- Events capturing team-related actions: invitations, shared resources, @mentions, collaborative edits, permission changes
- n8n instance for scheduled detection runs
- Attio CRM for storing expansion readiness scores
Steps
1. Define seat growth signals
Using the posthog-custom-events fundamental, identify and instrument the events that predict seat expansion. Good signals fall into three categories:
Direct signals (someone is actively trying to add users):
team_invite_sent— user sent an invitation to a colleagueteam_invite_failed— invitation blocked by seat limitseat_limit_hit— user attempted an action requiring more seatsadmin_viewed_billing— admin visited the billing or seats pageteam_settings_viewed— user browsed team management settings
Collaboration signals (usage patterns that outgrow current seats):
resource_shared_external— user shared a link or resource with someone not on the accountcollaborative_edit_attempted— user tried to collaborate on an assetmention_non_member— user @mentioned someone who is not on the teamexport_for_colleague— user exported data in a format suggesting they are sharing with others manually
Growth signals (the account is scaling):
project_count_increased— account crossed a project count threshold (e.g., 5, 10, 20)api_key_created— new API key suggests new integrations or team membersworkspace_created— new workspace or environment suggests departmental expansionusage_volume_spike— weekly active usage jumped 50%+ compared to 4-week average
Instrument each event with these properties:
posthog.capture('team_invite_sent', {
account_id: accountId,
inviter_user_id: userId,
current_seat_count: currentSeats,
seat_limit: planSeatLimit,
seats_remaining: planSeatLimit - currentSeats,
invite_target_domain: targetEmailDomain // same company domain = strong signal
});
2. Build a seat expansion readiness score
Each signal carries a different weight. Compute a per-account seat_expansion_score using this model:
| Signal | Points | Decay |
|--------|--------|-------|
| team_invite_failed (seat limit hit) | 30 | 14 days |
| seat_limit_hit | 25 | 14 days |
| team_invite_sent (same domain) | 20 | 7 days |
| admin_viewed_billing | 15 | 7 days |
| mention_non_member | 10 | 7 days |
| resource_shared_external | 8 | 14 days |
| usage_volume_spike | 8 | 14 days |
| project_count_increased | 5 | 30 days |
| workspace_created | 5 | 30 days |
Points decay over time — a team_invite_sent from 6 days ago is worth 20 points, but one from 10 days ago is worth ~7 points (linear decay). This prevents stale signals from triggering prompts.
Run a HogQL query to compute the score:
SELECT
properties.$group_0 AS account_id,
sum(
CASE event
WHEN 'team_invite_failed' THEN 30 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 14)
WHEN 'seat_limit_hit' THEN 25 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 14)
WHEN 'team_invite_sent' THEN 20 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 7)
WHEN 'admin_viewed_billing' THEN 15 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 7)
WHEN 'mention_non_member' THEN 10 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 7)
WHEN 'resource_shared_external' THEN 8 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 14)
WHEN 'usage_volume_spike' THEN 8 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 14)
WHEN 'project_count_increased' THEN 5 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 30)
WHEN 'workspace_created' THEN 5 * greatest(0, 1 - dateDiff('day', timestamp, now()) / 30)
ELSE 0
END
) AS seat_expansion_score
FROM events
WHERE event IN (
'team_invite_failed', 'seat_limit_hit', 'team_invite_sent',
'admin_viewed_billing', 'mention_non_member', 'resource_shared_external',
'usage_volume_spike', 'project_count_increased', 'workspace_created'
)
AND timestamp > now() - interval 30 day
GROUP BY account_id
HAVING seat_expansion_score > 15
ORDER BY seat_expansion_score DESC
3. Classify accounts into expansion tiers
Using posthog-cohorts, create three dynamic cohorts:
- Hot (score >= 40): Multiple strong signals in the last week. This account is actively trying to grow and hitting limits. Prompt immediately.
- Warm (score 20-39): Collaboration patterns suggest growth. Time a prompt when they next show a direct signal.
- Watch (score 15-19): Early indicators. Do not prompt yet. Monitor for score increase.
4. Store expansion data in CRM
Using the attio-custom-attributes fundamental, add these fields to the company record in Attio:
seat_expansion_score: numeric score from the modelseat_expansion_tier: hot | warm | watch | nonecurrent_seat_count: how many seats are in useseat_limit: plan seat limitseat_utilization_pct: current_seat_count / seat_limit * 100last_expansion_signal: timestamp of most recent qualifying signalexpansion_signal_count_30d: total qualifying signals in last 30 days
Using attio-lists, maintain a list called "Seat Expansion — Ready" that auto-populates with all hot and warm accounts.
5. Build the scheduled detection workflow
Using n8n-scheduling, create a workflow that runs every 6 hours:
- Query PostHog for all accounts with seat_expansion_score > 15 (Step 2 query)
- Classify into tiers (Step 3 logic)
- Update Attio records with current expansion data (Step 4)
- For accounts that moved into the hot tier since last run, fire a webhook to trigger the
seat-expansion-prompt-deliverydrill - For accounts whose score dropped below 15, clear the tier and log the cooldown
Using n8n-triggers, add a webhook endpoint so the product can request an on-demand score recalculation for a specific account (useful when a user hits a seat limit in real time).
6. Track detection accuracy
Using posthog-custom-events, log every detection event:
posthog.capture('seat_expansion_signal_detected', {
account_id: accountId,
expansion_tier: 'hot',
score: 52,
current_seats: 4,
seat_limit: 5,
top_signal: 'team_invite_failed'
});
After 30 days, measure: of accounts flagged as "hot," what percentage actually added seats within 14 days? Target: 35%+ of hot flags should convert. If false positive rate exceeds 65%, tighten the scoring weights or add exclusion rules (e.g., exclude accounts that just removed a seat).
Output
- Per-account seat expansion readiness scoring running every 6 hours in n8n
- Three PostHog cohorts (hot, warm, watch) updated automatically
- Attio records enriched with expansion score and seat utilization data
- Webhook trigger for downstream prompt delivery
- Detection accuracy tracking via PostHog events
Triggers
Runs every 6 hours via n8n cron. On-demand via webhook for real-time seat-limit events. Recalibrate scoring weights monthly based on actual conversion data.