---
title: "Next-step guidance for product-led teams | Snowplow Signals"
description: "Snowplow Signals tells your product which setup steps this user and their account completed, so the in-app guide shows the next one."
source: https://signals.snowplow.io/solutions/b2b-software/onboarding
---

[B2B software](https://signals.snowplow.io/solutions/b2b-software) / Next-step guidance 

# An in-app guide that knows what the user just did. 

Signals tells your product which steps this user and this account have completed, in this session and since signup, so the guide can point to the next one.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher activation rate

## Activate more new users by pointing the guide at the step they have not done. 

Activation moves when the guide shows a new user the step they have not done, at the moment they are ready for it. Signals gives your product the steps this user has completed, the steps anyone on their account has completed, which page they keep returning to and how long they have been away. The checklist, the tour and the activation email decide what to show from there.

Four ways product-led teams use it

01 A new user ahead of the guide Connected a source, invited a teammate, still told to invite one. Skip to the next step. 02 A user circling the integrations page Third visit to integrations this hour, nothing connected. Offer the connection help. 03 A second user joining a set-up account The admin connected the source last week. Start the newcomer at the account's next step. 04 A user who finished setup and left Set up on Monday, nothing since. Name the first thing to do with it. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Skip the steps this user has finished

A new user signed up twenty minutes ago, connected a data source and invited a teammate from the settings page. The checklist in the corner still says invite a teammate, because it reads a flag that updates after the next sync. The user now knows the guide is behind them, and the guide has one less chance to be read. The next thing they need is the step after the two they finished.

What Signals knows

`steps_completed` 

The setup steps this user has completed since signup

`last_step_completed` 

The step they completed most recently

`steps_completed_1h` 

Steps completed in the last hour

What your product does

Drops every step on this user's completed list from the checklist and opens the panel on the one after their last completed step. The order of the steps is your product's. Which ones are done comes from Signals.

Define it · PythonUse it · TypeScript

checklist.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

step_completed = Event(name="onboarding_step_completed", vendor="com.example", version="1-0-0")
step = EventProperty(vendor="com.example", name="onboarding_step_completed", major_version=1, path="step")

steps_completed = Attribute(
    name="steps_completed",
    type="string_list",
    events=[step_completed],
    aggregation="unique_list",
    property=step,
    ttl=timedelta(days=90),
)

last_step_completed = Attribute(
    name="last_step_completed",
    type="string",
    events=[step_completed],
    aggregation="last",
    property=step,
    ttl=timedelta(days=90),
)

steps_completed_1h = Attribute(
    name="steps_completed_1h",
    type="int32",
    events=[step_completed],
    aggregation="counter",
    period=timedelta(hours=1),
    default_value=0,
)

# New users are logged in from the first screen, so the group keys on the user id.
saas_onboarding_steps = StreamAttributeGroup(
    name="saas_onboarding_steps",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[steps_completed, last_step_completed, steps_completed_1h],
)
```

```
// In the code that renders the onboarding checklist
const user = await signals.getServiceAttributes({
  name: "saas_onboarding_steps",
  attribute_key: "user_id",
  identifier: userId,
});
// { steps_completed: ["connect_source", "invite_teammate"], last_step_completed: "invite_teammate", steps_completed_1h: 2 }

const done = new Set(user.steps_completed);
const remaining = checklist.steps.filter((step) => !done.has(step.id));

guide.render({ steps: remaining, open: remaining[0] });
```

### Offer the connection help, not the general tour

A user has opened the integrations page three times in the last hour and has not connected anything. The tour that runs for every new user on their first day is explaining the dashboard. Three visits to one page with nothing to show for it is a user stuck on something specific, and the general tour arrives beside a problem it does not mention.

What Signals knows

`integrations_views_1h` 

Times they opened the integrations page in the last hour

`integrations_connected` 

Integrations this user has connected since signup

What your product does

Swaps the general tour for the connection guide on the integrations page once a user has opened it three times without connecting one, and adds a link to a person if the next visit also ends without a connection.

Define it · PythonUse it · TypeScript

integrations.tsx · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AtomicProperty, Criteria, Criterion, Event, StreamAttributeGroup, user_id,
)

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
integration_connected = Event(name="integration_connected", vendor="com.example", version="1-0-0")

integrations_views_1h = Attribute(
    name="integrations_views_1h",
    type="int32",
    events=[page_view],
    aggregation="counter",
    criteria=Criteria(all=[
        Criterion.like(AtomicProperty(name="page_url"), "%/settings/integrations%"),
    ]),
    period=timedelta(hours=1),
    default_value=0,
)

integrations_connected = Attribute(
    name="integrations_connected",
    type="int32",
    events=[integration_connected],
    aggregation="counter",
    ttl=timedelta(days=90),
    default_value=0,
)

saas_onboarding_integrations = StreamAttributeGroup(
    name="saas_onboarding_integrations",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[integrations_views_1h, integrations_connected],
)
```

```
// In the code that renders the integrations page
const user = await signals.getServiceAttributes({
  name: "saas_onboarding_integrations",
  attribute_key: "user_id",
  identifier: userId,
});
// { integrations_views_1h: 3, integrations_connected: 0 }

if (user.integrations_views_1h >= 3 && user.integrations_connected === 0) {
  guide.show({
    flow: "connect_first_integration",
    offerHelp: user.integrations_views_1h > 3,
  });
}
```

### Start the second user where the account left off

An admin set the account up last week, connected the source and created the first project. Today a colleague accepts the invitation and lands on the same checklist the admin saw, starting from connect a data source. The guide knows what this user has done, which is nothing yet, and lags behind what their account has done. The newcomer's first step is the first one the account has not finished.

What Signals knows

`account_steps_completed` 

The setup steps anyone on this account has completed

`account_users_7d` 

Separate users active on the account in the last seven days

What your product does

Reads the account's completed steps as well as the user's, hides the account-level steps that are done, and opens the newcomer's checklist on the first step that belongs to them, such as creating their own view.

Define it · PythonUse it · TypeScript

checklist.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AttributeKey, AtomicProperty, EntityProperty, Event, EventProperty, StreamAttributeGroup,
)

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
step_completed = Event(name="onboarding_step_completed", vendor="com.example", version="1-0-0")
account = AttributeKey(
    name="account",
    property=EntityProperty(vendor="com.example", name="account", major_version=1, path="id"),
)

