Formbird × Dreamlit Lifecycle email audit · Oct 30, 2026

Lifecycle email score

Formbird's lifecycle email is partly covered

Formbird sends an email at 4 of the 8 moments where one helps.

  1. Welcome

    Greets new signups and points them to the first step.

    Covered

    Formbird sends a welcome that links to creating a form.

    10 / 10
  2. Activation

    Nudges people who signed up but haven't taken the first key step.

    Missing

    Nothing in the code sends a nudge to publish a form.

    0 / 25 See change 1 ↓
  3. Recap

    Shows people the value they got from their own activity.

    Unknown

    You mentioned a Loops newsletter, but we couldn't check what it covers.

    0 / 5
  4. Trial ending

    Helps people decide what to do before their trial ends.

    Weak

    Formbird sends the same trial reminder to active and inactive people.

    10 / 20 See change 2 ↓
  5. Trial expired

    Gives people a way back after their trial ends without payment.

    In code

    The trial-expired email is in the code, but we couldn't confirm it sends.

    11.25 / 15
  6. Failed payment

    Helps people fix a failed payment and keep their subscription.

    Covered

    You told us you see Stripe send failed-payment emails.

    10 / 10
  7. Going quiet

    Gives people a reason to return when their activity drops.

    Missing

    Nothing reaches paying customers whose forms stop getting responses.

    0 / 10
  8. Win-back

    Invites back people who cancelled or left.

    Missing

    Nothing sends an email after someone cancels.

    0 / 5 See change 3 ↓
What each moment meansHide the detail

Counts from production. Read-only, Oct 30, without 7 internal and test accounts. Assumes a 14-day subscription trial.

Dreamlit can build these emails for you. Nothing sends until you approve.

Get started

What we found

The short version of this audit.

Suggested changes

More trials that turn into paying customers. Three changes, biggest likely gain first.

1

Activation

Help people publish their first form

Only 15 of 81 people stuck on day 2 publish by day 14.

Who

38

people eligible now

Signed up 2–14 days ago, nothing published

What to send

  1. Day 0Welcome: create a form
  2. Day 2Nudge to publishFormbirdYour first form is two minutes awayStart from a template, publish it and share the link. Responses land in your inbox.
  3. Day 5One more if still stuck

Both stop once they publish.

Intended outcome

65%today

publish a form within 14 days of signup

Trigger and evidence

No activation follow-up in app/, lib/ or supabase/. Read-only counts: 38 eligible now; 15 of 81 previously stuck people published by day 14.

Missing

SELECT p.id, p.email, p.first_name
FROM profiles p
WHERE p.created_at <  now() - interval '2 days'
  AND p.created_at >= now() - interval '14 days'
  AND p.deleted_at IS NULL
  AND NOT EXISTS (
    SELECT 1 FROM forms f
    WHERE f.owner_id = p.id AND f.published_at IS NOT NULL
  );
Data coverage
Email address: 38 of 38. Unsubscribed: 2 of 38. First name: 31 of 38 (82%). Workspace name: 38 of 38.
By segment
By signup_source: Google Ads 24 · organic 11 · referral 3
2

Trial ending

Match trial reminders to what they've done

Active and inactive trials get the same reminder today.

Who

31

trials eligible now

Trial ends in the next 7 days

What to send

  1. 3 days leftActive: what paying keepsFormbirdKeep your forms collecting responsesYour trial ends in 3 days. Upgrade and your forms stay live, with every response kept.
  2. 3 days leftNot started: help publishing
  3. Last dayThe reminder

All stop once they pay.

Intended outcome

22%today

of ended trials become paid

Trigger and evidence

app/api/cron/trial-reminders/route.ts:22; production counts: 31 trials end in the next 7 days, 41 of 187 ended trials became paid.

Weak

SELECT p.id, p.email,
       EXISTS (SELECT 1 FROM forms f
               WHERE f.owner_id = p.id
                 AND f.published_at IS NOT NULL) AS published
FROM subscriptions sub
JOIN profiles p ON p.id = sub.user_id
WHERE sub.status = 'trialing'
  AND sub.trial_end >= now()
  AND sub.trial_end <  now() + interval '7 days';
Data coverage
Email address: 31 of 31. Trial end date: 31 of 31. Whether they published: from forms, 31 of 31.
By segment
By signup_source: Google Ads 17 · organic 10 · referral 4
3

Win-back

Follow up after a customer cancels

Nothing reaches customers after they cancel.

Who

23

former customers eligible now

Cancelled in the last 90 days, not back

What to send

  1. Day 0Cancels
  2. Day 1Ask what didn’t workFormbirdWhat should we have done better?You cancelled Formbird yesterday. One line back tells us what to fix first.
  3. LaterNote when their fix ships

Stops if they opt out.

Intended outcome

4%today

of cancelled customers pay again within 90 days

Trigger and evidence

