Update wiki

This commit is contained in:
Dries Augustyns
2025-12-07 13:36:31 +01:00
parent 0003e44db8
commit 683356c17c
48 changed files with 633 additions and 8620 deletions
@@ -1,227 +0,0 @@
---
title: Cart Abandonment Recovery
description: Recover abandoned carts with automated emails
icon: ShoppingCart
---
## Overview
Send automated recovery emails when users add items to cart but don't complete purchase. Uses a two-email sequence with a discount incentive.
## Prerequisites
- Track `cart_abandoned` and `purchase_completed` events from your app
- Create two email templates in dashboard
## Track cart events
### Cart abandoned
When user adds items but leaves without purchasing:
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'cart_abandoned',
email: user.email,
data: {
cartTotal: cart.total,
cartUrl: `https://yourstore.com/cart/${cart.id}`,
itemCount: cart.items.length,
items: cart.items.map(i => ({
name: i.product.name,
price: i.price,
quantity: i.quantity,
imageUrl: i.product.imageUrl
}))
}
})
});
```
### Purchase completed
When user completes checkout:
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'purchase_completed',
email: user.email,
data: {
orderId: order.id,
total: order.total
}
})
});
```
## Build the workflow
### 1. Create workflow
Go to **Workflows** → **Create Workflow**
- **Name:** Cart Abandonment Recovery
- **Trigger:** Event - `cart_abandoned`
- **Allow re-entry:** Yes (users can abandon multiple times)
### 2. Add workflow steps
```
[Trigger: cart_abandoned]
[Delay: 1 hour]
[Send Email: Cart Reminder]
[Wait for Event: purchase_completed, timeout: 23 hours]
├─ Purchased → [Exit]
└─ Timeout → [Send Email: Cart Discount] → [Exit]
```
**Step-by-step:**
1. **Delay** (1 hour)
- Duration: 1
- Unit: Hours
2. **Send Email** (Cart Reminder)
- Template: Cart Reminder
- Variables: (auto-populated from event data)
3. **Wait for Event**
- Event: `purchase_completed`
- Timeout: 23 hours
- Connect two paths:
- **Event triggered** → Exit
- **Timeout** → Continue to discount
4. **Send Email** (Cart Discount)
- Template: Cart Discount
- Variables:
- All cart data from event
- `discountedTotal`: Calculate in template or pass from backend
5. **Exit**
### 3. Enable workflow
Toggle workflow to **ON**
## Test the workflow
### Manual test
1. Go to **Workflows** → Cart Abandonment Recovery → **Executions**
2. Click **Create Execution**
3. Select test contact
4. Provide test data:
```json
{
"cartTotal": 149.99,
"cartUrl": "https://yourstore.com/cart/test123",
"itemCount": 2,
"items": [
{
"name": "Product A",
"price": 79.99,
"quantity": 1,
"imageUrl": "https://cdn.example.com/product-a.jpg"
},
{
"name": "Product B",
"price": 69.99,
"quantity": 1,
"imageUrl": "https://cdn.example.com/product-b.jpg"
}
],
"discountedTotal": 134.99
}
```
5. **Start Execution**
Watch the execution run. For faster testing, temporarily set delay to 1 minute instead of 1 hour.
### Live test
1. Trigger `cart_abandoned` event with your email
2. Wait 1 hour (or 1 minute if testing with shorter delay)
3. Check for first email
4. Either:
- Complete purchase → workflow ends
- Wait 23 hours → receive discount email
## Improve conversion
### Add cart item images
Pass product images in event data and display in email. Visual reminders increase clicks.
### Personalize timing
Test different delays:
- First email: 30min, 1hr, 2hr
- Second email: 12hr, 24hr, 48hr
Monitor which timing drives best conversion.
### Increase discount incrementally
Second email could offer 10%, third email (if you add one) could offer 15%.
### Segment by cart value
Create separate workflows for high-value carts (e.g., >$200) with different messaging or larger discounts.
### Track discount usage
When user applies discount code, track event:
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'discount_applied',
email: user.email,
data: {
code: 'SAVE10',
source: 'cart_abandonment_email'
}
})
});
```
This lets you measure email-driven conversions.
## Common issues
**Email sends but cart is already purchased** — Add a condition before sending emails to check if purchase event already occurred.
**Cart URL expired** — Ensure cart sessions last at least 48 hours, or regenerate cart from saved items.
**Discount code doesn't work** — Verify code exists in your system before sending email. Auto-generate unique codes per user for better tracking.
**Too many emails** — Users abandoning multiple carts quickly will enter workflow multiple times. Consider adding a delay condition or rate limiting.
## Next steps
- [Track more events](/tutorials/event-tracking-integration) for behavior-based workflows
- [Build segments](/tutorials/segment-based-targeting) for high-value cart abandoners
- [Use conditions](/automation-patterns/conditional-branching) for cart value-based logic
@@ -1,475 +0,0 @@
---
title: Event Tracking Integration
description: Track user behavior to trigger workflows
icon: Activity
---
## Overview
Track events from your application to trigger workflows and update contact data. Events like `user_signed_up`, `purchase_completed`, `feature_used` can start automated email sequences.
## Get your public key
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
2. Copy your **Public Key** (starts with `pk_`)
Public keys are safe to use in client-side code.
## Basic event tracking
### JavaScript (client-side)
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'button_clicked',
email: user.email,
data: {
buttonName: 'Get Started',
page: '/pricing'
}
})
});
```
### Node.js (server-side)
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'user_signed_up',
email: user.email,
data: {
name: user.name,
plan: 'free',
signupDate: new Date().toISOString()
}
})
});
```
### Python
```python
import requests
import os
requests.post('{{API_URL}}/v1/track',
headers={
'Authorization': f'Bearer {os.environ["PLUNK_PUBLIC_KEY"]}',
'Content-Type': 'application/json'
},
json={
'event': 'purchase_completed',
'email': user.email,
'data': {
'orderId': order.id,
'total': order.total,
'items': order.items
}
}
)
```
## Common events to track
### User lifecycle
```javascript
// Signup
await trackEvent('user_signed_up', user.email, {
name: user.name,
source: 'google',
plan: 'free'
});
// Activation
await trackEvent('first_value_achieved', user.email, {
action: 'created_first_project',
timestamp: new Date().toISOString()
});
// Upgrade
await trackEvent('subscription_upgraded', user.email, {
fromPlan: 'free',
toPlan: 'premium',
mrr: 99
});
// Churn
await trackEvent('subscription_cancelled', user.email, {
reason: user.cancellationReason,
cancelledAt: new Date().toISOString()
});
```
### Product engagement
```javascript
// Feature usage
await trackEvent('feature_used', user.email, {
featureName: 'data_export',
timestamp: new Date().toISOString()
});
// Content interaction
await trackEvent('video_watched', user.email, {
videoId: 'intro-101',
duration: 300,
completed: true
});
// Settings changes
await trackEvent('settings_updated', user.email, {
setting: 'notifications',
value: 'enabled'
});
```
### E-commerce
```javascript
// Cart
await trackEvent('cart_abandoned', user.email, {
cartId: cart.id,
cartTotal: cart.total,
items: cart.items.map(i => i.name)
});
// Purchase
await trackEvent('purchase_completed', user.email, {
orderId: order.id,
total: order.total,
paymentMethod: 'credit_card'
});
// Review
await trackEvent('review_submitted', user.email, {
productId: product.id,
rating: 5
});
```
## Event naming conventions
**Use lowercase with underscores:**
- ✅ `user_signed_up`
- ✅ `purchase_completed`
- ❌ `UserSignedUp`
- ❌ `purchase-completed`
**Be specific:**
- ✅ `trial_started`
- ❌ `event`
**Use past tense:**
- ✅ `email_opened`
- ❌ `email_open`
## Event data best practices
**Keep data flat when possible:**
```javascript
// Good
{
name: 'John',
plan: 'premium',
mrr: 99
}
// Works but harder to use
{
user: {
profile: {
name: 'John'
}
}
}
```
**Use consistent types:**
```javascript
// Good - number for numeric values
{ total: 99.99 }
// Bad - string for numeric values
{ total: "99.99" }
```
**Use ISO dates:**
```javascript
// Good
{ signupDate: new Date().toISOString() }
// Okay but less flexible
{ signupDate: '2024-03-15' }
```
## Integrate with React
### Context provider
```javascript
// EventTrackingContext.js
import { createContext, useContext } from 'react';
const EventTrackingContext = createContext();
export function EventTrackingProvider({ children }) {
const trackEvent = async (event, data = {}) => {
const user = getCurrentUser(); // Your auth logic
if (!user?.email) return;
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event,
email: user.email,
data: {
name: user.name,
...data
}
})
});
};
return (
<EventTrackingContext.Provider value={{ trackEvent }}>
{children}
</EventTrackingContext.Provider>
);
}
export const useEventTracking = () => useContext(EventTrackingContext);
```
### Use in components
```javascript
import { useEventTracking } from './EventTrackingContext';
function UpgradeButton() {
const { trackEvent } = useEventTracking();
const handleUpgrade = async () => {
await upgradePlan('premium');
await trackEvent('plan_upgraded', {
plan: 'premium',
source: 'pricing_page'
});
};
return <button onClick={handleUpgrade}>Upgrade</button>;
}
```
## Integrate with Next.js
### Client component
```javascript
'use client';
import { trackEvent } from '@/lib/plunk';
export function SignupForm() {
const handleSubmit = async (data) => {
const user = await createUser(data);
// Track event
await trackEvent('user_signed_up', user.email, {
name: user.name,
source: 'homepage'
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
```
### Server action
```javascript
'use server';
import { trackEvent } from '@/lib/plunk';
export async function createProject(formData) {
const user = await getCurrentUser();
const project = await db.projects.create({
name: formData.get('name'),
userId: user.id
});
await trackEvent('project_created', user.email, {
projectId: project.id,
projectName: project.name
});
return project;
}
```
## Create a helper function
```javascript
// lib/plunk.js
const PLUNK_PUBLIC_KEY = process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY;
export async function trackEvent(event, email, data = {}) {
try {
const response = await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event,
email,
data
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to track event:', error);
// Don't throw - tracking shouldn't break your app
}
}
```
## Testing events
### View tracked events
1. Go to **Activity** in Plunk dashboard
2. Filter by event type
3. View event data payloads
### Test locally
```javascript
// Track a test event
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': 'Bearer pk_your_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'test_event',
email: 'your-email@example.com',
data: {
test: true,
timestamp: new Date().toISOString()
}
})
});
```
Check Plunk Activity - event should appear within seconds.
## Connect to workflows
Once events are tracked, create workflows that trigger on them:
1. **Workflows** → **Create Workflow**
2. Trigger: Event `user_signed_up`
3. Build your automation
4. Enable workflow
Now when you track `user_signed_up`, the workflow runs automatically.
## Performance considerations
**Don't block user actions:**
```javascript
// Good - fire and forget
handleClick() {
trackEvent('button_clicked', user.email);
// Don't await
}
// Bad - user waits for tracking
async handleClick() {
await trackEvent('button_clicked', user.email);
// User has to wait
}
```
**Batch events for bulk operations:**
```javascript
// If importing 1000 users, track events in background
async function importUsers(users) {
const imported = await db.users.bulkCreate(users);
// Queue for background processing
await queue.add('track-events', {
event: 'user_imported',
users: imported
});
}
```
**Add retry logic:**
```javascript
async function trackEventWithRetry(event, email, data, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await trackEvent(event, email, data);
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
```
## Common issues
**Event tracked but workflow not triggering**
- Workflow is enabled
- Event name matches exactly (case-sensitive)
- Contact exists in Plunk
- Contact is subscribed
**CORS errors in browser**
- Use public key (not secret key)
- Plunk API allows CORS from all origins
**Contact not created**
- Email must be valid
- Contact is created automatically when event is tracked
## Next steps
- [Build a workflow](/tutorials/welcome-series-workflow) triggered by events
- [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) decision guide
- [Stripe integration](/integrations/stripe-billing) for billing events
@@ -4,14 +4,12 @@ description: Send a transactional email in 5 minutes
icon: Mail
---
## Get your API key
## 1. Get your API key
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
1. Go to **Settings → General**
2. Copy your **Secret Key** (starts with `sk_`)
**Important:** Use Secret Key server-side only. Never expose it in client code.
## Send an email
## 2. Send the email
```bash
curl -X POST {{API_URL}}/v1/send \
@@ -19,55 +17,12 @@ curl -X POST {{API_URL}}/v1/send \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"subject": "Reset your password",
"body": "<p>Click here to reset: <a href=\"https://app.com/reset/abc123\">Reset Password</a></p>",
"subscribed": true
"subject": "Hello from Plunk",
"body": "<p>Your first email!</p>"
}'
```
Replace `sk_your_secret_key` and `user@example.com` with your values.
### With JavaScript
```javascript
await fetch('{{API_URL}}/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: user.email,
subject: 'Reset your password',
body: `<p>Click here: <a href="${resetLink}">Reset Password</a></p>`,
subscribed: true
})
});
```
### With Python
```python
import requests
import os
requests.post('{{API_URL}}/v1/send',
headers={
'Authorization': f'Bearer {os.environ["PLUNK_SECRET_KEY"]}',
'Content-Type': 'application/json'
},
json={
'to': user.email,
'subject': 'Reset your password',
'body': f'<p>Click here: <a href="{reset_link}">Reset Password</a></p>',
'subscribed': True
}
)
```
## Use variables
Make emails dynamic with variables:
## 3. With variables
```javascript
await fetch('{{API_URL}}/v1/send', {
@@ -81,97 +36,21 @@ await fetch('{{API_URL}}/v1/send', {
subject: 'Reset your password',
body: '<p>Hi {{name}}, click here: <a href="{{resetLink}}">Reset</a></p>',
name: user.name,
resetLink: `https://app.com/reset/${token}`,
subscribed: true
resetLink: `https://app.com/reset/${token}`
})
});
```
Variables in the body (`{{name}}`, `{{resetLink}}`) are replaced with the values you provide.
## 4. Using a template
## Use templates
Instead of passing HTML every time, create reusable templates.
### Create a template
1. Go to **Templates** → **Create Template**
2. Name: `Password Reset`
3. Type: `Transactional`
4. Subject: `Reset your password`
5. Body:
```html
<p>Hi {{name}},</p>
<p>Click here to reset your password:</p>
<p><a href="{{resetLink}}">Reset Password</a></p>
<p>This link expires in 1 hour.</p>
```
### Send with template
1. Create template in **Templates → Create Template**
2. Send using template ID:
```javascript
await fetch('{{API_URL}}/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: user.email,
template: 'password-reset', // Use template slug
name: user.name,
resetLink: `https://app.com/reset/${token}`,
subscribed: true
})
});
{
to: user.email,
template: 'clx123abc456',
name: user.name,
resetLink: resetUrl
}
```
No need to pass `subject` or `body` - they come from the template.
## Integration example
```javascript
// Express.js password reset endpoint
app.post('/forgot-password', async (req, res) => {
const { email } = req.body;
const user = await db.users.findOne({ email });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const token = crypto.randomBytes(32).toString('hex');
await db.resetTokens.create({ userId: user.id, token, expiresAt: Date.now() + 3600000 });
// Send email via Plunk
await fetch('{{API_URL}}/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: user.email,
subject: 'Reset your password',
body: `<p>Click here: <a href="https://app.com/reset/${token}">Reset Password</a></p>`,
subscribed: true
})
});
res.json({ success: true });
});
```
## Troubleshooting
**"Unauthorized" error** — Check your API key. Must be Secret Key (starts with `sk_`).
**Email not received** — Check spam folder. If using custom domain, verify it in Settings → Domains.
**Variables not replaced** — Ensure variable names match exactly (case-sensitive).
## Next steps
- [Build a workflow](/tutorials/welcome-series-workflow) for automated sequences
- [Set up custom domain](/guides/custom-domains) for better deliverability
- [Track events](/tutorials/event-tracking-integration) to trigger workflows
@@ -1,37 +0,0 @@
---
title: Tutorials
description: Step-by-step guides
icon: BookOpen
---
## Getting started
<Cards>
<Card icon="Mail" title="Send Your First Email" href="/tutorials/first-transactional-email">
Send a transactional email via API
</Card>
<Card icon="Workflow" title="Build a Welcome Series" href="/tutorials/welcome-series-workflow">
Create a 3-email automated workflow
</Card>
<Card icon="Send" title="Send a Campaign" href="/tutorials/newsletter-campaign">
Broadcast to your audience
</Card>
</Cards>
## Advanced
<Cards>
<Card icon="Users" title="Segment Targeting" href="/tutorials/segment-based-targeting">
Filter and target specific audiences
</Card>
<Card icon="Activity" title="Event Tracking" href="/tutorials/event-tracking-integration">
Track user behavior to trigger workflows
</Card>
<Card icon="ShoppingCart" title="Cart Abandonment" href="/tutorials/cart-abandonment-automation">
Recover abandoned carts automatically
</Card>
</Cards>
+1 -10
View File
@@ -1,13 +1,4 @@
{
"title": "Tutorials",
"pages": [
"index",
"first-transactional-email",
"welcome-series-workflow",
"newsletter-campaign",
"segment-based-targeting",
"cart-abandonment-automation",
"user-lifecycle-emails",
"event-tracking-integration"
]
"pages": ["first-transactional-email", "welcome-series-workflow", "newsletter-campaign", "segment-based-targeting"]
}
@@ -1,292 +1,39 @@
---
title: Send a Newsletter Campaign
title: Send a Newsletter
description: Broadcast an email to your audience
icon: Send
---
## Overview
## 1. Create a campaign
Campaigns let you send one-time broadcasts to your contacts from the dashboard. No code required.
1. Go to **Campaigns → Create Campaign**
2. Name: `March Newsletter`
## Create a campaign
## 2. Write your email
1. Go to **Campaigns** → **Create Campaign**
2. Fill in basic info:
- Name: `March Product Update`
- Description: `Monthly newsletter for March 2024`
- **From:** Your verified domain
- **Subject:** `March updates you'll love`
- Write your content
## Write your email
## 3. Select audience
### Email settings
Choose one:
- **All contacts:** Everyone subscribed
- **Segment:** A saved segment
- **Filtered:** Custom filters for this campaign
- **From**: Your email or verified domain
- **Subject**: `March product updates you'll love`
- **Preview text**: Shows in inbox preview
### Email content
Use the visual editor or write HTML:
```html
<h1>What's new in March</h1>
<p>Hi {{firstName ?? 'there'}},</p>
<p>We've been busy this month. Here's what's new:</p>
<h2>🚀 New Feature: Team Collaboration</h2>
<p>Invite team members and collaborate in real-time.</p>
<h2>⚡ Improved Performance</h2>
<p>Everything is now 2x faster.</p>
<h2>📊 New Analytics Dashboard</h2>
<p>Better insights into your data.</p>
<p><a href="https://yourapp.com/changelog">View Full Changelog</a></p>
<p>Thanks,<br>The Team</p>
<p><small>
You're receiving this because you subscribed to updates.
<a href="{{unsubscribeUrl}}">Unsubscribe</a>
</small></p>
```
### Use variables
Available variables:
- `{{firstName}}` - Contact's first name
- `{{email}}` - Contact's email
- `{{id}}` - Contact ID
- `{{unsubscribeUrl}}` - Auto-generated unsubscribe link
- Any custom contact data fields
**Fallback values:**
```html
<p>Hi {{firstName ?? 'there'}},</p>
<!-- Shows "Hi John," or "Hi there," if firstName is missing -->
```
## Select your audience
### All contacts
Sends to everyone subscribed in your account.
### Specific segment
1. Select **Segment** audience type
2. Choose a segment (e.g., "Premium Users")
3. Campaign sends to all contacts in that segment
### Filtered audience
Create custom filters for this campaign only:
**Example filters:**
- `plan` equals `premium`
- `lastLoginAt` within `30` days
- `country` equals `United States`
Combine with AND/OR logic.
## Preview and test
### Send test email
## 4. Test
1. Click **Send Test**
2. Enter your email address
3. Check your inbox
2. Check your inbox
3. Verify links and content
Verify:
- Subject line
- Email content
- Variables are replaced
- Links work
- Unsubscribe link present
## 5. Send
## Send or schedule
- **Send Now:** Starts immediately
- **Schedule:** Pick date and time
### Send now
## 6. Monitor
1. Click **Send Now**
2. Confirm
3. Campaign starts sending immediately
### Schedule for later
1. Click **Schedule**
2. Select date and time
3. Confirm
Campaign will send automatically at scheduled time.
## Monitor performance
### Real-time stats
Go to **Campaigns** → Your campaign to see:
- **Recipients**: Total contacts targeted
- **Sent**: How many emails sent
- **Delivered**: Successfully delivered
- **Opened**: Unique opens
- **Clicked**: Unique clicks
- **Bounced**: Failed deliveries
### Open and click rates
- **Open rate** = Opened / Delivered × 100%
- **Click rate** = Clicked / Delivered × 100%
**Good benchmarks:**
- Open rate: 15-25%
- Click rate: 2-5%
### View in activity
Go to **Activity** to see:
- Individual email opens
- Link clicks
- Delivery timeline
## Campaign best practices
**Subject lines:**
- Keep under 50 characters
- Avoid spam words (FREE, $$, URGENT)
- Personalize: `{{name}}, check out our new feature`
- A/B test different subject lines
**Send timing:**
- Tuesday-Thursday perform best
- 10am-2pm in recipient's timezone
- Avoid Mondays and Fridays
- Test what works for your audience
**Content:**
- One clear call-to-action
- Mobile-friendly design
- Keep under 500 words
- Use images sparingly (slow loading)
- Always include unsubscribe link
**Frequency:**
- Weekly: Maximum for engaged audiences
- Monthly: Safe default
- Quarterly: Minimum to stay top-of-mind
- Don't email too often - causes unsubscribes
## Advanced: Segment-based campaigns
### Example: Product announcement to paying customers
1. Create segment "Paying Customers":
- Filter: `plan` is not `free`
- Filter: `subscribed` equals `true`
2. Create campaign:
- Subject: `New premium features just for you`
- Audience: Segment "Paying Customers"
3. Send campaign - only goes to paying customers
### Example: Re-engagement campaign
1. Create segment "Inactive Users":
- Filter: `lastLoginAt` within `90` days is `false`
- Filter: `subscribed` equals `true`
2. Create campaign:
- Subject: `We miss you! Here's what's new`
- Content: Highlight recent updates
- Special offer: 20% off upgrade
3. Send or schedule
## Campaign vs workflow
**Use Campaign when:**
- One-time send (newsletter, announcement)
- Manual timing
- Same message to everyone
- No automation needed
**Use Workflow when:**
- Multi-email sequence needed
- Trigger on user action
- Delays between emails
- Personalized paths (if/then logic)
See [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) for full comparison.
## Duplicate and reuse
### Duplicate a campaign
1. Go to campaign
2. Click **Duplicate**
3. Edit content
4. Send to same or different audience
Useful for monthly newsletters - duplicate last month's, update content.
### Save as template
If you'll reuse the design:
1. **Templates** → **Create Template**
2. Paste your campaign HTML
3. Save
Now you can create campaigns faster using the template.
## Cancel a campaign
### Before sending
1. Go to campaign (status: Draft or Scheduled)
2. Click **Delete**
### While sending
1. Go to campaign (status: Sending)
2. Click **Cancel**
3. Stops queuing new emails (already sent emails can't be recalled)
### After sending
Cannot cancel or recall. Sent emails are delivered.
## Troubleshooting
**Campaign not sending**
- Check campaign status (Draft needs to be sent)
- Verify audience has contacts
- Ensure contacts are subscribed
- Custom domain must be verified
**Low open rate**
- Improve subject line
- Check spam folder placement
- Verify sender email/domain
- Review send time
**High unsubscribe rate**
- Sending too frequently
- Content not relevant
- Set better expectations at signup
- Review targeting
**Emails going to spam**
- Verify custom domain
- Avoid spam trigger words
- Don't use all caps or excessive punctuation
- Warm up new sending domain
## Next steps
- [Build a workflow](/tutorials/welcome-series-workflow) for automated sequences
- [Create segments](/tutorials/segment-based-targeting) for better targeting
- [Set up custom domain](/guides/custom-domains) for better deliverability
View stats at **Campaigns → Your campaign**:
- Sent, delivered, opened, clicked, bounced
@@ -1,298 +1,33 @@
---
title: Segment-Based Targeting
description: Target specific audiences with filters
title: Target with Segments
description: Send to specific audiences
icon: Users
---
## Overview
## 1. Create a segment
Segments are dynamic groups of contacts based on filters. Use them to send targeted campaigns or trigger workflows when contacts enter/exit segments.
## Create a segment
1. Go to **Segments** → **Create Segment**
1. Go to **Segments → Create Segment**
2. Name: `Premium Users`
3. Description: `Users on premium or enterprise plan`
## Add filters
## 2. Add filters
### Simple filter
Filter by a single field:
Example: Premium subscribers
- Field: `plan`
- Operator: `equals`
- Value: `premium`
All contacts where `plan` equals `premium` are in this segment.
Add more conditions with AND/OR.
### Multiple filters (AND logic)
All conditions must be true:
- `plan` equals `premium`
- **AND** `subscribed` equals `true`
- **AND** `lastLoginAt` within `30` days
Only premium users who are subscribed AND logged in recently.
### Multiple filters (OR logic)
Any condition can be true:
- `plan` equals `premium`
- **OR** `plan` equals `enterprise`
Users on either premium or enterprise plan.
### Complex filters (AND + OR)
Combine both:
- `subscribed` equals `true`
- **AND** (`plan` equals `premium` **OR** `plan` equals `enterprise`)
Subscribed users on premium OR enterprise plans.
## Filter operators
### Equals
Exact match:
- `plan` equals `premium`
- `country` equals `United States`
### Not equals
Everything except:
- `plan` not equals `free`
- `status` not equals `cancelled`
### Contains
Partial text match:
- `email` contains `@gmail.com`
- `companyName` contains `Inc`
### Greater than / Less than
Numeric comparisons:
- `mrr` greater than `100`
- `age` less than `30`
- `loginCount` greater than `10`
### Exists / Does not exist
Field has any value:
- `phoneNumber` exists
- `referralCode` does not exist
### Within
Time-based (requires ISO date):
- `signupDate` within `7` days
- `lastLoginAt` within `30` days
- `trialExpiresAt` within `3` days
## Example segments
### Active users
Users who logged in recently:
- `lastLoginAt` within `7` days
- **AND** `subscribed` equals `true`
### High-value customers
Users spending over $100/month:
- `mrr` greater than `100`
- **AND** `plan` is not `free`
### Trial expiring soon
Users whose trial ends in 3 days:
- `trialExpiresAt` within `3` days
- **AND** `plan` equals `trial`
### Inactive users
Haven't logged in for 30+ days:
- `lastLoginAt` within `30` days is `false`
- **AND** `subscribed` equals `true`
- **AND** `status` equals `active`
**Note:** To check "NOT within", set the within filter and toggle the NOT operator.
### Power users
High engagement score:
- `loginCount` greater than `50`
- **AND** `featureUsageCount` greater than `100`
### Geographic targeting
Specific country or region:
- `country` equals `United States`
- **AND** `state` equals `California`
### Feature adopters
Used a specific feature:
- Custom event filter: `feature_used` triggered
- **AND** event data: `featureName` equals `advanced_analytics`
## Use segments in campaigns
### Target a segment
## 3. Use in campaigns
1. Create campaign
2. Audience: Select **Segment**
3. Choose `Premium Users`
## 4. Use in workflows
1. Create workflow
2. Trigger: **Segment entry**
3. Choose your segment
4. Campaign sends only to contacts in that segment
### Preview count
Before sending, see how many contacts match:
- Shows estimated recipient count
- Updates in real-time as you adjust filters
## Use segments in workflows
### Trigger on segment entry
Create workflow that runs when contacts enter a segment:
1. **Workflows** → **Create Workflow**
2. Trigger: Segment `trial_expiring_soon`
3. Trigger condition: Contact **enters** segment
4. Add email: Trial expiration reminder
When a contact enters "trial_expiring_soon" segment, workflow triggers.
### Trigger on segment exit
Run workflow when contacts leave a segment:
1. Trigger: Segment `active_users`
2. Trigger condition: Contact **exits** segment
3. Add workflow: Re-engagement sequence
When user becomes inactive (exits "active_users"), re-engagement starts.
## Track membership changes
Enable to trigger events when contacts enter/exit:
1. Edit segment
2. Toggle **Track Membership**
3. Save
Now when contacts move in/out of the segment:
- Event `segment_entry_[segment_id]` is tracked
- Event `segment_exit_[segment_id]` is tracked
- Can use these events in other workflows
**Performance note:** Only enable for segments you'll use for triggers. Adds processing overhead.
## Segment best practices
**Keep it simple**
- 3-5 filters per segment max
- Avoid deeply nested conditions
- Test with expected contacts
**Use consistent data**
- Store dates as ISO strings
- Use numbers for numeric values
- Consistent field naming
**Name clearly**
- ✅ `Premium Users - Active`
- ❌ `Segment 1`
**Monitor size**
- Check segment count regularly
- Too large = slow processing
- Too small = not enough data
## Update segments
Segments update automatically:
- When contact data changes
- When contacts are added/removed
- Typically updates within minutes
Force refresh:
1. Go to segment
2. Click **Refresh Count**
## Advanced: Multi-level targeting
### Premium users in specific region
- `plan` equals `premium`
- **AND** `country` equals `United States`
- **AND** `lastLoginAt` within `7` days
### Churn risk scoring
- `lastLoginAt` within `30` days is `false`
- **AND** `supportTickets` greater than `3`
- **AND** `npsScore` less than `7`
### Upsell targeting
- `plan` equals `free`
- **AND** `featureUsageCount` greater than `50`
- **AND** `teamSize` greater than `5`
Users on free plan who are power users with teams (good upsell candidates).
## Performance at scale
Segments work efficiently with millions of contacts:
- Indexed queries for fast filtering
- Cursor-based pagination
- Background count computation
**Tips for large segments:**
- Use specific filters (avoid `contains` on large text fields)
- Store commonly queried data as top-level fields
- Use numeric comparisons when possible (faster than text)
## Troubleshooting
**Segment count is 0 but should have contacts**
- Check filter logic (AND vs OR)
- Verify field names match exactly (case-sensitive)
- Ensure contacts have the required fields
- Try simpler filters to debug
**Segment not updating**
- Click **Refresh Count** to force update
- Check that contact data was actually updated
- Segments typically update within 5 minutes
**Workflow not triggering on segment entry**
- Workflow is enabled
- Track Membership is enabled on segment
- Trigger is set to segment entry (not exit)
**Too many contacts in segment**
- Filters too broad
- Add more specific conditions
- Use AND logic instead of OR
## Next steps
- [Send a campaign](/tutorials/newsletter-campaign) to a segment
- [Build a workflow](/tutorials/welcome-series-workflow) triggered by segment changes
- [Track events](/tutorials/event-tracking-integration) to update contact data
4. Enable **Track membership changes** on the segment
@@ -6,31 +6,26 @@ icon: Workflow
## What you'll build
An automated workflow that sends 3 emails when users sign up:
- Day 0: Welcome email (immediate)
- Day 1: Feature tour (24 hours later)
- Day 3: Help offer (48 hours after that)
- Day 1: Feature tour
- Day 3: Help offer
## Create the workflow
## 1. Create templates
1. **Workflows****Create Workflow**
Create 3 templates in **Templates → Create Template**:
- `Welcome` (use `{{name}}` for personalization)
- `Feature Tour`
- `Help Offer`
## 2. Create the workflow
1. Go to **Workflows → Create Workflow**
2. Name: `Welcome Series`
3. Trigger Event: `user_signed_up`
4. Allow Re-entry: `No` (users get this once)
5. Click **Create**
3. Trigger: Event `user_signed_up`
4. Allow Re-entry: No
## Build the flow
## 3. Build the flow
In the visual editor:
1. **Add Send Email** step → Select your welcome template
2. **Add Delay** step → 1 day
3. **Add Send Email** step → Select your feature tour template
4. **Add Delay** step → 2 days
5. **Add Send Email** step → Select your help offer template
6. **Add Exit** step
Your flow:
```
[Trigger: user_signed_up]
@@ -47,110 +42,27 @@ Your flow:
[Exit]
```
7. **Enable** the workflow (toggle switch)
## 4. Enable the workflow
## Track the signup event
Toggle the workflow ON.
Add event tracking to your app when users sign up.
## 5. Track signups
### JavaScript
Add to your app:
```javascript
// After successful signup
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY}`,
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'user_signed_up',
email: user.email,
data: {
name: user.name,
dashboardUrl: 'https://app.yourapp.com/dashboard',
docsUrl: 'https://docs.yourapp.com'
}
data: { name: user.name }
})
});
```
### Python
```python
import requests
requests.post('{{API_URL}}/v1/track',
headers={
'Authorization': f'Bearer {os.environ["PLUNK_PUBLIC_KEY"]}',
'Content-Type': 'application/json'
},
json={
'event': 'user_signed_up',
'email': user.email,
'data': {
'name': user.name,
'dashboardUrl': 'https://app.yourapp.com/dashboard',
'docsUrl': 'https://docs.yourapp.com'
}
}
)
```
**Important:** Use your **Public Key** (starts with `pk_`) for event tracking.
## Test the workflow
### Manual test
1. Go to **Workflows** → Your workflow → **Executions** tab
2. Click **Create Execution**
3. Select a test contact
4. Add test data:
```json
{
"name": "Test User",
"dashboardUrl": "https://app.yourapp.com",
"docsUrl": "https://docs.yourapp.com"
}
```
5. **Start Execution**
Check your email - you should receive the welcome email immediately. The workflow will pause at the delay steps.
### Faster testing
For testing, temporarily change delays to 5 minutes instead of days. Test the flow, then change back.
## Monitor performance
1. **Workflows** → Your workflow
2. Check **Executions** to see who's in the workflow
3. Go to **Activity** to see email opens/clicks
4. Track open rates for each email
Typical good rates:
- Email 1: 60-80% open rate
- Email 2: 40-60% open rate
- Email 3: 30-50% open rate
## Troubleshooting
**Workflow not triggering** — Check:
- Workflow is enabled (toggle ON)
- Event name matches exactly: `user_signed_up`
- Using Public Key for tracking
- Contact exists and is subscribed
**Email not sending** — Check:
- Template exists
- Contact is subscribed
- Variables in event data match template variables
**Duplicate emails** — Ensure `Allow Re-entry` is `No`.
## Next steps
- [Add conditional logic](/automation-patterns/conditional-branching) for different user types
- [Track more events](/tutorials/event-tracking-integration) to trigger workflows
- [Build cart abandonment](/tutorials/cart-abandonment-automation) workflow
Use your **Public Key** (starts with `pk_`).