---
title: "Sales handoff for product-led teams | Snowplow Signals"
description: "Snowplow Signals tells your CRM when an account opens pricing, hits a plan limit or adds teammates, so the seller reaches out while the buyer is logged in."
source: https://signals.snowplow.io/solutions/b2b-software/sales-handoff
---

[B2B software](https://signals.snowplow.io/solutions/b2b-software) / Sales handoff 

# Reach the buyer while the pricing page is open. 

Signals tells your CRM and sales tool when an account opens pricing, hits a plan limit or invites teammates, so the seller hears about it while the buyer is still logged in.

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

Faster speed to lead

## Get the seller to the buyer while the buyer is still deciding. 

Speed to lead moves when the seller hears about an account on the day the intent shows, rather than after the next scoring run. Signals gives your CRM and sales tool what each account did this week, in pricing views, plan limits and invites, and how long since anyone from it was in the product. Your CRM decides who gets told and your sequence decides what goes out.

Four ways product-led teams use it

01 An account back on the pricing page Three pricing views this week, mostly the team plan. Tell the seller now. 02 An account that hit a limit and grew the same day Hit the seat limit and invited two teammates today. Start the sequence today. 03 A new signup from a target account Signed up with a work email that matches an open target. Show the seller now. 04 A qualified account that has gone quiet Qualified last week, no one in the product for nine days. Hold the follow-up. 

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

### Tell the seller while the pricing page is open

Someone at an account has opened the pricing page three times this week, and each time the plan they compared was the team plan. The lead score picks this up in the next scoring run, and the seller reads it after the buyer has closed the tab and gone back to work. The moment to reach them is now, with the plan they looked at in the first line of the message.

What Signals knows

`pricing_views_7d` 

Times anyone at this account opened the pricing page in the last seven days

`plan_viewed_7d` 

The plan they compared most in the last seven days

What your CRM and sales tool do

Posts the account to the seller's queue the moment this week's pricing views reach three, with the plan they compared most, so the first message names it.

Define it · PythonUse it · TypeScript

crm-sync.ts · 15 lines Copy Copied 

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

page_view = Event(name="page_view", vendor="com.snowplowanalytics.snowplow", version="1-0-0")
on_pricing = Criteria(all=[Criterion.like(AtomicProperty(name="page_url"), "%/pricing%")])
account = AttributeKey(
    name="account",
    property=EntityProperty(vendor="com.example", name="account", major_version=1, path="id"),
)

pricing_views_7d = Attribute(
    name="pricing_views_7d",
    type="int32",
    events=[page_view],
    aggregation="counter",
    criteria=on_pricing,
    period=timedelta(days=7),
    default_value=0,
)

# The pricing page attaches the plan being compared as a plan entity on each page view.
plan_viewed_7d = Attribute(
    name="plan_viewed_7d",
    type="string",
    events=[page_view],
    aggregation="most_frequent",
    property=EntityProperty(vendor="com.example", name="plan", major_version=1, path="name"),
    criteria=on_pricing,
    period=timedelta(days=7),
)

# Keyed on the account, so one read covers everyone logged in under it.
saas_handoff_pricing = StreamAttributeGroup(
    name="saas_handoff_pricing",
    version=1,
    attribute_key=account,
    owner="you@example.com",
    attributes=[pricing_views_7d, plan_viewed_7d],
)
```

```
// In the job that posts account activity to your CRM
const account = await signals.getServiceAttributes({
  name: "saas_handoff_pricing",
  attribute_key: "account",
  identifier: accountId,
});
// { pricing_views_7d: 3, plan_viewed_7d: "team" }

if (account.pricing_views_7d >= 3) {
  crm.notify({
    account: accountId,
    to: "account_owner",
    note: `Opened pricing ${account.pricing_views_7d} times this week, comparing the ${account.plan_viewed_7d} plan`,
  });
}
```

### Start the sequence today, not after the scoring run

An account hit the seat limit on its plan this morning and has invited two teammates since. Thursday's scoring run would mark it as a product-qualified lead, and the sequence would start on Friday. By then a workaround is in place or the invites have lapsed. Both signs landed today, and the sequence should too.

What Signals knows

`limits_hit_7d` 

Plan limits this account hit in the last seven days

`last_limit_hit` 

Which limit it hit most recently

`invites_1d` 

Teammates invited in the last day

What your CRM and sales tool do

Enrolls the account in the expansion sequence the day a plan limit and two invites land together, and tells the seller which limit was hit so the first message speaks to it.

Define it · PythonUse it · TypeScript

crm-sync.ts · 15 lines Copy Copied 

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

plan_limit_hit = Event(name="plan_limit_hit", vendor="com.example", version="1-0-0")
teammate_invited = Event(name="teammate_invited", vendor="com.example", version="1-0-0")
account = AttributeKey(
    name="account",
    property=EntityProperty(vendor="com.example", name="account", major_version=1, path="id"),
)

limits_hit_7d = Attribute(
    name="limits_hit_7d",
    type="int32",
    events=[plan_limit_hit],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

last_limit_hit = Attribute(
    name="last_limit_hit",
    type="string",
    events=[plan_limit_hit],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="plan_limit_hit", major_version=1, path="limit"),
    period=timedelta(days=7),
)

invites_1d = Attribute(
    name="invites_1d",
    type="int32",
    events=[teammate_invited],
    aggregation="counter",
    period=timedelta(days=1),
    default_value=0,
)

saas_handoff_limit = StreamAttributeGroup(
    name="saas_handoff_limit",
    version=1,
    attribute_key=account,
    owner="you@example.com",
    attributes=[limits_hit_7d, last_limit_hit, invites_1d],
)
```

```
// In the job that posts account activity to your CRM
const account = await signals.getServiceAttributes({
  name: "saas_handoff_limit",
  attribute_key: "account",
  identifier: accountId,
});
// { limits_hit_7d: 1, last_limit_hit: "seats", invites_1d: 2 }

if (account.limits_hit_7d > 0 && account.invites_1d >= 2) {
  sequences.enroll({
    account: accountId,
    sequence: "expansion",
    context: { limit: account.last_limit_hit },
  });
}
```

### Put the signup in the account's thread within the hour

Someone signed up this morning with a work email, and the domain matches an account the seller has been working for a month. The match usually runs in a nightly enrichment, so the seller finds the signup in tomorrow's list, after the new user's first session has ended. The seller should see it in the account's thread while the user is still exploring, with how much of the product they have seen.

What Signals knows

`work_domain` 

The email domain this user signed up with

`minutes_since_signup` 

How long ago they signed up

`pages_1h` 

Pages they opened in the last hour

What your CRM and sales tool do

Looks the domain up against the open target accounts, and when it matches, posts the signup to that account's thread with how much of the product the user has seen, so the seller can say hello while they are still in it.

Define it · PythonUse it · TypeScript

crm-sync.ts · 17 lines Copy Copied 

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

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

work_domain = Attribute(
    name="work_domain",
    type="string",
    events=[signup],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="signup", major_version=1, path="email_domain"),
    ttl=timedelta(days=30),
)

