Contact Segmentation
Quick Access: Create targeted contact segments using behavioral data, demographics, and custom criteria for precise campaign targeting.
Overview
Contact Segmentation enables you to divide your contact database into meaningful groups based on shared characteristics, behaviors, or custom criteria. Send the right message to the right people at the right time.
Key Capabilities
-
Dynamic Segments: Auto-updating lists based on rules
-
Static Segments: Manual, fixed contact lists
-
Multi-Criteria Filtering: Combine multiple conditions
-
Behavioral Segmentation: Based on email engagement
-
Import/Export: Bulk segment management
-
Campaign Integration: Direct segment-to-campaign assignment
Level 1: Quick Start Guide
Your First Segment
Step 1: Create Segment
Contacts → Segments → Create New Segment
Segment Name: Active Subscribers
Description: Contacts who opened emails in last 30 days
Type: ○ Dynamic ○ Static
Step 2: Define Rules (Dynamic Segment)
Conditions (Match ALL):
☑ Last Email Opened: within last 30 days
☑ Subscription Status: Active
☑ Unsubscribed: No
Preview: 2,847 contacts match
Step 3: Save & Use
[Save Segment]
✓ Segment created: "Active Subscribers"
2,847 contacts
Quick Actions:
[Create Campaign] [Export List] [View Contacts]
Common Segment Types
By Engagement Level
Highly Engaged:
Conditions:
- Opened emails: ≥ 5 in last 30 days
- Clicked links: ≥ 2 in last 30 days
- Last activity: Within 7 days
Inactive Contacts:
Conditions:
- Last opened: More than 90 days ago
- Subscription status: Active
- Unsubscribed: No
Never Engaged:
Conditions:
- Total opens: 0
- Total clicks: 0
- Date added: More than 30 days ago
By Demographics
Location-Based:
Conditions:
- Country: United States
- State: California
OR
- City: San Francisco
Company Size:
Conditions:
- Custom Field: company_size
- Value: 51-200 employees
Industry:
Conditions:
- Custom Field: industry
- Value: SaaS, Technology, Software
By Lifecycle Stage
New Leads (Last 7 Days):
Conditions:
- Date added: Within last 7 days
- Emails sent: 0
Trial Users:
Conditions:
- Custom Field: account_type = "trial"
- Custom Field: trial_end_date: Within next 7 days
Customers:
Conditions:
- Custom Field: account_type = "paid"
- Custom Field: last_purchase_date: Within last 90 days
Static Segments
When to Use:
-
One-time campaigns (event invitations, announcements)
-
Manual contact selection
-
Imported lists from external sources
-
A/B test control groups
Creating Static Segment:
1. Select contacts manually from list
2. Or import CSV file
3. Name segment
4. Save as static segment (won't auto-update)
Level 2: Advanced Segmentation
Complex Rule Logic
AND/OR Combinations
Engaged OR High-Value:
Match ANY of these groups:
Group 1 (Engaged):
- Opens: ≥ 3 in last 30 days
- Clicks: ≥ 1 in last 30 days
Group 2 (High-Value):
- Custom Field: lead_score ≥ 75
- Custom Field: customer_ltv ≥ $1,000
Advanced Multi-Condition:
Match ALL:
Group 1 (Location):
- Country: United States OR Canada
- Timezone: PST, MST, CST, EST
Group 2 (Engagement):
- Last opened: Within 60 days
- Total clicks: ≥ 5
Group 3 (Not):
- Tag: does NOT include "unqualified"
- Custom Field: bounced ≠true
Behavioral Segmentation
Email Engagement Patterns
Click-But-Never-Convert:
Conditions:
- Total clicks: ≥ 10
- Total conversions: 0
- Last clicked: Within 30 days
Opens Mobile vs Desktop:
Conditions:
- Last opened device: Mobile
- Open count (mobile): ≥ 5
- Open count (desktop): 0
Time-Based Engagement:
Conditions:
- Most common open time: 9am-12pm
- Preferred day: Monday-Friday
Campaign-Specific Behavior
Clicked Specific Link:
Conditions:
- Campaign: "Product Launch Q4"
- Clicked URL: contains "pricing"
- Did NOT click URL: contains "demo"
Opened But Didn’t Click:
Conditions:
- Campaign: "Feature Announcement"
- Opened: Yes
- Clicked any link: No
- Days since sent: ≥ 3
Lead Score Integration
Score-Based Segments:
Hot Leads (Score 80-100):
- Lead Score: 80-100
- Last activity: Within 14 days
- Opened last email: Yes
Warm Leads (Score 50-79):
- Lead Score: 50-79
- Last activity: Within 30 days
Cold Leads (Score 0-49):
- Lead Score: 0-49
OR
- Last activity: More than 60 days ago
Custom Field Segmentation
Multi-Select Custom Fields:
Conditions:
- Custom Field: interests
- Contains ANY: "email marketing", "automation", "analytics"
Date-Based Custom Fields:
Trial Expiring Soon:
- Custom Field: trial_end_date
- Is between: today and 7 days from now
Anniversary Outreach:
- Custom Field: signup_date
- Anniversary: this month
Numeric Range Custom Fields:
High-Value Cohort:
- Custom Field: total_revenue
- Is greater than: $5,000
- Custom Field: lifetime_opens
- Is greater than: 50
Segment Exclusions
Exclude Other Segments:
Include:
- Segment: "All Active Subscribers"
Exclude:
- Segment: "Recent Purchasers"
- Segment: "Unengaged (90+ days)"
Suppress Based on Recent Activity:
Include:
- All contacts in "Product Users"
Exclude:
- Received campaign: Within last 7 days
- Campaign name: contains "Product"
Segment Performance Tracking
View Segment Analytics:
Segment: "Active Subscribers" (2,847 contacts)
Performance (Last 30 Days):
Average Open Rate: 32%
Average Click Rate: 8%
Unsubscribe Rate: 0.2%
Revenue Generated: $12,450
Trend:
- Segment size: +12% month-over-month
- Engagement: +5% vs all contacts
Segment A/B Testing
Test Segment Criteria:
Hypothesis: "High engagement = Lead Score > 60"
Test Setup:
Segment A: Lead Score > 60 (500 contacts)
Segment B: Lead Score 40-60 (500 contacts)
Send same campaign to both
Compare: Open rate, Click rate, Conversion rate
Result: Segment A 2.3x higher conversion
Action: Adjust scoring model
Level 3: Technical Implementation
Database Schema
-- Segments table
CREATE TABLE segments (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
workspace_id UUID REFERENCES workspaces(id),
name VARCHAR(255) NOT NULL,
description TEXT,
-- Segment type
segment_type VARCHAR(50), -- dynamic, static
-- Rules (for dynamic segments)
rules JSONB, -- Filtering criteria
-- Metadata
contact_count INTEGER DEFAULT 0,
last_recalculated_at TIMESTAMP,
-- Status
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_segments_tenant ON segments(tenant_id);
CREATE INDEX idx_segments_workspace ON segments(workspace_id);
CREATE INDEX idx_segments_type ON segments(segment_type);
-- Segment membership (for static segments or cached dynamic)
CREATE TABLE segment_contacts (
id UUID PRIMARY KEY,
segment_id UUID NOT NULL REFERENCES segments(id),
contact_id UUID NOT NULL REFERENCES contacts(id),
-- For dynamic segments, track when added
added_at TIMESTAMP DEFAULT NOW(),
added_via VARCHAR(50), -- rule_match, manual, import
UNIQUE(segment_id, contact_id)
);
CREATE INDEX idx_segment_contacts_segment ON segment_contacts(segment_id);
CREATE INDEX idx_segment_contacts_contact ON segment_contacts(contact_id);
-- Segment performance tracking
CREATE TABLE segment_analytics (
id UUID PRIMARY KEY,
segment_id UUID NOT NULL REFERENCES segments(id),
-- Timeframe
date DATE NOT NULL,
-- Metrics
contact_count INTEGER,
emails_sent INTEGER DEFAULT 0,
emails_delivered INTEGER DEFAULT 0,
opens INTEGER DEFAULT 0,
clicks INTEGER DEFAULT 0,
conversions INTEGER DEFAULT 0,
revenue DECIMAL(10,2) DEFAULT 0,
-- Rates
open_rate DECIMAL(5,2),
click_rate DECIMAL(5,2),
conversion_rate DECIMAL(5,2),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(segment_id, date)
);
CREATE INDEX idx_segment_analytics_segment_date ON segment_analytics(segment_id, date);
Segment Rules Engine
interface SegmentRule {
field: string; // contact.email, contact.created_at, customField.industry, etc.
operator: 'equals' | 'not_equals' | 'contains' | 'greater_than' | 'less_than' | 'between' | 'in' | 'not_in';
value: any;
dataType: 'string' | 'number' | 'date' | 'boolean' | 'array';
}
interface SegmentConditionGroup {
match: 'all' | 'any'; // AND or OR
conditions: (SegmentRule | SegmentConditionGroup)[];
}
interface SegmentDefinition {
match: 'all' | 'any';
groups: SegmentConditionGroup[];
exclusions?: SegmentConditionGroup[];
}
class SegmentEngine {
async evaluateSegment(segmentId: string): Promise<Contact[]> {
const segment = await db.segments.findById(segmentId);
if (segment.segmentType === 'static') {
return this.getStaticContacts(segmentId);
}
// Dynamic segment - evaluate rules
const query = this.buildQuery(segment.rules);
const contacts = await db.contacts.findAll(query);
// Update cached membership
await this.updateSegmentMembership(segmentId, contacts);
return contacts;
}
private buildQuery(rules: SegmentDefinition): any {
const conditions = this.buildConditions(rules.groups, rules.match);
let query: any = {
where: conditions,
};
// Apply exclusions
if (rules.exclusions) {
const exclusionConditions = this.buildConditions(
rules.exclusions.groups,
rules.exclusions.match
);
query.where = {
[Op.and]: [
conditions,
{ [Op.not]: exclusionConditions },
],
};
}
return query;
}
private buildConditions(
groups: SegmentConditionGroup[],
match: 'all' | 'any'
): any {
const operator = match === 'all' ? Op.and : Op.or;
const groupConditions = groups.map(group => {
return this.buildGroupConditions(group);
});
return { [operator]: groupConditions };
}
private buildGroupConditions(group: SegmentConditionGroup): any {
const operator = group.match === 'all' ? Op.and : Op.or;
const conditions = group.conditions.map(condition => {
if ('field' in condition) {
return this.buildRuleCondition(condition as SegmentRule);
} else {
return this.buildGroupConditions(condition as SegmentConditionGroup);
}
});
return { [operator]: conditions };
}
private buildRuleCondition(rule: SegmentRule): any {
const { field, operator, value } = rule;
// Parse field path (e.g., "contact.email" or "customField.industry")
const [entity, property] = field.split('.');
let condition: any = {};
if (entity === 'customField') {
// Query JSONB custom fields
condition = {
[`customFields.${property}`]: this.getOperatorCondition(operator, value),
};
} else {
// Standard field
condition = {
[property]: this.getOperatorCondition(operator, value),
};
}
return condition;
}
private getOperatorCondition(operator: string, value: any): any {
switch (operator) {
case 'equals':
return value;
case 'not_equals':
return { [Op.ne]: value };
case 'contains':
return { [Op.iLike]: `%${value}%` };
case 'greater_than':
return { [Op.gt]: value };
case 'less_than':
return { [Op.lt]: value };
case 'between':
return { [Op.between]: value }; // value = [min, max]
case 'in':
return { [Op.in]: value }; // value = array
case 'not_in':
return { [Op.notIn]: value };
default:
throw new Error(`Unknown operator: ${operator}`);
}
}
private async updateSegmentMembership(
segmentId: string,
contacts: Contact[]
): Promise<void> {
// Remove old memberships
await db.segmentContacts.destroy({
where: { segmentId },
});
// Add new memberships
await db.segmentContacts.bulkCreate(
contacts.map(contact => ({
segmentId,
contactId: contact.id,
addedVia: 'rule_match',
}))
);
// Update contact count
await db.segments.update(segmentId, {
contactCount: contacts.length,
lastRecalculatedAt: new Date(),
});
}
}
Background Jobs
// Recalculate dynamic segments
cron.schedule('*/30 * * * *', async () => { // Every 30 minutes
const dynamicSegments = await db.segments.findAll({
where: {
segmentType: 'dynamic',
isActive: true,
},
});
for (const segment of dynamicSegments) {
await segmentQueue.add('recalculate-segment', {
segmentId: segment.id,
});
}
});
// Queue worker
segmentQueue.process('recalculate-segment', async (job) => {
const { segmentId } = job.data;
const engine = new SegmentEngine();
await engine.evaluateSegment(segmentId);
});
// Calculate segment analytics daily
cron.schedule('0 2 * * *', async () => { // 2 AM daily
const segments = await db.segments.findAll({ where: { isActive: true } });
for (const segment of segments) {
await calculateSegmentAnalytics(segment.id);
}
});
API Endpoints
// Create segment
app.post('/api/segments', authenticate, async (req, res) => {
const { name, description, segmentType, rules, contactIds } = req.body;
const segment = await db.segments.create({
tenantId: req.user.tenantId,
workspaceId: req.body.workspaceId,
name,
description,
segmentType,
rules: segmentType === 'dynamic' ? rules : null,
createdBy: req.user.id,
});
// For static segments, add contacts
if (segmentType === 'static' && contactIds) {
await db.segmentContacts.bulkCreate(
contactIds.map(contactId => ({
segmentId: segment.id,
contactId,
addedVia: 'manual',
}))
);
await db.segments.update(segment.id, {
contactCount: contactIds.length,
});
}
// For dynamic segments, trigger calculation
if (segmentType === 'dynamic') {
await segmentQueue.add('recalculate-segment', {
segmentId: segment.id,
});
}
return res.json(segment);
});
// Get segment contacts
app.get('/api/segments/:id/contacts', authenticate, async (req, res) => {
const segment = await db.segments.findById(req.params.id);
if (segment.tenantId !== req.user.tenantId) {
return res.status(403).json({ error: 'Forbidden' });
}
const contacts = await db.contacts.findAll({
include: [{
model: db.segmentContacts,
where: { segmentId: req.params.id },
}],
offset: req.query.offset || 0,
limit: req.query.limit || 50,
});
return res.json({
segment,
contacts,
total: segment.contactCount,
});
});
Related Documentation
-
Leads Management - Contact database and management
-
Lead Scoring - Behavioral scoring system
-
Import/Export - Bulk contact operations
-
Campaign Management - Use segments in campaigns
-
Personalization System - Segment-specific content
Last Updated: November 25, 2025 Status: Planned - High Priority (Level 2) Target Release: Q1 2026 Owner: Leads Team