From 0003e44db82af89a4c88f7cbd8293277f36ad478 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Sun, 7 Dec 2025 10:25:18 +0100 Subject: [PATCH] Update wiki --- .../conditional-branching.mdx | 135 +++++ .../docs/automation-patterns/index.mdx | 53 ++ .../docs/automation-patterns/meta.json | 15 + .../visual-builder-guide.mdx | 258 ++++++++++ .../automation-patterns/workflow-patterns.mdx | 294 +++++++++++ .../docs/concepts/campaigns-vs-workflows.mdx | 129 +++++ .../docs/concepts/contacts-and-data.mdx | 214 ++++++++ .../docs/concepts/events-and-triggers.mdx | 180 +++++++ apps/wiki/content/docs/concepts/index.mdx | 49 ++ apps/wiki/content/docs/concepts/meta.json | 13 + .../docs/concepts/segments-and-filters.mdx | 150 ++++++ .../content/docs/concepts/templates-types.mdx | 112 +++++ apps/wiki/content/docs/guides/templates.mdx | 33 +- apps/wiki/content/docs/meta.json | 6 + .../tutorials/cart-abandonment-automation.mdx | 227 +++++++++ .../tutorials/event-tracking-integration.mdx | 475 ++++++++++++++++++ .../tutorials/first-transactional-email.mdx | 177 +++++++ apps/wiki/content/docs/tutorials/index.mdx | 37 ++ apps/wiki/content/docs/tutorials/meta.json | 13 + .../docs/tutorials/newsletter-campaign.mdx | 292 +++++++++++ .../tutorials/segment-based-targeting.mdx | 298 +++++++++++ .../tutorials/welcome-series-workflow.mdx | 156 ++++++ apps/wiki/openapi.json | 25 +- 23 files changed, 3336 insertions(+), 5 deletions(-) create mode 100644 apps/wiki/content/docs/automation-patterns/conditional-branching.mdx create mode 100644 apps/wiki/content/docs/automation-patterns/index.mdx create mode 100644 apps/wiki/content/docs/automation-patterns/meta.json create mode 100644 apps/wiki/content/docs/automation-patterns/visual-builder-guide.mdx create mode 100644 apps/wiki/content/docs/automation-patterns/workflow-patterns.mdx create mode 100644 apps/wiki/content/docs/concepts/campaigns-vs-workflows.mdx create mode 100644 apps/wiki/content/docs/concepts/contacts-and-data.mdx create mode 100644 apps/wiki/content/docs/concepts/events-and-triggers.mdx create mode 100644 apps/wiki/content/docs/concepts/index.mdx create mode 100644 apps/wiki/content/docs/concepts/meta.json create mode 100644 apps/wiki/content/docs/concepts/segments-and-filters.mdx create mode 100644 apps/wiki/content/docs/concepts/templates-types.mdx create mode 100644 apps/wiki/content/docs/tutorials/cart-abandonment-automation.mdx create mode 100644 apps/wiki/content/docs/tutorials/event-tracking-integration.mdx create mode 100644 apps/wiki/content/docs/tutorials/first-transactional-email.mdx create mode 100644 apps/wiki/content/docs/tutorials/index.mdx create mode 100644 apps/wiki/content/docs/tutorials/meta.json create mode 100644 apps/wiki/content/docs/tutorials/newsletter-campaign.mdx create mode 100644 apps/wiki/content/docs/tutorials/segment-based-targeting.mdx create mode 100644 apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx diff --git a/apps/wiki/content/docs/automation-patterns/conditional-branching.mdx b/apps/wiki/content/docs/automation-patterns/conditional-branching.mdx new file mode 100644 index 0000000..8b413a8 --- /dev/null +++ b/apps/wiki/content/docs/automation-patterns/conditional-branching.mdx @@ -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 diff --git a/apps/wiki/content/docs/automation-patterns/index.mdx b/apps/wiki/content/docs/automation-patterns/index.mdx new file mode 100644 index 0000000..8859ada --- /dev/null +++ b/apps/wiki/content/docs/automation-patterns/index.mdx @@ -0,0 +1,53 @@ +--- +title: Automation Patterns +description: Workflow patterns and examples +icon: Zap +--- + +## Learn workflows + + + + Use the drag-and-drop workflow editor + + + + Common workflow examples + + + +## By use case + + + + Welcome and activate new users + + + + Drive feature adoption + + + + Prevent churn and win back users + + + + Order confirmations and receipts + + + +## Advanced + + + + If/then logic in workflows + + + + Pass event data through workflows + + + + Debug workflow issues + + diff --git a/apps/wiki/content/docs/automation-patterns/meta.json b/apps/wiki/content/docs/automation-patterns/meta.json new file mode 100644 index 0000000..cfc1357 --- /dev/null +++ b/apps/wiki/content/docs/automation-patterns/meta.json @@ -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" + ] +} diff --git a/apps/wiki/content/docs/automation-patterns/visual-builder-guide.mdx b/apps/wiki/content/docs/automation-patterns/visual-builder-guide.mdx new file mode 100644 index 0000000..b66ba0f --- /dev/null +++ b/apps/wiki/content/docs/automation-patterns/visual-builder-guide.mdx @@ -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) diff --git a/apps/wiki/content/docs/automation-patterns/workflow-patterns.mdx b/apps/wiki/content/docs/automation-patterns/workflow-patterns.mdx new file mode 100644 index 0000000..4288e89 --- /dev/null +++ b/apps/wiki/content/docs/automation-patterns/workflow-patterns.mdx @@ -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) diff --git a/apps/wiki/content/docs/concepts/campaigns-vs-workflows.mdx b/apps/wiki/content/docs/concepts/campaigns-vs-workflows.mdx new file mode 100644 index 0000000..39c46c5 --- /dev/null +++ b/apps/wiki/content/docs/concepts/campaigns-vs-workflows.mdx @@ -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) diff --git a/apps/wiki/content/docs/concepts/contacts-and-data.mdx b/apps/wiki/content/docs/concepts/contacts-and-data.mdx new file mode 100644 index 0000000..26a4160 --- /dev/null +++ b/apps/wiki/content/docs/concepts/contacts-and-data.mdx @@ -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": "user@example.com", + "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": "user@example.com", + "subscribed": true, + "data": { + "firstName": "Sarah", + "plan": "pro" + } + }' +``` + +### Automatic upsert + +If email exists, updates instead of creating duplicate. + +```javascript +// First call +POST /contacts { email: "user@example.com", data: { plan: "free" } } + +// Second call - updates same contact +POST /contacts { email: "user@example.com", 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": "user@example.com", + "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 +