minutes_since_signup = Attribute(
    name="minutes_since_signup",
    type="double",
    events=[signup],
    aggregation="time_since_last",
    time_unit="min",
    ttl=timedelta(days=30),
)

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

# A signup belongs to the person, not yet to an account, so this group keys on the user id.
saas_handoff_signup = StreamAttributeGroup(
    name="saas_handoff_signup",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[work_domain, minutes_since_signup, pages_1h],
)
```

```
// In the job that posts account activity to your CRM
const user = await signals.getServiceAttributes({
  name: "saas_handoff_signup",
  attribute_key: "user_id",
  identifier: userId,
});
// { work_domain: "example.com", minutes_since_signup: 12.4, pages_1h: 9 }

const target = await crm.findAccount({ domain: user.work_domain });

if (target && user.minutes_since_signup < 60) {
  crm.post({
    account: target.id,
    thread: "activity",
    note: `New signup from ${user.work_domain}, ${user.pages_1h} pages in the first hour`,
  });
}
```

### Hold the follow-up and tell the seller why

An account qualified last week on pricing views and a plan limit, and the second email in the sequence goes out tomorrow. No one from the account has been in the product for nine days. The score was right when it ran and the sequence follows its calendar, so the next message asks for a call about a plan nobody is evaluating this week. The seller should hold it, and know the reason.

What Signals knows

`days_since_session` 

Days since anyone at this account was in the product

`pricing_views_7d` 

Times anyone at this account opened the pricing page in the last seven days

`limits_hit_7d` 

Plan limits this account hit in the last seven days

What your CRM and sales tool do

Pauses the sequence when the account has been out of the product for a week with no pricing views or limits since, and leaves the seller a note saying so, so the next touch waits for the account to come back rather than following the calendar.

Define it · PythonUse it · TypeScript

crm-sync.ts · 14 lines Copy Copied 

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

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

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

pricing_views_7d = Attribute(
    name="pricing_views_7d",
    type="int32",
    events=[page_view],
    aggregation="counter",
    criteria=Criteria(all=[Criterion.like(AtomicProperty(name="page_url"), "%/pricing%")]),
    period=timedelta(days=7),
    default_value=0,
)

limits_hit_7d = Attribute(
    name="limits_hit_7d",
    type="int32",
    events=[plan_limit_hit],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

saas_handoff_quiet = StreamAttributeGroup(
    name="saas_handoff_quiet",
    version=1,
    attribute_key=account,
    owner="you@example.com",
    attributes=[days_since_session, pricing_views_7d, limits_hit_7d],
)
```

```
// In the job that posts account activity to your CRM
const account = await signals.getServiceAttributes({
  name: "saas_handoff_quiet",
  attribute_key: "account",
  identifier: accountId,
});
// { days_since_session: 9.3, pricing_views_7d: 0, limits_hit_7d: 0 }

if (account.days_since_session >= 7 && account.pricing_views_7d === 0 && account.limits_hit_7d === 0) {
  sequences.pause({
    account: accountId,
    reason: `No one from the account has been in the product for ${Math.floor(account.days_since_session)} days`,
  });
}
```

## 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)
