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
|
### Transactional emails
|
||||||
|
|
||||||
|
Use the `template` field with the **template ID** (not the template name):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST {{API_URL}}/v1/send \
|
curl -X POST {{API_URL}}/v1/send \
|
||||||
-H "Authorization: Bearer sk_your_secret_key" \
|
-H "Authorization: Bearer sk_your_secret_key" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"to": "[email protected]",
|
"to": "[email protected]",
|
||||||
"template": "order-confirmation",
|
"template": "clx123abc456",
|
||||||
"data": {
|
"data": {
|
||||||
"orderNumber": "12345",
|
"orderNumber": "12345",
|
||||||
"deliveryDate": "March 20"
|
"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
|
### 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.
|
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",
|
"index",
|
||||||
"---Getting Started---",
|
"---Getting Started---",
|
||||||
"getting-started",
|
"getting-started",
|
||||||
|
"---Tutorials---",
|
||||||
|
"tutorials",
|
||||||
|
"---Core Concepts---",
|
||||||
|
"concepts",
|
||||||
"---Guides---",
|
"---Guides---",
|
||||||
"guides",
|
"guides",
|
||||||
|
"---Automation Patterns---",
|
||||||
|
"automation-patterns",
|
||||||
"---API Reference---",
|
"---API Reference---",
|
||||||
"api-reference",
|
"api-reference",
|
||||||
"---Self-Hosting---",
|
"---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": {
|
"template": {
|
||||||
"type": "string",
|
"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": {
|
"from": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@@ -384,13 +384,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"withTemplate": {
|
"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": {
|
"value": {
|
||||||
"to": "[email protected]",
|
"to": "[email protected]",
|
||||||
"template": "welcome-email",
|
"template": "clx123abc456",
|
||||||
"data": {
|
"data": {
|
||||||
"firstName": "John",
|
"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