Hi {{firstName}}!

+

Your {{plan}} plan renews on {{renewalDate}}.

+``` + +### 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) diff --git a/apps/wiki/content/docs/concepts/events-and-triggers.mdx b/apps/wiki/content/docs/concepts/events-and-triggers.mdx new file mode 100644 index 0000000..4076b69 --- /dev/null +++ b/apps/wiki/content/docs/concepts/events-and-triggers.mdx @@ -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', 'user@example.com'); +``` + +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) diff --git a/apps/wiki/content/docs/concepts/index.mdx b/apps/wiki/content/docs/concepts/index.mdx new file mode 100644 index 0000000..068c32d --- /dev/null +++ b/apps/wiki/content/docs/concepts/index.mdx @@ -0,0 +1,49 @@ +--- +title: Core Concepts +description: Understand how Plunk works +icon: Lightbulb +--- + +## Email methods + + + + When to use each email method + + + +## Data & contacts + + + + Store and manage your audience + + + + Create dynamic audience groups + + + +## Sending emails + + + + Marketing vs Transactional templates + + + + Track behavior and trigger workflows + + + +## Performance & delivery + + + + Reach the inbox + + + + Optimize for millions of contacts + + diff --git a/apps/wiki/content/docs/concepts/meta.json b/apps/wiki/content/docs/concepts/meta.json new file mode 100644 index 0000000..41530b9 --- /dev/null +++ b/apps/wiki/content/docs/concepts/meta.json @@ -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" + ] +} diff --git a/apps/wiki/content/docs/concepts/segments-and-filters.mdx b/apps/wiki/content/docs/concepts/segments-and-filters.mdx new file mode 100644 index 0000000..7117520 --- /dev/null +++ b/apps/wiki/content/docs/concepts/segments-and-filters.mdx @@ -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) diff --git a/apps/wiki/content/docs/concepts/templates-types.mdx b/apps/wiki/content/docs/concepts/templates-types.mdx new file mode 100644 index 0000000..5c3f3be --- /dev/null +++ b/apps/wiki/content/docs/concepts/templates-types.mdx @@ -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: "user@example.com", 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: "user@example.com", 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": "

