Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 188 additions & 4 deletions emails/CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,23 @@ Generated from the email library with `bun run emails:catalog`. Edit the source

## Lifecycle flows

These are the locally configured draft journeys. This document is not a live status check. Imports remain held; production enrollment is not deployed. Run `bun run emails:check-loops` to verify the actual Loops drafts.
These are the locally configured journeys. This document is not a live status check. Imports remain held. Run `bun run emails:check-loops --structure-only --allow-live` to verify the remote workflow structure and status; review custom MJML content in the browser.

| Flow | Audience | Schedule after entry | Emails |
| --- | --- | --- | --- |
| [Independent free-user onboarding](https://app.loops.so/workflows/cmtvpih6201fp0jznodf8fpwz) | free | Day 0, Day 2, Day 5, Day 9 | 4 |
| [Customer onboarding](https://app.loops.so/workflows/cmtvpilm601f40jzwx3474cko) | customer | Day 0, Day 3, Day 7 | 3 |
| [Free activation and Pro conversion v2](https://app.loops.so/workflows/cmtxlhkcw0dta0jzkd6an7xz0) | free | Day 0, Day 1, Day 3, Day 8, Day 10, Day 12 | 6 |
| [Teammate onboarding](https://app.loops.so/workflows/cmtvpm0ag01it0j37w02o6wav) | teammate | Day 0, Day 3 | 2 |
| [Former customer follow-up](https://app.loops.so/workflows/cmtvpm8zm01ii0j01cz7d15qr) | former | Day 14 | 1 |

Completed signups reach Loops through Stripe; SSO uses a small direct fallback. Cap supplies targeting through a durable sync queue, without a separate marketing opt-in step. Existing opt-outs and suppressions take precedence. The integration is not deployed. Historical imports stay held; a new accepted invitation can start teammate help only.
Completed signups reach Loops through Stripe; SSO uses a small direct fallback. Cap supplies targeting through a durable sync queue, without a separate marketing opt-in step. Existing opt-outs and suppressions take precedence. Historical imports stay held; a new accepted invitation can start teammate help only.

Current draft journeys require global subscription, capConsent=subscribed, the exact audience, lifecycle enabled and onboarding eligible. capConsent is a legacy migration guard, not a separate consent-capture requirement for new signups. These filters continue to apply downstream. Free/former promotional flows additionally exclude teammates and require promotional eligibility.
Journeys require global subscription, capConsent=subscribed, the exact audience, lifecycle enabled and onboarding eligible. capConsent is a legacy migration guard, not a separate consent-capture requirement for new signups. These filters continue to apply downstream. Free/former promotional flows additionally exclude teammates and require promotional eligibility.

Teammate history takes priority over paid/free classification. Ambiguous contacts receive no journey. [Audience classification and consent](../scripts/loops/README.md#audience-rules).

An independent watchdog can block all four automatic journeys by adding an impossible subscription condition to their downstream guards. Recovery never resumes delivery automatically. This does not cancel manually scheduled campaigns; check sync health before every campaign send. See the [outage and resume procedure](../scripts/loops/README.md#outage-protection).
The independent watchdog checks every registered journey and alerts when an active flow requires a manual pause. It can hold paused/draft journeys with an impossible subscription condition. Recovery never resumes delivery automatically. Check sync health before campaign sends. See the [outage and resume procedure](../scripts/loops/README.md#outage-protection) and [onboarding runtime](onboarding-runtime.md).

### Independent free-user onboarding

Expand Down Expand Up @@ -76,6 +77,53 @@ flowchart TD
| 3 | [One less thing to explain twice](../emails/marketing/customer-workflow.ts) | Journey guards |
| 7 | [How is Cap working for you?](../emails/marketing/customer-feedback.ts) | Journey guards |

### Free activation and Pro conversion v2

Entry: capLifecycleStage changes into free-v2. Re-entry is disabled. Delays below are relative to the previous step; day numbers are cumulative from entry.

Downstream conditions: subscribed isTrue; capConsent equals subscribed; capAudience equals free; capTeammate isFalse; capPromotionalEligible isTrue; capLifecycleEnabled isTrue; capOnboardingEligible isTrue; capLifecycleStage equals free-v2.

```mermaid
flowchart TD
entry["Stage becomes free-v2"] --> guard["Consent and audience guards"]
guard --> email0["Day 0: welcome"]
email0 --> wait1["Wait 1 days"]
wait1 --> branch1{"capNeedsRecordingHelp is true?"}
branch1 -->|Yes| email1["Day 1: record"]
branch1 -->|No: skip| next1((Continue))
email1 --> next1
next1 --> wait2["Wait 2 days"]
wait2 --> branch2{"capReadyForPro is true?"}
branch2 -->|Yes| email2["Day 3: plans"]
branch2 -->|No: skip| next2((Continue))
email2 --> next2
next2 --> wait3["Wait 5 days"]
wait3 --> branch3{"capNeedsSharingHelp is true?"}
branch3 -->|Yes| email3["Day 8: share"]
branch3 -->|No: skip| next3((Continue))
email3 --> next3
next3 --> wait4["Wait 2 days"]
wait4 --> branch4{"capReadyForPro is true?"}
branch4 -->|Yes| email4["Day 10: ai"]
branch4 -->|No: skip| next4((Continue))
email4 --> next4
next4 --> wait5["Wait 2 days"]
wait5 --> branch5{"capNeedsRecordingHelp is true?"}
branch5 -->|Yes| email5["Day 12: help"]
branch5 -->|No: skip| next5((Continue))
email5 --> next5
next5 --> exit["End"]
```

| Day | Subject and source | Send condition |
| --- | --- | --- |
| 0 | [Your first Cap only needs 30 seconds](../emails/marketing/free-v2-welcome.ts) | Journey guards |
| 1 | [One small thing to record today](../emails/marketing/free-v2-record.ts) | `capNeedsRecordingHelp=true` |
| 3 | [Some explanations need more than five minutes](../emails/marketing/free-v2-plans.ts) | `capReadyForPro=true` |
| 8 | [Put your recording to work](../emails/marketing/free-v2-share.ts) | `capNeedsSharingHelp=true` |
| 10 | [Record the walkthrough. Skip the extra write-up.](../emails/marketing/free-v2-ai.ts) | `capReadyForPro=true` |
| 12 | [Anything getting in the way?](../emails/marketing/free-v2-help.ts) | `capNeedsRecordingHelp=true` |

### Teammate onboarding

Entry: capLifecycleStage changes into teammate. Re-entry is disabled. Delays below are relative to the previous step; day numbers are cumulative from entry.
Expand Down Expand Up @@ -300,6 +348,142 @@ Just reply here. I read every reply, and hearing how people actually use Cap hel

Thanks again for backing us :)

### free-v2-welcome

Help a new independent free user record and share one useful explanation.

**Subject:** Your first Cap only needs 30 seconds

**Preview:** Record one thing, send the link, and you're off.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-welcome.ts](../emails/marketing/free-v2-welcome.ts)

{contact.capGreeting}

Richie here, founder of Cap. Thanks for giving it a go :)

For your first recording, pick something you'd normally explain in a long message. Open Cap, choose Instant Mode, and spend 30 seconds showing it on screen.

Once it's ready, send the link to someone who needs that explanation. No polished presentation needed.

Download Cap and make your first recording

If you get stuck, reply here and I'll help.

### free-v2-record

Offer a small first cloud recording task when no completed video is visible.

**Subject:** One small thing to record today

**Preview:** Try explaining something you already know.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-record.ts](../emails/marketing/free-v2-record.ts)

{contact.capGreeting}

If you're still finding your feet with Cap, try recording a quick walkthrough of something you already know: a setting, a page, or a problem you want to show someone.

Choose Instant Mode, keep it short, then send the link when it's ready.

Get started with your first recording

If screen or microphone permissions are getting in the way, reply with what you're seeing and I'll help you sort it.

### free-v2-plans

Offer Pro to active free users through a specific cloud-sharing benefit.

**Subject:** Some explanations need more than five minutes

**Preview:** Share the full walkthrough with Cap Pro.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-plans.ts](../emails/marketing/free-v2-plans.ts)

{contact.capGreeting}

Five minutes works for a quick question. A full walkthrough sometimes needs longer.

Cap Pro removes the five-minute limit on cloud recordings and gives you unlimited shareable links, so you can send the whole explanation in one video.

It also includes the desktop commercial license for work recordings.

Pro is US$12 per user, billed monthly. Annual billing is also available.

Upgrade to Cap Pro

### free-v2-share

Help an active user put a completed recording into a real conversation.

**Subject:** Put your recording to work

**Preview:** Send the link with one sentence about what you need.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-share.ts](../emails/marketing/free-v2-share.ts)

{contact.capGreeting}

A useful way to share a Cap is to add one sentence telling the other person what you need from them.

"Here's the bit I'm stuck on. Can you take a look?"

Or: "Here's how to change that setting. Does that solve it?"

Open your recordings

Send the link wherever you're already having the conversation.

### free-v2-ai

Show active free users how Pro reduces the work around a recording.

**Subject:** Record the walkthrough. Skip the extra write-up.

**Preview:** Give people a summary and chapters alongside your video.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-ai.ts](../emails/marketing/free-v2-ai.ts)

{contact.capGreeting}

A recording saves you typing everything out. Writing a summary afterwards can feel like doing the job twice.

Cap Pro generates a title, summary, transcript and clickable chapters for your recordings. The person watching can get the context, then jump to the part they need.

That's especially useful for walkthroughs people come back to later.

See Cap Pro

### free-v2-help

Invite a reply from users who have not reached a completed cloud recording.

**Subject:** Anything getting in the way?

**Preview:** Reply and tell me where you're getting stuck.

**Variables:** `contact.capGreeting` (fallback: Hey,)

**Edit:** [emails/marketing/free-v2-help.ts](../emails/marketing/free-v2-help.ts)

{contact.capGreeting}

If you haven't found a useful way to fit Cap into your day yet, is anything getting in the way?

Maybe you're not sure what to record, something isn't working, or it isn't quite what you expected.

Reply and let me know. If I can help you get a useful first recording out of it, I'd like to.

### teammate-welcome

Help an invited teammate find their workspace without promoting upgrades.
Expand Down
6 changes: 6 additions & 0 deletions emails/audiences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export const contactProperties = {
capLifecycleStage: "string",
capHasVideo: "boolean",
capHasSharedVideo: "boolean",
capReadyForPro: "boolean",
capNeedsSharingHelp: "boolean",
capNeedsRecordingHelp: "boolean",
capFreeOnboardingVariant: "string",
capFreeOnboardingExperiment: "string",
capFreeOnboardingAssignedAt: "date",
capVerifiedAt: "date",
capImportedAt: "date",
capSignupAt: "date",
Expand Down
3 changes: 3 additions & 0 deletions emails/delivery-safety.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const workflowAudience = (journey: Journey) => ({
...audienceFilter(journey.audience, journey.promotional).conditions,
condition("capLifecycleEnabled", true),
condition("capOnboardingEligible", true),
...(journey.key === "free-v2"
? [condition("capLifecycleStage", "free-v2")]
: []),
],
});

Expand Down
40 changes: 40 additions & 0 deletions emails/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { formerFeedback } from "./marketing/former-feedback";
import { freePlans } from "./marketing/free-plans";
import { freeRecord } from "./marketing/free-record";
import { freeShare } from "./marketing/free-share";
import { freeV2Ai } from "./marketing/free-v2-ai";
import { freeV2Help } from "./marketing/free-v2-help";
import { freeV2Plans } from "./marketing/free-v2-plans";
import { freeV2Record } from "./marketing/free-v2-record";
import { freeV2Share } from "./marketing/free-v2-share";
import { freeV2Welcome } from "./marketing/free-v2-welcome";
import { freeWelcome } from "./marketing/free-welcome";
import { teammateHandoff } from "./marketing/teammate-handoff";
import { teammateWelcome } from "./marketing/teammate-welcome";
Expand Down Expand Up @@ -42,6 +48,40 @@ export const journeys: Journey[] = [
{ ...customerFeedback, delayDays: 4 },
],
},
{
key: "free-v2",
name: "Cap | Free activation and Pro conversion v2",
audience: "free",
promotional: true,
messages: [
{ ...freeV2Welcome, delayDays: 0 },
{
...freeV2Record,
delayDays: 1,
onlyIf: { property: "capNeedsRecordingHelp", value: true },
},
{
...freeV2Plans,
delayDays: 2,
onlyIf: { property: "capReadyForPro", value: true },
},
{
...freeV2Share,
delayDays: 5,
onlyIf: { property: "capNeedsSharingHelp", value: true },
},
{
...freeV2Ai,
delayDays: 2,
onlyIf: { property: "capReadyForPro", value: true },
},
{
...freeV2Help,
delayDays: 2,
onlyIf: { property: "capNeedsRecordingHelp", value: true },
},
],
},
{
key: "teammate",
name: "Cap | Teammate onboarding",
Expand Down
18 changes: 18 additions & 0 deletions emails/marketing/free-v2-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { EmailDefinition } from "../types";

export const freeV2Ai = {
id: "free-v2-ai",
key: "ai",
purpose:
"Show active free users how Pro reduces the work around a recording.",
subject: "Record the walkthrough. Skip the extra write-up.",
previewText: "Give people a summary and chapters alongside your video.",
variables: ["capGreeting"],
body: [
"<Paragraph>{contact.capGreeting}</Paragraph>",
"<Paragraph>A recording saves you typing everything out. Writing a summary afterwards can feel like doing the job twice.</Paragraph>",
"<Paragraph><Strong>Cap Pro</Strong> generates a title, summary, transcript and clickable chapters for your recordings. The person watching can get the context, then jump to the part they need.</Paragraph>",
"<Paragraph>That's especially useful for walkthroughs people come back to later.</Paragraph>",
'<Paragraph><Link href="https://cap.so/login?next=%2Fdashboard%2Fsettings%2Forganization%2Fbilling%3Futm_source%3Dloops%26utm_medium%3Demail%26utm_campaign%3Dfree-pro-v2%26utm_content%3Dai">See Cap Pro</Link></Paragraph>',
].join(""),
} satisfies EmailDefinition;
17 changes: 17 additions & 0 deletions emails/marketing/free-v2-help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { EmailDefinition } from "../types";

export const freeV2Help = {
id: "free-v2-help",
key: "help",
purpose:
"Invite a reply from users who have not reached a completed cloud recording.",
subject: "Anything getting in the way?",
previewText: "Reply and tell me where you're getting stuck.",
variables: ["capGreeting"],
body: [
"<Paragraph>{contact.capGreeting}</Paragraph>",
"<Paragraph>If you haven't found a useful way to fit Cap into your day yet, is anything getting in the way?</Paragraph>",
"<Paragraph>Maybe you're not sure what to record, something isn't working, or it isn't quite what you expected.</Paragraph>",
"<Paragraph>Reply and let me know. If I can help you get a useful first recording out of it, I'd like to.</Paragraph>",
].join(""),
} satisfies EmailDefinition;
19 changes: 19 additions & 0 deletions emails/marketing/free-v2-plans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { EmailDefinition } from "../types";

export const freeV2Plans = {
id: "free-v2-plans",
key: "plans",
purpose:
"Offer Pro to active free users through a specific cloud-sharing benefit.",
subject: "Some explanations need more than five minutes",
previewText: "Share the full walkthrough with Cap Pro.",
variables: ["capGreeting"],
body: [
"<Paragraph>{contact.capGreeting}</Paragraph>",
"<Paragraph>Five minutes works for a quick question. A full walkthrough sometimes needs longer.</Paragraph>",
"<Paragraph><Strong>Cap Pro</Strong> removes the five-minute limit on cloud recordings and gives you unlimited shareable links, so you can send the whole explanation in one video.</Paragraph>",
"<Paragraph>It also includes the desktop commercial license for work recordings.</Paragraph>",
"<Paragraph>Pro is US$12 per user, billed monthly. Annual billing is also available.</Paragraph>",
'<Paragraph><Link href="https://cap.so/login?next=%2Fdashboard%2Fsettings%2Forganization%2Fbilling%3Futm_source%3Dloops%26utm_medium%3Demail%26utm_campaign%3Dfree-pro-v2%26utm_content%3Dplans">Upgrade to Cap Pro</Link></Paragraph>',
].join(""),
} satisfies EmailDefinition;
18 changes: 18 additions & 0 deletions emails/marketing/free-v2-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { EmailDefinition } from "../types";

export const freeV2Record = {
id: "free-v2-record",
key: "record",
purpose:
"Offer a small first cloud recording task when no completed video is visible.",
subject: "One small thing to record today",
previewText: "Try explaining something you already know.",
variables: ["capGreeting"],
body: [
"<Paragraph>{contact.capGreeting}</Paragraph>",
"<Paragraph>If you're still finding your feet with Cap, try recording a quick walkthrough of something you already know: a setting, a page, or a problem you want to show someone.</Paragraph>",
"<Paragraph>Choose <Strong>Instant Mode</Strong>, keep it short, then send the link when it's ready.</Paragraph>",
'<Paragraph><Link href="https://cap.so/download?utm_source=loops&amp;utm_medium=email&amp;utm_campaign=free-pro-v2&amp;utm_content=record">Get started with your first recording</Link></Paragraph>',
"<Paragraph>If screen or microphone permissions are getting in the way, reply with what you're seeing and I'll help you sort it.</Paragraph>",
].join(""),
} satisfies EmailDefinition;
19 changes: 19 additions & 0 deletions emails/marketing/free-v2-share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { EmailDefinition } from "../types";

export const freeV2Share = {
id: "free-v2-share",
key: "share",
purpose:
"Help an active user put a completed recording into a real conversation.",
subject: "Put your recording to work",
previewText: "Send the link with one sentence about what you need.",
variables: ["capGreeting"],
body: [
"<Paragraph>{contact.capGreeting}</Paragraph>",
"<Paragraph>A useful way to share a Cap is to add one sentence telling the other person what you need from them.</Paragraph>",
"<Paragraph>\"Here's the bit I'm stuck on. Can you take a look?\"</Paragraph>",
'<Paragraph>Or: "Here\'s how to change that setting. Does that solve it?"</Paragraph>',
'<Paragraph><Link href="https://cap.so/login?next=%2Fdashboard%2Fcaps%3Futm_source%3Dloops%26utm_medium%3Demail%26utm_campaign%3Dfree-pro-v2%26utm_content%3Dshare">Open your recordings</Link></Paragraph>',
"<Paragraph>Send the link wherever you're already having the conversation.</Paragraph>",
].join(""),
} satisfies EmailDefinition;
Loading
Loading