app/api/stripe/webhook/route.ts:118; production counts: 23 eligible cancellations, 2 of 51 older cancellations returned within 90 days.

Missing

SELECT p.id, p.email
FROM subscriptions sub
JOIN profiles p ON p.id = sub.user_id
WHERE sub.status = 'canceled'
  AND sub.canceled_at >= now() - interval '90 days'
  AND NOT EXISTS (
    SELECT 1 FROM subscriptions s2
    WHERE s2.user_id = p.id AND s2.status = 'active'
  );
Data coverage
Email address: 23 of 23. Unsubscribed: 0 of 23. Cancel reason: 9 of 23 (subscriptions.cancel_reason).

One broadcast worth sending

Bring back people who never published a form

The new nudge only reaches new signups, not them.

What to send

FormbirdYour first form is still waitingPick a template, publish it and share one link. Responses start landing in your inbox.

Send once. Skip anyone who has published, cancelled or unsubscribed.

Who

57

people who never published

Signed up 15–90 days ago

Intended outcome

People who publish within a week

Fix these before you send

The detail behind the plan

Other opportunities
4

Going quiet

Check in when a customer's forms go quiet

Twelve paying customers stopped getting responses. A check-in tells a finished survey apart from a broken form.

Who

12

customers eligible now

Paying, had responses before, none in 14 days

Medium confidence: forms for one-off events go quiet on their own.

What to send

  1. Day 0Last response comes in
  2. Day 14Stuck, between surveys, or done?

At most once every 60 days.

Intended outcome

–

Customers getting responses again within 30 days

Not measured yet

Trigger and evidence

No inactivity follow-up in app/, lib/ or supabase/. Production counts: 12 paying customers had no responses for 14 days; forms for one-off events may be included.

Missing

SELECT p.id, p.email
FROM profiles p
JOIN subscriptions sub ON sub.user_id = p.id AND sub.status = 'active'
WHERE EXISTS (
    SELECT 1 FROM responses r JOIN forms f ON f.id = r.form_id
    WHERE f.owner_id = p.id
      AND r.created_at >= now() - interval '44 days'
      AND r.created_at <  now() - interval '14 days')
  AND NOT EXISTS (
    SELECT 1 FROM responses r JOIN forms f ON f.id = r.form_id
    WHERE f.owner_id = p.id
      AND r.created_at >= now() - interval '14 days');
Data coverage
Email address: 12 of 12. Activity is read from responses, so forms for one-off events count as quiet too.
5

Recap

Check whether the newsletter already covers a recap

A weekly recap could show each customer what their forms collected. Check the existing newsletter before adding another email.

Who

129

customers eligible now

Got responses in the last 7 days

What to send

  1. MonthlyLoops newsletter
  2. MondayTheir week's responses

Skips anyone with no responses.

Intended outcome

88%today

active again the following week

Check first Your Loops newsletter may already show each customer their own responses. If it does, skip this one.

Trigger and evidence

You named a Loops newsletter but its content wasn't checked. Production counts: 129 customers got responses in the last 7 days.

Unknown

SELECT p.id, p.email,
       count(DISTINCT f.id) AS forms_with_responses,
       count(r.id)          AS responses_last_week
FROM profiles p
JOIN forms f     ON f.owner_id = p.id
JOIN responses r ON r.form_id = f.id
WHERE r.created_at >= now() - interval '7 days'
GROUP BY p.id, p.email;
Data coverage
Email address: 129 of 129. Responses per form: from responses, 129 of 129.
Email coverage
  1. 1

    Welcome with a first step

    Covered

    Sent when they confirm their email, with a Create your first form button.

  2. 2

    Activation nudge

    Missing

    Nothing reaches people who signed up but haven't published a form.

  3. 3

    Habit or value recap

    Unknown

    Your Loops newsletter might cover it. We couldn't see it.

  4. 4

    Trial ending

    Weak

    Goes to everyone the day before, whatever they did.

  5. 5

    Trial expired

    In code, not verified

    In the code, but nothing shows it going out.

  6. 6

    Failed payment

    Covered

    Stripe's own failed payment emails go out.

  7. 7

    Going quiet

    Missing

    Nothing notices when a paying customer's forms stop getting responses.

  8. 8

    Win-back

    Missing

    Nothing goes out after someone cancels.

Left out, doesn't apply: Free plan limit (there's a trial instead).

6 emails found in your code, plus 2 you told me about

Account and product emails are listed here but don't count toward the lifecycle score.

Sending checks · 3 of 7 passed

