> ## Documentation Index
> Fetch the complete documentation index at: https://loops.so/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe: Churn risk segment and re-engagement workflow

> Define a churn risk segment based on inactivity signals, then automate a re-engagement sequence for those users.

Churn rarely happens suddenly. Users go quiet, skip a few sessions, stop getting value, and only then cancel. This recipe defines the segment that catches them early and a [workflow](https://loops.so/docs/workflows.md) that tries to pull them back.

## What you need

* A `lastActive` ([date contact property](https://loops.so/docs/contacts/properties.md))
* A `churnRisk` ([boolean contact property](https://loops.so/docs/contacts/properties.md)) your app computes and writes (`true`/`false`)
* Optional: a count property like `sessionCount30d` if you want tighter rules

These contact properties need to be updated via the [Update contact endpoint](https://loops.so/docs/api-reference/update-contact.md) or an [integration](https://loops.so/docs/integrations.md) when the user performs a key action.

For extended context on this pattern, [Sending a campaign to active users](https://loops.so/docs/guides/onboarding-emails.md#sending-a-campaign-to-active-users-in-workflows) is worth reading.

## Define the segment

Go to the [Audience page](https://app.loops.so/audience), apply filters, then save the [segment](https://loops.so/docs/contacts/filters-segments.md#audience-segments).

A reasonable starting definition:

* `churnRisk` `Is true`, AND
* `Subscribed` `Is true`

Set `churnRisk` from your app as:

* `true` when `lastActive` is between 14 and 60 days ago
* `false` otherwise

This keeps long-inactive users out so you do not wake up dormant signups, while also excluding brand-new contacts who have not had a chance to be active yet.

> [!TIP]
>
> Sending to users who have been inactive for months can hurt your [sender
> reputation](https://loops.so/docs/deliverability/sending-reputation.md). That's why this recipe caps
> the window at 60 days.

Because Loops filters do not support dynamic date expressions like "today minus 14 days," compute this window in your app or a daily sync job and write `churnRisk` directly.

```ts syncToLoops.ts theme={"dark"}
import { LoopsClient } from "loops";

type UserActivityRow = {
  email: string;
  userId: string;
  lastActiveAtMs: number | null;
};

const loops = new LoopsClient(process.env.LOOPS_API_KEY!);
const DAY_MS = 24 * 60 * 60 * 1000;
const CHURN_MIN_DAYS = 14;
const CHURN_MAX_DAYS = 60;

async function syncChurnRisk(users: UserActivityRow[]) {
  const now = Date.now();
  const minCutoff = now - CHURN_MIN_DAYS * DAY_MS;
  const maxCutoff = now - CHURN_MAX_DAYS * DAY_MS;

  for (const user of users) {
    const last = user.lastActiveAtMs;
    const churnRisk = last !== null && last <= minCutoff && last >= maxCutoff;

    await loops.updateContact({
      email: user.email,
      userId: user.userId,
      properties: {
        lastActive: last,
        churnRisk,
      },
    });
  }
}
```

## Set up the re-engagement workflow

**1. Add a lifecycle property**

Add a boolean [contact property](https://loops.so/docs/contacts/properties.md) in Loops called `churnRisk`.
This should be updated via the [Update contact endpoint](https://loops.so/docs/api-reference/update-contact.md) whenever activity changes, or in a daily sync job.

**2. Trigger on Contact updated**

In the [workflow builder](https://loops.so/docs/workflows.md), pick **Contact updated** with
`churnRisk` changing to `true`. See [Workflow
triggers](https://loops.so/docs/workflows/triggers.md).

**3. Add an audience filter**

Match the trigger so only currently-at-risk users continue:
`churnRisk` `Is true`.

Choose between **One time** or **Every time** for the trigger frequency, depending on whether you want contacts to re-enter the workflow in the future.

Set the filter scope to **All following nodes**, so Loops will continue evaluating each contact against the filter as the workflow progresses.

**4. Build the email sequence**

Add email nodes to the workflow, with timer nodes in between.

* **Day 0**: "We noticed you have been quiet. Here is the one thing worth coming back for." Personal, useful, not promotional.
* **Timer 4 days**
* Send a customer story or missed-feature email.
* **Timer 6 more days**
* Send a direct ask with a link to cancel if they prefer.

If you use **All following nodes**, contacts will be checked against the filter before they reach each node, and will exit the workflow as soon as `churnRisk` is `false`.

## Why this works

* The segment updates automatically as `churnRisk` changes
* The filter in the workflow prevents messaging users who already came back
* The 60-day upper bound protects deliverability by keeping long-dormant users out

## Testing before you ship

* Send a test to your own account with `lastActive` manually set 20 days back and `churnRisk` set to `true`
* Use the [experiments feature](https://loops.so/docs/workflows/experiments.md) to A/B test subject lines if volume permits

## Read more

> [card] [Lifecycle emails overview](https://loops.so/docs/guides/lifecycle-emails.md)

> [card] [Filters and segments](https://loops.so/docs/contacts/filters-segments.md)

> [card] [Branching workflows](https://loops.so/docs/workflows/branching.md)

> [card] [Sender reputation](https://loops.so/docs/deliverability/sending-reputation.md)


## Related topics

- [Recipe: Active 30-day users segment](https://loops.so/docs/guides/active-30-day-users.md)
- [Recipe: SaaS customer lifecycle emails](https://loops.so/docs/guides/lifecycle-emails.md)
- [Recipe: Onboarding completion workflow](https://loops.so/docs/guides/onboarding-completion.md)
- [Reducing product email bounce rate](https://loops.so/docs/deliverability/reducing-bounce-rate.md)
- [Filters and Segments](https://loops.so/docs/contacts/filters-segments.md)