Click: {{resetLink}}

" + }' +``` + +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 +Unsubscribe +``` + +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) diff --git a/apps/wiki/content/docs/guides/templates.mdx b/apps/wiki/content/docs/guides/templates.mdx index 237ccce..39aa5e5 100644 --- a/apps/wiki/content/docs/guides/templates.mdx +++ b/apps/wiki/content/docs/guides/templates.mdx @@ -148,13 +148,15 @@ Access nested objects with dot notation: ### Transactional emails +Use the `template` field with the **template ID** (not the template name): + ```bash curl -X POST {{API_URL}}/v1/send \ -H "Authorization: Bearer sk_your_secret_key" \ -H "Content-Type: application/json" \ -d '{ "to": "user@example.com", - "template": "order-confirmation", + "template": "clx123abc456", "data": { "orderNumber": "12345", "deliveryDate": "March 20" @@ -162,6 +164,35 @@ curl -X POST {{API_URL}}/v1/send \ }' ``` +**Finding your template ID:** +- In the dashboard: Go to Templates → Click on your template → Copy the ID from the URL or template details +- Via API: Use `GET /templates` to list all templates with their IDs + +When using a template: +- **Subject, body, from, and reply-to** are automatically taken from the template +- **Template variables** (e.g., `{{orderNumber}}`) are populated from the `data` field +- You can **override** any template value by explicitly providing it in the request (see example below) + +### Overriding template values + +```bash +curl -X POST {{API_URL}}/v1/send \ + -H "Authorization: Bearer sk_your_secret_key" \ + -H "Content-Type: application/json" \ + -d '{ + "to": "user@example.com", + "template": "clx123abc456", + "subject": "Custom Subject (overrides template)", + "from": { + "name": "Custom Sender", + "email": "custom@example.com" + }, + "data": { + "orderNumber": "12345" + } + }' +``` + ### In workflows When creating a **Send Email** workflow step, select the template from the dropdown. Variables are automatically filled from contact data and workflow context. diff --git a/apps/wiki/content/docs/meta.json b/apps/wiki/content/docs/meta.json index b32dd27..c596140 100644 --- a/apps/wiki/content/docs/meta.json +++ b/apps/wiki/content/docs/meta.json @@ -3,8 +3,14 @@ "index", "---Getting Started---", "getting-started", + "---Tutorials---", + "tutorials", + "---Core Concepts---", + "concepts", "---Guides---", "guides", + "---Automation Patterns---", + "automation-patterns", "---API Reference---", "api-reference", "---Self-Hosting---", diff --git a/apps/wiki/content/docs/tutorials/cart-abandonment-automation.mdx b/apps/wiki/content/docs/tutorials/cart-abandonment-automation.mdx new file mode 100644 index 0000000..1af9db8 --- /dev/null +++ b/apps/wiki/content/docs/tutorials/cart-abandonment-automation.mdx @@ -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 diff --git a/apps/wiki/content/docs/tutorials/event-tracking-integration.mdx b/apps/wiki/content/docs/tutorials/event-tracking-integration.mdx new file mode 100644 index 0000000..c3e403a --- /dev/null +++ b/apps/wiki/content/docs/tutorials/event-tracking-integration.mdx @@ -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 ( + + {children} + + ); +} + +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 ; +} +``` + +## 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
...
; +} +``` + +### Server action + +```javascript +'use server'; + +import { trackEvent } from '@/lib/plunk'; + +export async function createProject(formData) { + const user = await getCurrentUser(); + const project = await db.projects.create({ + name: formData.get('name'), + userId: user.id + }); + + await trackEvent('project_created', user.email, { + projectId: project.id, + projectName: project.name + }); + + return project; +} +``` + +## Create a helper function + +```javascript +// lib/plunk.js +const PLUNK_PUBLIC_KEY = process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY; + +export async function trackEvent(event, email, data = {}) { + try { + const response = await fetch('{{API_URL}}/v1/track', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + event, + email, + data + }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('Failed to track event:', error); + // Don't throw - tracking shouldn't break your app + } +} +``` + +## Testing events + +### View tracked events + +1. Go to **Activity** in Plunk dashboard +2. Filter by event type +3. View event data payloads + +### Test locally + +```javascript +// Track a test event +await fetch('{{API_URL}}/v1/track', { + method: 'POST', + headers: { + 'Authorization': 'Bearer pk_your_key', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + event: 'test_event', + email: 'your-email@example.com', + data: { + test: true, + timestamp: new Date().toISOString() + } + }) +}); +``` + +Check Plunk Activity - event should appear within seconds. + +## Connect to workflows + +Once events are tracked, create workflows that trigger on them: + +1. **Workflows** → **Create Workflow** +2. Trigger: Event `user_signed_up` +3. Build your automation +4. Enable workflow + +Now when you track `user_signed_up`, the workflow runs automatically. + +## Performance considerations + +**Don't block user actions:** + +```javascript +// Good - fire and forget +handleClick() { + trackEvent('button_clicked', user.email); + // Don't await +} + +// Bad - user waits for tracking +async handleClick() { + await trackEvent('button_clicked', user.email); + // User has to wait +} +``` + +**Batch events for bulk operations:** + +```javascript +// If importing 1000 users, track events in background +async function importUsers(users) { + const imported = await db.users.bulkCreate(users); + + // Queue for background processing + await queue.add('track-events', { + event: 'user_imported', + users: imported + }); +} +``` + +**Add retry logic:** + +```javascript +async function trackEventWithRetry(event, email, data, retries = 3) { + for (let i = 0; i < retries; i++) { + try { + return await trackEvent(event, email, data); + } catch (error) { + if (i === retries - 1) throw error; + await new Promise(r => setTimeout(r, 1000 * (i + 1))); + } + } +} +``` + +## Common issues + +**Event tracked but workflow not triggering** +- Workflow is enabled +- Event name matches exactly (case-sensitive) +- Contact exists in Plunk +- Contact is subscribed + +**CORS errors in browser** +- Use public key (not secret key) +- Plunk API allows CORS from all origins + +**Contact not created** +- Email must be valid +- Contact is created automatically when event is tracked + +## Next steps + +- [Build a workflow](/tutorials/welcome-series-workflow) triggered by events +- [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) decision guide +- [Stripe integration](/integrations/stripe-billing) for billing events diff --git a/apps/wiki/content/docs/tutorials/first-transactional-email.mdx b/apps/wiki/content/docs/tutorials/first-transactional-email.mdx new file mode 100644 index 0000000..e9d2a8b --- /dev/null +++ b/apps/wiki/content/docs/tutorials/first-transactional-email.mdx @@ -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": "user@example.com", + "subject": "Reset your password", + "body": "

