> ## 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](/workflows) that tries to pull them back.

## What you need

* A `lastActive` ([date contact property](/contacts/properties))
* A `churnRisk` ([boolean contact property](/contacts/properties)) 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](/api-reference/update-contact) or an [integration](/integrations) when the user performs a key action.

For extended context on this pattern, [Sending a campaign to active users](/guides/onboarding-emails#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](/contacts/filters-segments#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](/deliverability/sending-reputation). That's why this recipe caps
  the window at 60 days.
</Tip>

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

<Steps>
  <Step title="Add a lifecycle property">
    Add a boolean [contact property](/contacts/properties) in Loops called `churnRisk`.
    This should beupdated via the [Update contact endpoint](/api-reference/update-contact) whenever activity changes, or in a daily sync job.
  </Step>

  <Step title="Trigger on Contact updated">
    In the [workflow builder](/workflows), pick **Contact updated** with
    `churnRisk` changing to `true`. See [Workflow
    triggers](/workflows/triggers).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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`.
  </Step>
</Steps>

## 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](/workflows/experiments) to A/B test subject lines if volume permits

## Read more

<CardGroup>
  <Card title="Lifecycle emails overview" icon="arrows-rotate" href="/guides/lifecycle-emails" />

  <Card title="Filters and segments" icon="filter-list" href="/contacts/filters-segments" />

  <Card title="Branching workflows" icon="code-branch" href="/workflows/branching" />

  <Card title="Sender reputation" icon="shield" href="/deliverability/sending-reputation" />
</CardGroup>
