> ## 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: Onboarding completion workflow

> Send a congratulatory email the moment a user finishes your onboarding checklist, plus a next-step nudge if they don't.

A common SaaS lifecycle pattern: celebrate when a user completes onboarding, and re-engage the users who stall. This recipe sets up both with two [workflows](/workflows).

The core idea: fire an [event](/events) from your app when the user completes onboarding and update an `onboardingCompletedAt` contact property in the same request. The celebration workflow triggers from the event, and the stalled-user workflow filters out contacts with a completion timestamp.

## What you need

* A timestamp [contact property](/contacts/properties) like `onboardingCompletedAt`
* A named [event](/events) for "onboarding completed"
* The [Loops API](/api-reference/intro) or an [integration](/integrations) writing into Loops

## Set up the trigger

Use the **Event received** [workflow trigger](/workflows/triggers) with an event name like `onboardingCompleted`.

Update the contact's `onboardingCompletedAt` property at the same time so your stalled-user workflow can exclude users who already finished.

If you prefer using contact property changes as the trigger, see the **Contact updated** approach used in [the lifecycle emails guide](/guides/lifecycle-emails).

<Steps>
  <Step title="Send the event from your app">
    Call the [Send event endpoint](/api-reference/send-event) when the user finishes the last onboarding step. The API accepts contact properties as top-level fields in the same request:

    <CodeGroup>
      ```json API request theme={"dark"}
      {
        "email": "user@example.com",
        "onboardingCompletedAt": "2026-04-27T17:00:00Z",
        "eventName": "onboardingCompleted",
        "eventProperties": {
          "daysToComplete": 2
        }
      }
      ```

      ```ts SDK theme={"dark"}
      await loops.sendEvent({
        email: "user@example.com",
        eventName: "onboardingCompleted",
        contactProperties: {
          onboardingCompletedAt: new Date().toISOString(),
        },
        eventProperties: { daysToComplete: 2 },
      });
      ```
    </CodeGroup>
  </Step>

  <Step title="Create the workflow">
    In the [Workflow builder](/workflows), pick **Event received** and select `onboardingCompleted`.
  </Step>

  <Step title="Send the celebration email">
    Add a **Send email** node. Congratulate them, surface the next high-value feature, and offer a next action.
  </Step>
</Steps>

## Catching users who stall

The other half of this recipe is a separate workflow that fires when a contact is added but does not complete onboarding within a timer window.

<Steps>
  <Step title="Trigger on Contact added">
    Pick **Contact added** as the [trigger](/workflows/triggers).
  </Step>

  <Step title="Add a timer">
    Wait 3 days.
  </Step>

  <Step title="Filter on onboarding state">
    Use an [audience filter](/workflows#audience-filters) to only send if `onboardingCompletedAt` is empty.
  </Step>

  <Step title="Send the nudge">
    Keep it short. Link to the specific step they missed if you track that.
  </Step>
</Steps>

## Measuring success

* Open rate on the celebration email: should be >50% because it is expected
* Click-through on the stall nudge: track with the `email.clicked` [webhook](/webhooks)
* Completion lift: compare `daysToComplete` distributions before and after this workflow ships

## Read more

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

  <Card title="Workflows" icon="arrows-rotate" href="/workflows" />

  <Card title="Events" icon="bolt" href="/events" />

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