Click here to reset: Reset Password

", + "subscribed": true + }' +``` + +Replace `sk_your_secret_key` and `user@example.com` with your values. + +### With JavaScript + +```javascript +await fetch('{{API_URL}}/v1/send', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + to: user.email, + subject: 'Reset your password', + body: `

Click here: Reset Password

`, + 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'

Click here: Reset Password

', + '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: '

Hi {{name}}, click here: Reset

', + 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 +

Hi {{name}},

+

Click here to reset your password:

+

Reset Password

+

This link expires in 1 hour.

+``` + +### 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: `

Click here: Reset Password

`, + 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 diff --git a/apps/wiki/content/docs/tutorials/index.mdx b/apps/wiki/content/docs/tutorials/index.mdx new file mode 100644 index 0000000..8db94fc --- /dev/null +++ b/apps/wiki/content/docs/tutorials/index.mdx @@ -0,0 +1,37 @@ +--- +title: Tutorials +description: Step-by-step guides +icon: BookOpen +--- + +## Getting started + + + + Send a transactional email via API + + + + Create a 3-email automated workflow + + + + Broadcast to your audience + + + +## Advanced + + + + Filter and target specific audiences + + + + Track user behavior to trigger workflows + + + + Recover abandoned carts automatically + + diff --git a/apps/wiki/content/docs/tutorials/meta.json b/apps/wiki/content/docs/tutorials/meta.json new file mode 100644 index 0000000..d3d18ff --- /dev/null +++ b/apps/wiki/content/docs/tutorials/meta.json @@ -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" + ] +} diff --git a/apps/wiki/content/docs/tutorials/newsletter-campaign.mdx b/apps/wiki/content/docs/tutorials/newsletter-campaign.mdx new file mode 100644 index 0000000..9ec29be --- /dev/null +++ b/apps/wiki/content/docs/tutorials/newsletter-campaign.mdx @@ -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 +

What's new in March

+ +

Hi {{firstName ?? 'there'}},

+ +

We've been busy this month. Here's what's new:

+ +

🚀 New Feature: Team Collaboration

+

Invite team members and collaborate in real-time.

+ +

⚡ Improved Performance

+

Everything is now 2x faster.

+ +

📊 New Analytics Dashboard

+

Better insights into your data.

+ +

View Full Changelog

+ +

Thanks,
The Team

+ +

+ You're receiving this because you subscribed to updates. + Unsubscribe +

+``` + +### 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 +