1 unknown. DMARC, reply-to and unsubscribe need fixing. These checks are separate from the lifecycle score.

  • SPFPass

    send.formbird.example has v=spf1 include:amazonses.com ~all (Resend's sending subdomain).

  • DKIMPass

    Found resend._domainkey.formbird.example.

  • DMARCFail

    No record at _dmarc.formbird.example. Gmail and Yahoo require one from bulk senders.

    Fix: Add a TXT record at _dmarc.formbird.example with v=DMARC1; p=none; rua=mailto:dmarc@formbird.example, then tighten it once reports look clean. To check a real message: Send Test Email tool.

  • Account emails branded, through your own senderPass

    Supabase Auth sends through Resend with your own templates (supabase/config.toml:88, supabase/templates/).

  • A real reply-toFail

    Lifecycle emails come from no-reply@formbird.example with no reply-to (lib/email.ts:9).

    Fix: Set replyTo to an inbox someone reads. Replies to these emails are some of the best feedback you'll get.

  • One-click unsubscribe on lifecycle emailFail

    Trial emails go out without List-Unsubscribe headers (lib/email.ts:14).

    Fix: Add List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click headers to lifecycle emails, and stop sending to people who use them. Gmail and Yahoo require it from bulk senders.

  • Bounces and complaints handledUnknown

    No webhook for Resend's bounce and complaint events. Resend stops sending to bounced addresses on its own, but your app doesn't know about them.

    Fix: Handle email.bounced and email.complained in a webhook and mark those people, so lifecycle email skips them.

How this is scored

Ship the activation nudge and you're at 66. This is a coverage score, not a forecast of results.

Up to 46 if the unknown moment is covered.

Moment Weight Status Points Weight × points Evidence
Welcome with a first step 10 Covered 1 10 app/auth/callback/route.ts:48; you told me you see them in Resend
Activation nudge 25 Missing 0 0 Nothing found in app/, lib/, supabase/; you told me nothing else sends it
Habit or value recap 5 Unknown 0 0 Nothing in code; your Loops newsletter might cover it, content not seen
Trial ending 20 Weak 0.5 10 app/api/cron/trial-reminders/route.ts:22; same message for everyone, 1 day before
Trial expired 15 In code, not verified 0.75 11.25 app/api/cron/trial-reminders/route.ts:57; no email log to show it sends
Failed payment 10 Covered 1 10 You told me you see Stripe's failed payment emails going out; app/api/stripe/webhook/route.ts:91
Going quiet 10 Missing 0 0 Nothing found in app/, lib/, supabase/; you told me nothing else sends it
Win-back 5 Missing 0 0 Nothing found; the cancel webhook only updates the status (app/api/stripe/webhook/route.ts:118)
Total 100 41.25 Score = 100 × 41.25 ÷ 100 = 41.25 → 41
  • Points: covered 1, you said it's covered 0.75, found in code but not verified 0.75, weak 0.5, missing 0, unknown 0. Moments that don't apply are left out.
  • Score = 100 × the sum of weight × points ÷ the sum of the weights, rounded. "Up to" counts unknown moments as covered. Each moment is worth 100 × its weight ÷ the sum of the weights.
  • 0–39 Big gaps · 40–74 Partly covered · 75–100 Well covered. These are labels, not a comparison with other apps.
  • Priority = open points × people eligible now × count confidence (high 1, medium 0.6, low 0.3). Known gaps come before unknowns. Without comparable production counts, rank by open points instead.
  • After #1: 100 × (41.25 + 25) ÷ 100 = 66.25 → 66, counting the activation nudge as covered.
  • Every status cites its evidence, so your agent can recompute this. The score can shift a little between runs.
Rates and counting windows

Baseline rates, with the counts and time windows used.

Left out 7 accounts: 5 from your own team and 2 marked as test accounts.

Moment Rate Counts Window Today
Welcome First step within a day Created a form within 24 hours ÷ signed up Last 30 days, ending yesterday 70% (134 of 192)
Activation nudge Activation rate Published within 14 days ÷ signed up Signed up 44–14 days ago 65% (120 of 186)
Recap Weekly return Active this week and next ÷ active this week Last two full weeks 88% (113 of 129)
Trial ending Trial to paid Became paid ÷ trials that ended Last 30 days 22% (41 of 187)
Trial expired Late conversions Paid within 30 days after ÷ trials that ended unpaid Trial ended 60–30 days ago 3% (4 of 139)
Failed payment Failed payments paid Paid within 14 days ÷ failed payments Last 60 days, ending 14 days ago 82% (9 of 11)
Going quiet Return rate Came back within 30 days ÷ went quiet Went quiet 60–30 days ago not counted
Win-back Came back Paid again within 90 days ÷ cancelled Cancelled 180–90 days ago 4% (2 of 51)

Put lifecycle email on autopilot

Connect Dreamlit to your agent and it builds everything above, lifecycle emails and one-time broadcasts, so each one reacts to what your users do in your app. Preview it with real data. Nothing goes live until you approve it.

Get started

7-day free trial with a card, then $20 a month on Pro.

If you'd rather build it yourself, the triggers above are yours.

The button shares only this app's name, score and top moments.