account_steps_completed = Attribute(
    name="account_steps_completed",
    type="string_list",
    events=[step_completed],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="onboarding_step_completed", major_version=1, path="step"),
    ttl=timedelta(days=90),
)

account_users_7d = Attribute(
    name="account_users_7d",
    type="int32",
    events=[page_view],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="user_id"),
    period=timedelta(days=7),
)

# Keyed on the account entity every event carries, so a step one user finishes counts for the next.
saas_onboarding_account = StreamAttributeGroup(
    name="saas_onboarding_account",
    version=1,
    attribute_key=account,
    owner="you@example.com",
    attributes=[account_steps_completed, account_users_7d],
)
```

```
// In the code that renders the onboarding checklist for a user who just joined
const account = await signals.getServiceAttributes({
  name: "saas_onboarding_account",
  attribute_key: "account",
  identifier: accountId,
});
// { account_steps_completed: ["connect_source", "create_project"], account_users_7d: 2 }

const accountDone = new Set(account.account_steps_completed);
const mine = checklist.steps.filter((step) => step.scope === "user" || !accountDone.has(step.id));

guide.render({ steps: mine, open: mine[0] });
```

### Send the first thing to do before the setup is forgotten

A user finished setup on Monday, connected one integration and has not opened the product in three days. The onboarding email sequence runs on a fixed calendar and is up to day-four tips, so the note that arrives is about a feature they have not reached. The message this user needs names the integration they connected and the first thing to do with it, and it needs to go when the user stops coming in, not when the calendar says so.

What Signals knows

`days_since_session` 

Days since they last opened the product

`steps_completed` 

The setup steps this user has completed since signup

`connected_integrations` 

The integrations this user has connected since signup

What your product does

Sends one note, from your email tool or as an in-app message when they return, naming the integration they connected and the first thing to do with it, once a user who finished setup has been away for three days. A user who still opens the product each day gets nothing.

Define it · PythonUse it · TypeScript

activation-email.ts · 16 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
step_completed = Event(name="onboarding_step_completed", vendor="com.example", version="1-0-0")
integration_connected = Event(name="integration_connected", vendor="com.example", version="1-0-0")

days_since_session = Attribute(
    name="days_since_session",
    type="double",
    events=[page_view],
    aggregation="time_since_last",
    time_unit="d",
)

steps_completed = Attribute(
    name="steps_completed",
    type="string_list",
    events=[step_completed],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="onboarding_step_completed", major_version=1, path="step"),
    ttl=timedelta(days=90),
)

connected_integrations = Attribute(
    name="connected_integrations",
    type="string_list",
    events=[integration_connected],
    aggregation="unique_list",
    property=EventProperty(vendor="com.example", name="integration_connected", major_version=1, path="integration"),
    ttl=timedelta(days=90),
)

saas_onboarding_return = StreamAttributeGroup(
    name="saas_onboarding_return",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[days_since_session, steps_completed, connected_integrations],
)
```

```
// In the job that decides which onboarding email goes out next
const user = await signals.getServiceAttributes({
  name: "saas_onboarding_return",
  attribute_key: "user_id",
  identifier: userId,
});
// { days_since_session: 3.1, steps_completed: ["connect_source", "invite_teammate", "create_project"], connected_integrations: ["salesforce"] }

const setupDone = checklist.required.every((step) => user.steps_completed.includes(step));

if (setupDone && user.days_since_session >= 3) {
  email.send({
    template: "first_thing_to_do",
    integration: user.connected_integrations[0],
  });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
