Back to Patterns

Building flows for lost customers

Combine events into a single function to build things like cart abandonment, sales processes, and churn flows

ActivationUser JourneysEvent Coordination

It's common to want to build functions which react to user journeys, and allow you to react to user activity within code directly. Some common examples are:

  • In e-commerce, when a user adds a product to their cart without checking out in 24 hours send them a reminder email
  • In a SaaS app, if a user signs up but doesn't perform a required action in 3 days, send them a message
  • When a lead enters your sales pipeline, send a reminder to the sales team if there's no outreach in a week

In each example, you want to start a function using one event, then wait some time for another event, then continue your logic when the time is up (or when the event is received). This allows you to model complex user journeys by simply waiting for new events triggered by users in your product.

How to implement this pattern

Inngest allows you to build functions which coordinate between events using a few lines fo the SDK:

typescript
import { inngest } from "./client";
/*
This is the data received whenever the `cart/product.added` event is received:
type CartProductAdded = {
name: "cart/product.added"
data: {
cart_id: string;
product_id: string;
product_name: string;
};
user: {
email: string;
};
};
*/
export default inngest.createFunction(
{
id: "product-added-to-cart",
// Automatically cancel this instance of the function whenever a user adds another
// product to their cart. Another instance of this function will run and schedule
// another wait from 24 hours after the new product was added to their cart.
cancelOn: {
event: "cart/product.added",
timeout: "24h",
match: "data.cart_id",
},
},
{ event: "cart/product.added" },
({ event, step }) => {
// This function runs as soon as a product is added to the cart.
// We immediately pause and wait up to 24 hours for the `cart/purchased`
// event from the same cart_id.
const purchased = await step.waitForEvent("wait-for-purchase", {
event: "cart/purchased",
timeout: "24h",
match: "data.cart_id", // The "data.cart_id" field in both events must match.
});
// waitForEvent will return the `cart/purchased` event immediately when the
// matching event is received, or after the timeout with `null` if the event
// was not received (ie. the user didn't purchase).
if (purchased !== null) {
// The user has purchased their products; we can end.
return;
}
await step.run("send-reminder", () => {
sendCartReminderEmail({
email: event.user.email,
cart: event.data.cart_id,
});
});
// Idea: We could also wait another 6 days to send a 1 week cart reminder
}
);

In this example we create a function that models key points in the user journey declaratively, without worrying about state or timing in code.

First, we create a function that runs any time a user adds a product to their cart. We then wait for the checkout event (cart/purchased) for up to 24 hours, via await step.waitForEvent. This pauses the function until a matching event is received or the event isn't received within your time specified.

Inngest resumes the function passing in the received event data, or null if the event was not received within the timeout. Your code then continues to run from after waitForEvent, allowing you to process the incoming event or timeouts easily.

This has the following benefits:

  • All user-journey code is colocated within the same function
  • This makes the code easy to read, understand, and modify
  • You don't have to manage state, queues, or crons to check for specific conditions to run logic

Cancellations: running the function once

We only want to run this function once for each cart_id. A user might add 3 products, but they should only receive a single reminder email. In this case, we set a cancelOn option when defining the function which cancels any running or paused functions. Any time another product is added to the same cart ID, older runs of this function are cancelled. For example:

  • A user adds a product to the cart. This function runs with an ID of #1
  • A user adds another product. Function #1 is cancelled automatically, and another copy of this function runs with an ID of #2

The first product's journey is cancelled automatically, and the user only receives an email 24 hours after the second product is added.

Alternative approaches

It's possible to handle these flows within standard applications by:

  • Creating scheduled functions that run every hour to check for abandoned carts within the age range of 23-24 hours.
  • Enqueueing a function to wait for 24 hours, then checking to see if the cart has been purchased directly within the database.

These are both possible within Inngest and using traditional queueing systems, although these approaches typically have more code and distributed state to maintain, increasing the chances of bugs and breaking changes.

Additional resources

Ready to start building?

Ship background functions & workflows like never before

$ npx inngest-cli devGet started for free