Hi {{firstName ?? 'there'}},

+ +``` + +## 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 diff --git a/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx b/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx new file mode 100644 index 0000000..5afdd5c --- /dev/null +++ b/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx @@ -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 diff --git a/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx b/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx new file mode 100644 index 0000000..b550070 --- /dev/null +++ b/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx @@ -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 diff --git a/apps/wiki/openapi.json b/apps/wiki/openapi.json index 82ee4c9..e35ba59 100644 --- a/apps/wiki/openapi.json +++ b/apps/wiki/openapi.json @@ -251,7 +251,7 @@ }, "template": { "type": "string", - "description": "Template identifier to use" + "description": "Template ID to use for this email. When provided, uses the template's subject, body, from, and reply-to settings. You can override these by explicitly providing subject, body, from, or reply fields in the request. Template variables are populated from the data field." }, "from": { "oneOf": [ @@ -384,13 +384,30 @@ } }, "withTemplate": { - "summary": "Using template", + "summary": "Using a template", + "description": "Send email using a template. Provide the template ID and any data for template variables. The template's subject, body, from address, and reply-to will be used automatically.", "value": { "to": "user@example.com", - "template": "welcome-email", + "template": "clx123abc456", "data": { "firstName": "John", - "lastName": "Doe" + "lastName": "Doe", + "resetCode": { + "value": "ABC123", + "persistent": false + } + } + } + }, + "withTemplateOverride": { + "summary": "Using template with overrides", + "description": "You can override template values by providing subject, body, from, or reply fields. This example overrides the template's subject line.", + "value": { + "to": "user@example.com", + "template": "clx123abc456", + "subject": "Custom Subject Override", + "data": { + "firstName": "Jane" } } },