Update wiki

This commit is contained in:
Dries Augustyns
2025-12-07 10:25:18 +01:00
parent b2ecf60a13
commit 0003e44db8
23 changed files with 3336 additions and 5 deletions
@@ -0,0 +1,135 @@
---
title: Conditional Branching
description: If/then logic in workflows
icon: GitBranch
---
## Overview
Conditions split workflows into two paths based on contact data.
```
[Condition: plan equals "premium"]
├─ True → [Send: Premium features]
└─ False → [Send: Upgrade offer]
```
Both paths required.
## Operators
| Operator | Use | Example |
|----------|-----|---------|
| `equals` | Exact match | `plan equals "pro"` |
| `notEquals` | Not matching | `plan notEquals "free"` |
| `contains` | Substring | `company contains "tech"` |
| `greaterThan` | Greater than | `mrr greaterThan 100` |
| `lessThan` | Less than | `loginCount lessThan 5` |
| `greaterThanOrEqual` | Greater than or equal | `age greaterThanOrEqual 18` |
| `lessThanOrEqual` | Less than or equal | `daysInactive lessThanOrEqual 30` |
| `exists` | Has value | `company exists` |
| `notExists` | Missing/null | `lastName notExists` |
| `startsWith` | Prefix | `coupon startsWith "SAVE"` |
| `endsWith` | Suffix | `email endsWith "@company.com"` |
**Important:** Numeric operators require field stored as number, not string.
## Common patterns
### Filter by plan
```
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise onboarding]
└─ False → [Send: Standard onboarding]
```
### Activity check
```
[Condition: loginCount greaterThan 10]
├─ True → [Send: Power user tips]
└─ False → [Send: Getting started]
```
### Nested conditions
Chain for multi-tier logic:
```
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise email]
└─ False ↓
[Condition: plan equals "pro"]
├─ True → [Send: Pro email]
└─ False → [Send: Free email]
```
### Multiple field checks (AND)
Nest conditions:
```
[Condition: plan equals "pro"]
├─ True ↓
│ [Condition: trialDaysLeft lessThan 3]
│ ├─ True → [Send: Trial ending]
│ └─ False → [Exit]
└─ False → [Exit]
```
Matches: `plan = "pro"` AND `trialDaysLeft < 3`
## Nested data
Access with dot notation:
```json
{
"field": "preferences.newsletter",
"operator": "equals",
"value": true
}
```
## Best practices
**Store correct types** — `99` (number) not `"99"` (string) for numeric comparisons.
**Check existence first** — If field might not exist:
```
[Condition: mrr exists]
├─ True → [Condition: mrr greaterThan 100]
└─ False → [Exit]
```
**Limit nesting** — More than 3 levels gets hard to maintain. Use separate workflows.
**Test both paths** — Verify true and false outcomes work.
**Use segments when filtering many** — Segment-based triggers more efficient than in-workflow conditions for large audiences.
## Common mistakes
**Case sensitivity** — `equals "Pro"` doesn't match `"pro"`
**Type mismatch** — `"100" greaterThan 50` fails (string vs number)
**Missing both paths** — Every condition needs true AND false connections
## Debugging
Check contact data first:
```bash
curl -X GET {{API_URL}}/contacts/contact_id \
-H "Authorization: Bearer sk_your_secret_key"
```
Verify field names and types match your condition.
## Next steps
- [See workflow patterns](/automation-patterns/workflow-patterns)
- [Build segments](/guides/segments) for trigger filtering
@@ -0,0 +1,53 @@
---
title: Automation Patterns
description: Workflow patterns and examples
icon: Zap
---
## Learn workflows
<Cards>
<Card icon="Workflow" title="Visual Builder Guide" href="/automation-patterns/visual-builder-guide">
Use the drag-and-drop workflow editor
</Card>
<Card icon="Layers" title="Workflow Patterns" href="/automation-patterns/workflow-patterns">
Common workflow examples
</Card>
</Cards>
## By use case
<Cards>
<Card icon="Users" title="Onboarding" href="/automation-patterns/onboarding-sequences">
Welcome and activate new users
</Card>
<Card icon="Lightbulb" title="Engagement" href="/automation-patterns/engagement-campaigns">
Drive feature adoption
</Card>
<Card icon="RefreshCw" title="Retention" href="/automation-patterns/retention-automation">
Prevent churn and win back users
</Card>
<Card icon="CreditCard" title="Transactional" href="/automation-patterns/transactional-workflows">
Order confirmations and receipts
</Card>
</Cards>
## Advanced
<Cards>
<Card icon="GitBranch" title="Conditional Branching" href="/automation-patterns/conditional-branching">
If/then logic in workflows
</Card>
<Card icon="Database" title="Execution Context" href="/automation-patterns/advanced-execution-context">
Pass event data through workflows
</Card>
<Card icon="Bug" title="Troubleshooting" href="/automation-patterns/troubleshooting-workflows">
Debug workflow issues
</Card>
</Cards>
@@ -0,0 +1,15 @@
{
"title": "Automation Patterns",
"pages": [
"index",
"visual-builder-guide",
"workflow-patterns",
"onboarding-sequences",
"engagement-campaigns",
"retention-automation",
"transactional-workflows",
"conditional-branching",
"advanced-execution-context",
"troubleshooting-workflows"
]
}
@@ -0,0 +1,258 @@
---
title: Visual Workflow Builder
description: Build workflows with drag-and-drop
icon: Workflow
---
## Overview
The workflow builder is a node-based visual editor for creating email automation. Each node is a step (send email, delay, condition, etc.) and connections define the flow.
## Canvas controls
- **-** / **+** - Zoom out/in
- **Fit** - Center and zoom to show entire workflow
- **Auto-Layout** - Automatically arrange nodes
- **Minimap** - Toggle overview map (bottom right)
## Step types
### Trigger
The starting point. Every workflow has one trigger.
**Types:**
- Event (e.g., `user_signed_up`)
- Segment entry/exit
- Schedule (cron)
Cannot be deleted.
### Send Email
Sends an email to the contact.
**Configuration:**
- Template (required)
- Variables (optional overrides)
Contact must be subscribed for marketing templates. Transactional templates send regardless.
### Delay
Pauses execution for a specified time.
**Configuration:**
- Duration: Number
- Unit: Minutes, Hours, or Days
**Example:** Delay 24 hours before next email.
### Wait for Event
Pauses until an event occurs or timeout is reached.
**Configuration:**
- Event name
- Timeout duration (optional)
**Requires two outgoing connections:**
- Event triggered path
- Timeout path
**Example:**
```
[Wait for: purchase_completed, timeout: 48h]
├─ Purchased → Send thank you
└─ Timeout → Send discount reminder
```
### Condition
Branches workflow based on contact data.
**Configuration:**
- Field to check
- Operator (equals, contains, greaterThan, etc.)
- Value to compare
**Requires two outgoing connections:**
- True path
- False path
**Example:**
```
[Condition: plan equals "premium"]
├─ True → Send premium features
└─ False → Send upgrade offer
```
### Webhook
Sends HTTP request to external URL.
**Configuration:**
- URL
- Method (GET, POST, PUT, DELETE)
- Headers (optional)
- Body (JSON, optional)
**Example:** Update CRM when workflow completes.
### Update Contact
Updates contact data fields.
**Configuration:**
- Fields: Key-value pairs
**Example:**
```
Fields:
onboardingCompleted: true
lastWorkflowStep: "welcome_series_done"
```
### Exit
Ends workflow execution. Can have multiple exit points for different paths.
## Building a workflow
### Add a step
**Method 1:** Click **+** button below any node
**Method 2:** Drag step type from sidebar onto canvas
### Configure a step
1. Click node to select
2. Right panel opens
3. Fill required fields
4. Save
### Connect steps
Connections auto-create when using + button. To manually connect:
1. Drag from node's bottom handle
2. Drop on another node's top handle
### Arrange layout
Click **Auto-Layout** for automatic top-to-bottom arrangement.
## Example workflows
### Linear sequence
```
[Trigger: user_signed_up]
[Send: Welcome]
[Delay: 24h]
[Send: Feature tour]
[Exit]
```
### With condition
```
[Trigger: trial_started]
[Send: Welcome]
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise onboarding]
└─ False → [Send: Standard onboarding]
```
### With event wait
```
[Trigger: cart_abandoned]
[Delay: 1h]
[Send: First reminder]
[Wait for: purchase_completed, timeout: 23h]
├─ Purchased → [Exit]
└─ Timeout → [Send: Discount offer] → [Exit]
```
## Validation
The builder validates in real-time. Red border = error.
**Common errors:**
- "No outgoing connections" → Add connection to next step
- "Wait for Event requires two paths" → Add event + timeout paths
- "Condition requires true and false paths" → Add both paths
- "Template not found" → Select valid template
## Testing
### Manual execution
1. **Workflows** → Your workflow → **Executions**
2. **Create Execution**
3. Select test contact
4. Provide test data (JSON)
5. **Start**
Watch execution progress in real-time.
### Faster testing
For testing delays:
- Temporarily change to 5 minutes instead of days
- Test the flow
- Change back to real durations
## Keyboard shortcuts
| Key | Action |
|-----|--------|
| Delete | Delete selected node |
| Cmd/Ctrl + Z | Undo |
| Cmd/Ctrl + Shift + Z | Redo |
| + / - | Zoom |
| F | Fit to screen |
| Esc | Deselect |
## Troubleshooting
**Workflow not triggering**
- Workflow is enabled (toggle ON)
- Event name matches exactly (case-sensitive)
- Contact exists and is subscribed
**Execution stuck**
- Check delay configuration
- Verify wait timeout hasn't expired
- No circular references
**Email not sending**
- Template exists
- Contact is subscribed
- Variables in data match template
## Best practices
**Keep it simple** - 5-10 steps per workflow. Break complex flows into multiple workflows.
**Descriptive names** - "Send Welcome Email - Day 0" not "Email 1"
**Test first** - Always test with a test contact before enabling.
**Space emails** - Minimum 12-24 hours between emails to avoid fatigue.
## Next steps
- [Build your first workflow](/tutorials/welcome-series-workflow)
- [Workflow patterns](/automation-patterns/workflow-patterns)
- [Conditional branching](/automation-patterns/conditional-branching)
@@ -0,0 +1,294 @@
---
title: Common Workflow Patterns
description: Reusable workflow templates
icon: Layers
---
## Linear sequence
Send a series of emails with delays between them.
```
[Trigger: event]
[Send Email 1]
[Delay]
[Send Email 2]
[Delay]
[Send Email 3]
[Exit]
```
**Use for:**
- Onboarding series
- Educational drip campaigns
- Feature introduction sequences
**Example timing:**
- Day 0: Welcome
- Day 1: Getting started guide
- Day 3: Tips and tricks
- Day 7: Feature deep dive
## Wait and branch
Pause for an event, then branch based on outcome.
```
[Trigger: event]
[Wait for Event: timeout X days]
├─ Event occurred → [Send success email] → [Exit]
└─ Timeout → [Send reminder email] → [Exit]
```
**Use for:**
- Trial conversion (waiting for subscription)
- Activation campaigns (waiting for key action)
- Re-engagement (waiting for login)
**Example:**
```
[Trigger: trial_started]
[Send: Welcome to trial]
[Wait for: subscription_created, timeout: 7 days]
├─ Subscribed → [Send: Thanks for subscribing] → [Exit]
└─ Timeout → [Send: Last chance offer] → [Exit]
```
## Conditional branch
Split workflow based on contact data.
```
[Trigger: event]
[Condition: check contact field]
├─ True → [Send email A] → [Exit]
└─ False → [Send email B] → [Exit]
```
**Use for:**
- Personalization by plan/tier
- Segmented messaging
- Feature availability checks
**Example:**
```
[Trigger: signed_up]
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise onboarding] → [Exit]
└─ False → [Send: Standard onboarding] → [Exit]
```
## Multi-condition cascade
Chain multiple conditions for complex logic.
```
[Trigger: event]
[Condition: is premium?]
├─ True → [Send: Premium content] → [Exit]
└─ False ↓
[Condition: trial active?]
├─ True → [Send: Trial content] → [Exit]
└─ False → [Send: Free content] → [Exit]
```
**Use for:**
- Tiered content delivery
- Progressive feature reveals
- Access-based messaging
## Wait chain
Multiple wait steps with progressive urgency.
```
[Trigger: cart_abandoned]
[Delay: 1 hour]
[Send: Gentle reminder]
[Wait for: purchase, timeout: 23 hours]
├─ Purchased → [Exit]
└─ Timeout ↓
[Send: Discount offer]
[Wait for: purchase, timeout: 48 hours]
├─ Purchased → [Exit]
└─ Timeout → [Send: Final reminder] → [Exit]
```
**Use for:**
- Cart abandonment
- Multi-touch re-engagement
- Escalating incentives
## Segment entry drip
Triggered when contact joins segment.
```
[Trigger: enters "Inactive Users" segment]
[Send: We miss you]
[Delay: 3 days]
[Condition: still in segment?]
├─ Yes → [Send: Special offer] → [Exit]
└─ No → [Exit]
```
**Use for:**
- Churn prevention
- Win-back campaigns
- Behavior-based sequences
**Important:** Segment must have `trackMembership: true` enabled.
## Schedule with filter
Runs on schedule, filtered by conditions.
```
[Trigger: daily at 9am]
[Condition: trial expires in 3 days]
├─ True → [Send: Trial expiring] → [Exit]
└─ False → [Exit]
```
**Use for:**
- Trial expiration reminders
- Subscription renewal notices
- Scheduled checks with conditional sends
## Update and continue
Modify contact data mid-workflow.
```
[Trigger: signed_up]
[Send: Welcome]
[Update Contact: onboardingStep = 1]
[Delay: 1 day]
[Send: Getting started]
[Update Contact: onboardingStep = 2]
[Exit]
```
**Use for:**
- Tracking workflow progress
- Triggering other systems via segment changes
- Marking completion milestones
## Webhook integration
Call external API during workflow.
```
[Trigger: purchase_completed]
[Send: Order confirmation]
[Delay: 7 days]
[Webhook: Create review request in external system]
[Send: Review request]
[Exit]
```
**Use for:**
- Syncing with CRM
- Triggering other automation tools
- Logging workflow events
## Complex multi-path
Combine patterns for sophisticated flows.
```
[Trigger: trial_started]
[Send: Welcome]
[Delay: 1 day]
[Send: Feature tour]
[Wait for: feature_used, timeout: 3 days]
├─ Used ↓
│ [Send: Great job]
│ ↓
│ [Delay: 3 days]
│ ↓
│ [Condition: trial ending in 1 day]
│ ├─ True → [Send: Upgrade reminder] → [Exit]
│ └─ False → [Exit]
└─ Timeout ↓
[Send: Help offer]
[Wait for: feature_used, timeout: 3 days]
├─ Used → [Send: Nice work] → [Exit]
└─ Timeout → [Update Contact: needs_help = true] → [Exit]
```
**Use for:**
- Advanced onboarding
- Complex user journeys
- Adaptive content delivery
## Best practices
**Keep it simple** — Start with 3-5 steps. Add complexity only when needed.
**Exit explicitly** — Every path should end at an Exit step.
**Handle both outcomes** — Conditions and waits need success + failure paths.
**Test incrementally** — Build and test each section before adding more.
**Use meaningful delays** — Don't spam. Minimum 12-24 hours between emails.
**Name clearly** — "Send Welcome Email - Day 0" not "Email 1".
**Monitor drop-off** — Check execution stats to see where contacts exit.
**Prevent loops** — Never create circular paths. Workflow should always progress forward.
## Performance tips
**Batch schedule triggers** — If using schedules, run at off-peak hours to spread load.
**Limit concurrent waits** — Long waits (weeks/months) keep executions in memory. Use scheduled workflows for very long delays.
**Avoid deep nesting** — More than 3-4 condition levels becomes hard to maintain.
**Use segments over conditions** — If filtering many contacts, segment-based triggers are more efficient than in-workflow conditions.
## Next steps
- [Build conditional logic](/automation-patterns/conditional-branching)
- [Understand execution context](/automation-patterns/advanced-execution-context)
- [Troubleshoot workflows](/automation-patterns/troubleshooting-workflows)
@@ -0,0 +1,129 @@
---
title: Campaigns vs Workflows
description: Choose the right tool for sending emails
icon: GitCompare
---
## When to use each
**Transactional API** (`/v1/send`) — Immediate one-off emails from your code
- Password resets, confirmations, receipts
- Triggered directly by user actions
- Instant delivery
**Campaigns** — One-time broadcasts to many contacts
- Newsletters, announcements, promotions
- Created in dashboard, send now or schedule
- No code required
**Workflows** — Automated multi-email sequences
- Onboarding series, abandoned cart, trial reminders
- Triggered by events, segment changes, or schedules
- Delays, conditions, multiple emails
## Transactional API
Send emails directly from your application code.
```javascript
await fetch('{{API_URL}}/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: user.email,
subject: 'Reset your password',
body: `Click here: ${resetLink}`
})
});
```
**Use for:**
- Emails that must send immediately
- One-to-one triggered emails
- Context-specific data (reset tokens, order IDs)
## Campaigns
Create and send broadcasts in the dashboard.
1. Create campaign
2. Write email content
3. Select audience (all contacts, segment, or filter)
4. Send now or schedule
**Use for:**
- One-time sends to many people
- Scheduled announcements
- Manual email sends
**Can't do:**
- Automation or triggers
- Multi-step sequences
- Delays between emails
## Workflows
Build automated sequences with the visual workflow builder.
**Example workflow:**
```
[Trigger: user_signed_up]
[Send Email: Welcome]
[Delay: 24 hours]
[Send Email: Feature tour]
[Delay: 48 hours]
[Send Email: Help offer]
```
**Use for:**
- Multi-email sequences
- Time-delayed follow-ups
- Event-triggered automation
- Conditional logic (if/then)
**Setup required:**
- Track events from your app via `/v1/track`
- Create workflow in dashboard
- Enable workflow
## Decision matrix
| Need | Use |
|------|-----|
| Send password reset now | Transactional API |
| Send monthly newsletter | Campaign |
| Send welcome series over 3 days | Workflow |
| Send order confirmation | Transactional API |
| Send announcement to all users | Campaign |
| Send trial reminder 3 days before expiration | Workflow |
| Send receipt after payment | Transactional API |
| Send seasonal promotion | Campaign |
| Send abandoned cart recovery (1hr + 24hr) | Workflow |
## Using them together
Most apps use all three:
**SaaS example:**
- **Transactional**: Password resets, email verification
- **Campaigns**: Monthly product updates
- **Workflows**: Trial onboarding, churn prevention
**E-commerce example:**
- **Transactional**: Order confirmations, shipping updates
- **Campaigns**: Weekly deals newsletter
- **Workflows**: Abandoned cart, review requests
## Next steps
- [Send a transactional email](/tutorials/first-transactional-email)
- [Create your first workflow](/tutorials/welcome-series-workflow)
- [Build a campaign](/tutorials/newsletter-campaign)
@@ -0,0 +1,214 @@
---
title: Contacts and Data
description: Store and manage your audience
icon: Users
---
## What are contacts
Contacts are people in your email list. Each contact has:
- **Email** — Unique identifier
- **Subscription status** — Subscribed or unsubscribed
- **Custom data** — Any fields you need
## Data structure
```json
{
"id": "contact_abc123",
"email": "[email protected]",
"subscribed": true,
"data": {
"firstName": "Sarah",
"plan": "pro",
"mrr": 99
}
}
```
The `data` field stores custom information as key-value pairs.
## Data types
**Strings:**
```json
{ "firstName": "Sarah", "company": "Acme Inc" }
```
**Numbers:**
```json
{ "mrr": 99, "loginCount": 15 }
```
Store as numbers for `greaterThan`/`lessThan` comparisons.
**Booleans:**
```json
{ "verified": true, "newsletter": false }
```
**Dates:**
```json
{ "signupDate": "2024-03-15T10:30:00Z" }
```
Use ISO 8601 format.
**Arrays:**
```json
{ "tags": ["vip", "enterprise"] }
```
**Objects:**
```json
{
"address": {
"city": "San Francisco",
"country": "US"
}
}
```
Access nested fields: `address.country`
## Creating contacts
### Via API
```bash
curl -X POST {{API_URL}}/contacts \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"subscribed": true,
"data": {
"firstName": "Sarah",
"plan": "pro"
}
}'
```
### Automatic upsert
If email exists, updates instead of creating duplicate.
```javascript
// First call
POST /contacts { email: "[email protected]", data: { plan: "free" } }
// Second call - updates same contact
POST /contacts { email: "[email protected]", data: { mrr: 99 } }
// Result: { plan: "free", mrr: 99 }
```
Data merges automatically.
### Via event tracking
```bash
curl -X POST {{API_URL}}/v1/track \
-H "Authorization: Bearer pk_your_public_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"event": "signed_up",
"data": {
"plan": "pro",
"source": "landing"
}
}'
```
Creates contact if doesn't exist, updates if does.
## Updating contacts
```bash
curl -X PATCH {{API_URL}}/contacts/contact_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"data": {
"plan": "premium",
"mrr": 199
}
}'
```
New fields added, existing fields overwritten, unmentioned fields preserved.
### Remove fields
Set to `null`:
```json
{ "data": { "temporaryToken": null } }
```
## Subscription status
**Subscribed (true):**
- Receives marketing emails
- Receives transactional emails
**Unsubscribed (false):**
- Does NOT receive marketing emails
- Still receives transactional emails
Template type controls this behavior. See [Template Types](/concepts/templates-types).
## Using contact data
### In templates
```html
<h1>Hi {{firstName}}!</h1>
<p>Your {{plan}} plan renews on {{renewalDate}}.</p>
```
### In segments
Filter by data fields:
- `plan equals "premium"`
- `mrr greaterThan 100`
- `loginCount lessThan 5`
### In workflows
```
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise content]
└─ False → [Send: Standard content]
```
## Best practices
**Consistent naming** — Use camelCase or snake_case, not both.
**Correct types** — Use `99` not `"99"` for numbers.
**ISO dates** — `"2024-03-15T10:30:00Z"` for date fields.
**Sync critical fields only** — Don't mirror entire database. Only fields used in emails, segments, or workflows.
**Update in real-time** — When user data changes, update contact immediately.
**Respect unsubscribes** — Never re-subscribe automatically.
## Deleting contacts
```bash
curl -X DELETE {{API_URL}}/contacts/contact_id \
-H "Authorization: Bearer sk_your_secret_key"
```
Permanent deletion. Consider unsubscribing instead to preserve history.
## Next steps
- [Create segments](/concepts/segments-and-filters)
- [Track events](/concepts/events-and-triggers)
- [Use templates](/concepts/templates-types)
@@ -0,0 +1,180 @@
---
title: Events and Triggers
description: Track behavior and trigger workflows
icon: Activity
---
## What are events
Events track user actions from your application. Use them to update contact data, trigger workflows, and build segments.
## Tracking events
### Basic event
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'signed_up',
email: user.email
})
});
```
Creates or updates contact. Event is recorded.
### Event with data
```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: {
plan: 'premium',
mrr: 99
}
})
});
```
Updates contact data fields. Data persists on contact record.
## Public vs Secret keys
### Public Key (pk_*)
- Safe for client-side code
- Only works with `/v1/track`
- Use in browser/mobile apps
### Secret Key (sk_*)
- Server-side only
- Works with all endpoints
- Full API access
## Event naming
Use clear, past-tense verbs with underscores:
✅ `signed_up`, `purchase_completed`, `feature_activated`
❌ `signup`, `buy`, `feature`
## Workflow triggers
### Event trigger
Workflow starts when event is tracked.
```
Trigger: event = "signed_up"
```
Track event:
```javascript
track('signed_up', '[email protected]');
```
Workflow starts for that contact.
### Wait for event
Workflow pauses until event occurs.
```
[Wait for Event: purchase_completed, timeout: 7 days]
├─ Event occurred → [Send: Thank you]
└─ Timeout → [Send: Discount offer]
```
## Common patterns
### User lifecycle
```javascript
// Signup
track('signed_up', email, { source: 'landing' });
// First login
track('first_login', email, { lastLoginAt: new Date().toISOString() });
// Subscription
track('subscription_created', email, { plan: 'premium', mrr: 99 });
```
### E-commerce
```javascript
// Cart abandonment
track('cart_abandoned', email, {
cartTotal: 149.99,
cartUrl: `https://store.com/cart/${cartId}`
});
// Purchase
track('purchase_completed', email, {
orderId: order.id,
orderTotal: order.total
});
```
## Event-based segments
Track events to update contact data, then segment on that data.
1. Track login:
```javascript
track('login', email, {
lastLoginAt: new Date().toISOString()
});
```
2. Create segment:
```
Field: lastLoginAt
Operator: greaterThan
Value: {{7_days_ago}}
```
Contacts auto-join segment when they log in.
## Testing events
**Dashboard:** Contacts → Search email → Events tab
Shows all events for that contact.
**Workflows:** Check executions after tracking event to verify workflow triggered.
## Rate limits
- Public key: 100 requests/minute
- Secret key: 1000 requests/minute
## Common issues
**Workflow not triggering:**
- Workflow is enabled
- Event name matches exactly (case-sensitive)
- Contact email is correct
**Contact data not updating:**
- Field names are case-sensitive
- Values are correct type (number vs string)
## Next steps
- [Build event-triggered workflows](/tutorials/welcome-series-workflow)
- [Create event-based segments](/concepts/segments-and-filters)
- [Track events from your app](/tutorials/event-tracking-integration)
+49
View File
@@ -0,0 +1,49 @@
---
title: Core Concepts
description: Understand how Plunk works
icon: Lightbulb
---
## Email methods
<Cards>
<Card icon="GitCompare" title="Campaigns vs Workflows" href="/concepts/campaigns-vs-workflows">
When to use each email method
</Card>
</Cards>
## Data & contacts
<Cards>
<Card icon="Users" title="Contacts and Data" href="/concepts/contacts-and-data">
Store and manage your audience
</Card>
<Card icon="Filter" title="Segments" href="/concepts/segments-and-filters">
Create dynamic audience groups
</Card>
</Cards>
## Sending emails
<Cards>
<Card icon="Mail" title="Template Types" href="/concepts/templates-types">
Marketing vs Transactional templates
</Card>
<Card icon="Activity" title="Events and Triggers" href="/concepts/events-and-triggers">
Track behavior and trigger workflows
</Card>
</Cards>
## Performance & delivery
<Cards>
<Card icon="Send" title="Email Deliverability" href="/concepts/email-deliverability">
Reach the inbox
</Card>
<Card icon="Zap" title="Scale and Performance" href="/concepts/scale-and-performance">
Optimize for millions of contacts
</Card>
</Cards>
+13
View File
@@ -0,0 +1,13 @@
{
"title": "Core Concepts",
"pages": [
"index",
"contacts-and-data",
"templates-types",
"campaigns-vs-workflows",
"segments-and-filters",
"events-and-triggers",
"email-deliverability",
"scale-and-performance"
]
}
@@ -0,0 +1,150 @@
---
title: Segments and Filters
description: Create dynamic audience groups
icon: Filter
---
## What are segments
Segments are dynamic groups of contacts based on data filters. They update automatically when contact data changes.
Use segments to:
- Target specific audiences in campaigns
- Trigger workflows when contacts enter/exit
- Filter contacts in dashboard
## Creating segments
### In dashboard
**Contacts** → **Segments** → **Create Segment**
1. Name your segment
2. Add filters
3. Save
### Via API
```bash
curl -X POST {{API_URL}}/segments \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Users",
"filters": [
{
"field": "plan",
"operator": "equals",
"value": "premium"
}
]
}'
```
## Filter operators
| Operator | Use | Example |
|----------|-----|---------|
| `equals` | Exact match | `plan equals "pro"` |
| `notEquals` | Not matching | `plan notEquals "free"` |
| `contains` | Substring (case-insensitive) | `email contains "@company.com"` |
| `greaterThan` | Number comparison | `mrr greaterThan 100` |
| `lessThan` | Number comparison | `loginCount lessThan 5` |
| `greaterThanOrEqual` | Inclusive comparison | `age greaterThanOrEqual 18` |
| `lessThanOrEqual` | Inclusive comparison | `daysInactive lessThanOrEqual 30` |
| `exists` | Field has value | `company exists` |
| `notExists` | Field missing/null | `lastName notExists` |
| `startsWith` | String prefix | `coupon startsWith "SAVE"` |
| `endsWith` | String suffix | `email endsWith ".edu"` |
## Multiple filters (AND logic)
All filters must match.
```json
{
"name": "Active Premium Users",
"filters": [
{ "field": "plan", "operator": "equals", "value": "premium" },
{ "field": "loginCount", "operator": "greaterThan", "value": 5 }
]
}
```
Contact matches only if: `plan = "premium"` AND `loginCount > 5`
## Dynamic updates
Segments update automatically.
**Example:**
1. Segment: `plan equals "premium"`
2. User upgrades: `plan` changes from "free" to "premium"
3. Contact auto-added to segment
No manual refresh needed.
## Workflow triggers
Trigger workflows when contacts enter/exit segments.
### Enable tracking
```bash
curl -X PATCH {{API_URL}}/segments/segment_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"trackMembership": true}'
```
Only enable for segments used as triggers.
### Entry trigger
```
[Trigger: contact enters "Premium Users"]
[Send: Welcome to premium]
```
### Exit trigger
```
[Trigger: contact exits "Trial Users"]
[Send: Trial ended offer]
```
## Nested fields
Access nested data with dot notation.
```json
{
"field": "preferences.newsletter",
"operator": "equals",
"value": true
}
```
## Best practices
**Store correct types** — Use `99` not `"99"` for numbers. Use `true` not `"true"` for booleans.
**Track membership sparingly** — Only enable on segments used as workflow triggers.
**Name descriptively** — "High-Value Customers (MRR >$200)" not "Segment 3"
## Common issues
**Numeric filters not working** — Ensure field is stored as number, not string.
**Case-sensitive matching** — `equals "Pro"` doesn't match `plan = "pro"`. Use `contains` for case-insensitive.
**Missing field** — If field doesn't exist on contact, filter won't match.
## Next steps
- [Use segments in campaigns](/guides/campaigns)
- [Trigger workflows on segment entry](/guides/workflows)
- [Store contact data](/concepts/contacts-and-data)
@@ -0,0 +1,112 @@
---
title: Template Types
description: Marketing vs Transactional templates
icon: Mail
---
## Two template types
| Template Type | Sends to Unsubscribed? | Use For |
|--------------|------------------------|---------|
| **Marketing** | No | Newsletters, promotions, announcements |
| **Transactional** | Yes | Receipts, confirmations, password resets |
## Marketing templates
Only sends to subscribed contacts.
**Use for:** Newsletters, product updates, promotional emails.
**Behavior:**
```javascript
// Contact is unsubscribed
POST /v1/send { to: "[email protected]", template: "newsletter" }
// → Email NOT sent
```
## Transactional templates
Sends regardless of subscription status.
**Use for:** Order confirmations, password resets, account alerts.
**Behavior:**
```javascript
// Contact is unsubscribed
POST /v1/send { to: "[email protected]", template: "receipt" }
// → Email sent
```
## Setting template type
### In dashboard
**Templates** → Create/Edit → **Type** dropdown
### Via API
```bash
curl -X POST {{API_URL}}/templates \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Password Reset",
"type": "TRANSACTIONAL",
"subject": "Reset your password",
"body": "<p>Click: {{resetLink}}</p>"
}'
```
Type: `MARKETING` or `TRANSACTIONAL`
## Changing type
Update anytime:
```bash
curl -X PATCH {{API_URL}}/templates/template_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"type": "MARKETING"}'
```
**Warning:** Changing transactional → marketing stops sending to unsubscribed contacts.
## Unsubscribe links
Marketing templates auto-include unsubscribe link in footer.
Custom placement:
```html
<a href="{{unsubscribeUrl}}">Unsubscribe</a>
```
Transactional templates don't need unsubscribe links.
## Choosing the right type
Ask: "Would user be frustrated if they didn't receive this after unsubscribing?"
**If yes → Transactional**
- Password resets
- Order confirmations
- Account alerts
**If no → Marketing**
- Newsletters
- Product announcements
- Promotions
## Best practices
**Default to marketing** — Only use transactional for truly necessary emails.
**Don't abuse transactional** — Sending marketing content via transactional templates violates regulations and damages reputation.
**Test both states** — Verify behavior with subscribed and unsubscribed contacts.
## Next steps
- [Create templates](/guides/templates)
- [Manage subscriptions](/concepts/contacts-and-data)
- [Send emails](/tutorials/first-transactional-email)
+32 -1
View File
@@ -148,13 +148,15 @@ Access nested objects with dot notation:
### Transactional emails
Use the `template` field with the **template ID** (not the template name):
```bash
curl -X POST {{API_URL}}/v1/send \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"template": "order-confirmation",
"template": "clx123abc456",
"data": {
"orderNumber": "12345",
"deliveryDate": "March 20"
@@ -162,6 +164,35 @@ curl -X POST {{API_URL}}/v1/send \
}'
```
**Finding your template ID:**
- In the dashboard: Go to Templates → Click on your template → Copy the ID from the URL or template details
- Via API: Use `GET /templates` to list all templates with their IDs
When using a template:
- **Subject, body, from, and reply-to** are automatically taken from the template
- **Template variables** (e.g., `{{orderNumber}}`) are populated from the `data` field
- You can **override** any template value by explicitly providing it in the request (see example below)
### Overriding template values
```bash
curl -X POST {{API_URL}}/v1/send \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"template": "clx123abc456",
"subject": "Custom Subject (overrides template)",
"from": {
"name": "Custom Sender",
"email": "[email protected]"
},
"data": {
"orderNumber": "12345"
}
}'
```
### In workflows
When creating a **Send Email** workflow step, select the template from the dropdown. Variables are automatically filled from contact data and workflow context.
+6
View File
@@ -3,8 +3,14 @@
"index",
"---Getting Started---",
"getting-started",
"---Tutorials---",
"tutorials",
"---Core Concepts---",
"concepts",
"---Guides---",
"guides",
"---Automation Patterns---",
"automation-patterns",
"---API Reference---",
"api-reference",
"---Self-Hosting---",
@@ -0,0 +1,227 @@
---
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
@@ -0,0 +1,475 @@
---
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: '[email protected]',
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
@@ -0,0 +1,177 @@
---
title: Send Your First Email
description: Send a transactional email in 5 minutes
icon: Mail
---
## Get your API key
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
2. Copy your **Secret Key** (starts with `sk_`)
**Important:** Use Secret Key server-side only. Never expose it in client code.
## Send an email
```bash
curl -X POST {{API_URL}}/v1/send \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Reset your password",
"body": "<p>Click here to reset: <a href=\"https://app.com/reset/abc123\">Reset Password</a></p>",
"subscribed": true
}'
```
Replace `sk_your_secret_key` and `[email protected]` 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:
```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>Hi {{name}}, click here: <a href="{{resetLink}}">Reset</a></p>',
name: user.name,
resetLink: `https://app.com/reset/${token}`,
subscribed: true
})
});
```
Variables in the body (`{{name}}`, `{{resetLink}}`) are replaced with the values you provide.
## 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
```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
})
});
```
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
@@ -0,0 +1,37 @@
---
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>
@@ -0,0 +1,13 @@
{
"title": "Tutorials",
"pages": [
"index",
"first-transactional-email",
"welcome-series-workflow",
"newsletter-campaign",
"segment-based-targeting",
"cart-abandonment-automation",
"user-lifecycle-emails",
"event-tracking-integration"
]
}
@@ -0,0 +1,292 @@
---
title: Send a Newsletter Campaign
description: Broadcast an email to your audience
icon: Send
---
## Overview
Campaigns let you send one-time broadcasts to your contacts from the dashboard. No code required.
## Create a campaign
1. Go to **Campaigns** → **Create Campaign**
2. Fill in basic info:
- Name: `March Product Update`
- Description: `Monthly newsletter for March 2024`
## Write your email
### Email settings
- **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
1. Click **Send Test**
2. Enter your email address
3. Check your inbox
Verify:
- Subject line
- Email content
- Variables are replaced
- Links work
- Unsubscribe link present
## Send or schedule
### Send now
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
@@ -0,0 +1,298 @@
---
title: Segment-Based Targeting
description: Target specific audiences with filters
icon: Users
---
## Overview
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**
2. Name: `Premium Users`
3. Description: `Users on premium or enterprise plan`
## Add filters
### Simple filter
Filter by a single field:
- Field: `plan`
- Operator: `equals`
- Value: `premium`
All contacts where `plan` equals `premium` are in this segment.
### 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
1. Create campaign
2. Audience: Select **Segment**
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
@@ -0,0 +1,156 @@
---
title: Build a Welcome Series
description: Create a 3-email onboarding workflow
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)
## Create the workflow
1. **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**
## 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]
[Send: Welcome]
[Delay: 1 day]
[Send: Feature Tour]
[Delay: 2 days]
[Send: Help Offer]
[Exit]
```
7. **Enable** the workflow (toggle switch)
## Track the signup event
Add event tracking to your app when users sign up.
### JavaScript
```javascript
// After successful signup
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: 'user_signed_up',
email: user.email,
data: {
name: user.name,
dashboardUrl: 'https://app.yourapp.com/dashboard',
docsUrl: 'https://docs.yourapp.com'
}
})
});
```
### 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