Update wiki
This commit is contained in:
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
+21
-4
@@ -251,7 +251,7 @@
|
||||
},
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "Template identifier to use"
|
||||
"description": "Template ID to use for this email. When provided, uses the template's subject, body, from, and reply-to settings. You can override these by explicitly providing subject, body, from, or reply fields in the request. Template variables are populated from the data field."
|
||||
},
|
||||
"from": {
|
||||
"oneOf": [
|
||||
@@ -384,13 +384,30 @@
|
||||
}
|
||||
},
|
||||
"withTemplate": {
|
||||
"summary": "Using template",
|
||||
"summary": "Using a template",
|
||||
"description": "Send email using a template. Provide the template ID and any data for template variables. The template's subject, body, from address, and reply-to will be used automatically.",
|
||||
"value": {
|
||||
"to": "[email protected]",
|
||||
"template": "welcome-email",
|
||||
"template": "clx123abc456",
|
||||
"data": {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe"
|
||||
"lastName": "Doe",
|
||||
"resetCode": {
|
||||
"value": "ABC123",
|
||||
"persistent": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"withTemplateOverride": {
|
||||
"summary": "Using template with overrides",
|
||||
"description": "You can override template values by providing subject, body, from, or reply fields. This example overrides the template's subject line.",
|
||||
"value": {
|
||||
"to": "[email protected]",
|
||||
"template": "clx123abc456",
|
||||
"subject": "Custom Subject Override",
|
||||
"data": {
|
||||
"firstName": "Jane"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user