Which product events to track for email: a starter list for SaaS

Analytics tracking plans list fifty events. Email needs about ten, each one tied to a message it starts, stops or fills in. Here is the starter list, how to name the events, what to put in them, and a spec you can copy.

The Pombo team ·

Search for "which events to track" and you get product analytics tracking plans: every click, every page, every feature, piped into a dashboard. That's a fine plan for answering questions about usage. It is the wrong plan for email.

Email needs a much shorter list. An event belongs on it only if it changes what someone receives. Everything else can live in your analytics tool, where nobody gets an email because of it.

This post gives you that shorter list: ten events that cover most of what a small SaaS product emails about, the naming rule that keeps them readable, the properties worth sending with each, and a spec you can paste into your repo. It picks up where event-triggered email leaves off. That post covers why email should react to what users do and how an event reaches a sequence. This one covers which events to send.

The test for an email event

Before you add an event, name the email it drives. There are three jobs an event can do, and it should do at least one:

  • Start something. It enrolls the user in a sequence or sends a transactional email. user.signed_up starts the welcome sequence.
  • Stop something. It counts as a goal, so a sequence stops nudging someone who already did the thing. subscription.started ends the trial sequence.
  • Fill something in. Its properties supply the details an email quotes: the plan name, the amount, the project the user just created.

If you can't name the email, leave the event out. You can always add it later, and an event nobody listens to is only noise in your logs.

How to name them

Use object.past_tense, lowercase, with dots between the parts:

user.signed_up
project.created
subscription.cancelled

A few rules keep the list readable a year from now:

  • Past tense. An event records something that already happened. payment.failed, not payment.fail or fail_payment.
  • The object first. Related events sort together: subscription.started, subscription.upgraded, subscription.cancelled.
  • Your product's nouns. If your users make "boards", fire board.created, not item.created. The name should read like a sentence about your product.
  • No values in the name. plan.upgraded_to_pro becomes one event per plan. Send subscription.upgraded with the plan as a property instead.
  • Pick once. The name is the only link between your backend and the sequence listening for it. Renaming it later means changing both.

In Pombo, event names match regardless of case, but pick one style and keep to it anyway. The people reading your code will thank you.

The starter list

Ten events, grouped by the stage of the customer's life they belong to. You won't need all of them on day one. The last section covers where to start.

Getting started

1. user.signed_up Fire it when the account is created and saved. It starts your welcome sequence and it's the event almost every other flow builds on. Properties: first_name, signup_source, plan.

2. onboarding.completed Fire it when a user finishes whatever setup your product needs before it's useful: connected a data source, installed the snippet, imported their list. It makes a good goal: set it on the welcome sequence with stopping turned on, and anyone who finishes setup stops getting the "here's how to set up" emails. Properties: steps_completed, days_since_signup.

3. The first real result Name this one after your product: report.created, invoice.sent, site.published. It's the moment a user first gets what they signed up for. Fire it on the first occurrence only, and use it to send a short "nice, here's what to try next" email while the moment is still fresh. Properties: the name or title of the thing, and a link to it.

Growing

4. teammate.invited In a product where teams matter, an invitation is a strong sign a trial will convert. It can send the inviter a note about roles and permissions, and it's a useful goal for a sequence that encourages collaboration. Properties: inviter_name, team_name.

5. feature.first_used Fire it the first time a user touches a feature you know predicts retention. Send the feature's name as a property rather than creating one event per feature. It works as a goal for a feature adoption sequence: stop explaining a feature once someone has used it. Properties: feature.

Paying

6. trial.started If signup and trial start happen at different times (a user picks a plan later, or a trial needs a card), track the trial separately. It starts the trial sequence. Properties: plan, trial_ends_on as a readable date.

7. subscription.started The conversion. It's the goal for your trial sequence, and it can start a short "welcome to the paid plan" sequence of its own. Properties: plan, amount, billing_interval.

8. payment.failed A card was declined on renewal. The notice that the payment failed is transactional and must always send. The reminders that follow can be a sequence, with payment.succeeded as its goal so they stop the moment the card works. Properties: amount, plan, update_payment_url.

Leaving

9. subscription.cancelled Fire it when the user cancels, not when access ends. It can send a short "sorry to see you go" email that asks one question, and start a win-back sequence that waits a few weeks before its first email. Properties: plan, cancellation_reason, access_ends_on.

10. user.went_inactive Your product can't notice someone doing nothing at the moment they do it, so this one comes from a scheduled job: once a day, find users with no activity in the last 14 days and post the event for each. It starts a re-engagement sequence, and an event that shows they came back can be its goal. Properties: days_inactive, last_active_on.

