---
title: "How do you nudge a user who signed up and never came back?"
url: "https://palumb.com/use-cases/activation-reminders"
description: "Three reminders over a week, each one preceded by a wait. The first sign of life ends the sequence, and the messages still to come never go out."
---

[← Use cases](/use-cases/)

# How do you nudge a user who signed up and never came back?

The workflow waits for the user to act, and sends only if they do not. Three reminders over a week, spaced a day, a day and five days apart. Each one is preceded by a wait, and the first sign of life ends the sequence: the messages that were still to come never go out, because they were never scheduled.

Three groups, one under the other, and a single vertical line running down outside them on the left that marks the flow of data. Nothing travels along that line. Instead the stretch between two ends fills, from whoever is calling toward whoever is called, holds while that one works, and then drains back the way it came. The fill is always the same grey, the ink of the Palumb mark: the line belongs to Palumb, and which end the fill starts from already says who placed the call. It carries a small stop at every height where something happens.

First group, yourapp.com/backend: your own application, and the calls it makes into Palumb: trigger for activation on topic.user-91 and sendEvent for user.activated. Neither the bubble nor its mark exists until you make that call, and the mark is a small teal square that fills in once Palumb has it. While a call is in flight the bubble's border turns and the line below it fills down to Palumb.

Second group, palumb, the durable engine: the one that is not an address, because it is not something you run. It appears when the trigger arrives, and holds one bubble that says running workflow, except while it is waiting: then it names what it waits for, waiting user.activated, waiting user.activated and waiting user.activated, with the elapsed count in front where the wait has a limit. At the end it says run complete. Its border turns for as long as the run lives, through every wait. Just under it the journal grows, one small violet token per step that has closed, each carrying that step's number.

From the second group the fill runs down to the exact row that is about to run, and stops on it. It is not the channel of the call: your workflow is one signed endpoint, and the body replays from the top with the completed steps rehydrated until it reaches the row it has not run yet. The line is the flow of control, and control really does end up on that row.

Third group, yourapp.com/workflows/activation: your workflow file itself, 23 lines of TypeScript, with its line numbers. In the margin of every line that calls a step there is a numbered circle, the way an editor marks a breakpoint: 1 for step.waitForEvent, 2 for step.send, 3 for step.waitForEvent, 4 for step.send, 5 for step.waitForEvent, 6 for step.send. At rest the margin holds only the line numbers; a step's number appears when that step starts running.

When the fill reaches a row, a band lights it, the way a debugger marks where it has stopped, and the ring in its margin turns. The moment the step is recorded the number leaves the margin, a tick takes its place, and the number reappears as a token in the row under Palumb: you wrote the step, we are the ones who remember it ran.

Not every wait is ended by an event: some are not, and those run out their clock instead. While that happens the line down to your code is grey and empty and no row is lit, while the run stays alive in Palumb. That is the frame that matters: for as long as the wait lasts your server does nothing at all and ours does.

While the run waits, the Palumb mark leaves its column and a small clock takes its place, its hand turning once for every unit of time that goes by, and the bubble puts the elapsed count in front of what it is waiting for: day 6, waiting order.shipped. The hand is fast on purpose: it is there to say that the time is moving, not to be counted.

Each wait has its own limit, taken from the code: 24 hours for step.waitForEvent, run out in full, 24 hours for step.waitForEvent, run out in full and 5 days for step.waitForEvent, cut short by the event on day 2. A wait that runs out counts up to its limit; one an event closes stops where the event arrived, and that is the number the bubble ends on.

The drawing follows one path through the code. An if has two ways out and the animation takes the one this page is about; the other ways are in the file, which is printed in full below. On this path the run ends before the file does: step.send is never reached, so no mark appears in the margin of that line at all, only the line number.

At rest, which is also what anyone who has asked for less motion sees, every step the run reached is ticked, the journal holds its 5 tokens, the bubble says run complete, and the line is empty: the drawing is the finished picture of this path.

activation.ts

```ts
workflow("activation", async ({ payload, step }) => {
  const day1 = await step.waitForEvent("wait-1", {
    event: "user.activated",
    timeout: "24h",
  })
  if (day1) return
  await step.send("nudge-1", "email", () => gettingStarted(payload))

  const day2 = await step.waitForEvent("wait-2", {
    event: "user.activated",
    timeout: "24h",
  })
  if (day2) return
  await step.send("nudge-2", "email", () => whatOthersBuild(payload))

  // five more days — seven since sign-up
  const week = await step.waitForEvent("wait-3", {
    event: "user.activated",
    timeout: "5d",
  })
  if (week) return
  return step.send("nudge-3", "email", () => lastCall(payload))
})
```

## How it works

The workflow starts when the user signs up (trigger). First it waits (step 11) a day for any sign of life, and if it comes the run ends there with nothing sent. Nobody shows up here, so the first reminder goes out (step 22).

It waits another day (step 33), then sends the second (step 44). Then it waits five more days (step 55), and this time the user comes back (sendEvent): the run ends the moment the event lands.

A third reminder sits in the file for the user who never returns. On this path it is never reached.

The other one: [a weekly summary of upcoming deadlines](/use-cases/weekly-summary/), where the events are gathered by a run that stays open instead of ending it.

Everyone who joins gets in, and it costs nothing. Your invite goes out when the beta opens.

[Join the beta](/waitlist/)