What to put in the properties

Properties are the details an email quotes. In Pombo they're available in the email as {{ event.plan }}, {{ event.amount }} and so on, with one rule that catches people out: send them ready to display.

{
  "event": "payment.failed",
  "email": "[email protected]",
  "properties": {
    "plan": "Growth",
    "amount": "$49",
    "update_payment_url": "https://app.example.com/billing"
  }
}

Send "$49", not 4900 cents. Send "October 14", not a Unix timestamp. The value goes into the sentence exactly as you sent it.

Keep properties to what an email would actually say. Don't send passwords, tokens, full card numbers or anything you wouldn't want printed in an inbox. For properties that are only sometimes present, see the note on conditionals in event-triggered email.

Events are moments, fields are state

One distinction saves a lot of confusion later. An event records that something happened. A contact field records how things stand now.

When someone upgrades, two things are true: the upgrade happened (an event) and they're now on the Growth plan (a fact about them). Send both. Post subscription.upgraded to the events API, and update the contact's plan custom field through the contacts API.

It matters because they do different jobs. Events start sequences and count as goals. Fields are what you segment on ("everyone on the Growth plan") and what a date-based trigger reads.

That last point changes how you handle trial expiry. You might expect a trial.expiring event, but you don't need one: store trial_ends_on as a date field on the contact, and a sequence with a date field trigger can fire three days before it, with no scheduled job on your side.

Where to fire them in your code

Three rules keep the events honest:

  1. Fire after the change is saved. Post subscription.started after the subscription is written to your database, not in the controller before the transaction commits. Otherwise a rolled-back payment can still send a "welcome to Pro" email.
  2. Fire from a background job. Your email platform being slow or down should never stop a user from signing up. Queue the call and let it retry.
  3. Guard the once-only events. A retried request can post twice. For events that should happen once per user, such as the first real result, record that you sent it and check before sending again.

If you'd rather not write this by hand, a coding agent connected to your email platform can find the right places in your code and add the calls for you.

A spec you can copy

Paste this into your repo, rename the product-specific events, and delete the ones you don't need yet. It's the list above in a form you can hand to a teammate or an agent.

# Email events: each one starts, stops or fills in an email.
# Name: object.past_tense, lowercase. Values in properties are display-ready.

user.signed_up:
  when: account created and saved
  drives: starts the welcome sequence
  properties: [first_name, signup_source, plan]

onboarding.completed:
  when: user finishes required setup
  drives: goal for the welcome sequence
  properties: [steps_completed, days_since_signup]

report.created:            # rename to your product's first real result
  when: first time only
  drives: sends a "what to try next" email
  properties: [title, url]

teammate.invited:
  when: invitation sent
  drives: goal for a collaboration sequence
  properties: [inviter_name, team_name]

feature.first_used:
  when: first use of a feature that predicts retention
  drives: goal for a feature adoption sequence
  properties: [feature]

trial.started:
  when: trial begins, if separate from signup
  drives: starts the trial sequence
  properties: [plan, trial_ends_on]

subscription.started:
  when: first successful payment
  drives: goal for the trial sequence
  properties: [plan, amount, billing_interval]

payment.failed:
  when: renewal charge declined
  drives: transactional notice, plus a reminder sequence
  properties: [amount, plan, update_payment_url]

payment.succeeded:
  when: charge succeeds
  drives: goal for the payment reminder sequence
  properties: [amount, plan]

subscription.cancelled:
  when: user cancels (not when access ends)
  drives: starts the win-back sequence
  properties: [plan, cancellation_reason, access_ends_on]

user.went_inactive:
  when: daily job, no activity in 14 days
  drives: starts the re-engagement sequence
  properties: [days_inactive, last_active_on]

# State, not events: keep these on the contact as fields.
contact_fields:
  plan: current plan name
  trial_ends_on: date; drives the "trial ending" sequence by date trigger

(payment.succeeded is on the spec but not the list of ten: it's only there to be the goal that stops the payment reminders.)

Start with three

You don't need all of this before your first email goes out. Start with the three events that cover the most ground:

  • user.signed_up, to start a welcome sequence.
  • onboarding.completed, so that sequence stops once it has done its job.
  • subscription.started, so your trial emails stop at the conversion.

Add the next event from the list when you have an email for it, not before.

For the full picture of how events fit with sequences, segments and transactional email, see the lifecycle email basics section, or read the events docs for the API itself.