Set the fare watch without asking them to fill in a form
A traveler has priced Chicago to Lisbon for the same November dates three times this week and left without booking each time. The fare alert on your site is a form, and the alert job hears about a traveler only once they fill it in. The traveler who priced a route three times and left has told you the same thing without the form. Three prices in a week for one route is the watch, set by what they did. If your CRM holds an email for this browser, the alert can go out the moment your fares system prices that route below what they last saw.
watched_route_7d- The route they priced most in the last seven days
routes_priced_7d- How many times they priced each route in the last seven days
watched_depart_7d- The departure date on their most recent search
last_fare_seen- The fare they last saw, in your currency
Opens a fare watch for this browser once a route has been priced three times in a week, with the fare they last saw as the line to beat, and sends the alert when your fares system next prices that route and date below it. The email is one your CRM holds under your consent rules, and a browser without one gets the watch only, ready for when they leave an address.
from datetime import timedelta
from snowplow_signals import (
Attribute, Event, EventProperty, StreamAttributeGroup, domain_userid,
)
flight_search = Event(name="flight_search", vendor="com.example", version="1-0-0")
fare_viewed = Event(name="fare_viewed", vendor="com.example", version="1-0-0")
watched_route_7d = Attribute(
name="watched_route_7d",
type="string",
events=[flight_search],
aggregation="most_frequent",
property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
period=timedelta(days=7),
)
routes_priced_7d = Attribute(
name="routes_priced_7d",
type="dict",
events=[flight_search],
aggregation="category_count",
property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="route"),
period=timedelta(days=7),
)
watched_depart_7d = Attribute(
name="watched_depart_7d",
type="string",
events=[flight_search],
aggregation="last",
property=EventProperty(vendor="com.example", name="flight_search", major_version=1, path="depart_date"),
period=timedelta(days=7),
)
last_fare_seen = Attribute(
name="last_fare_seen",
type="double",
events=[fare_viewed],
aggregation="last",
property=EventProperty(vendor="com.example", name="fare_viewed", major_version=1, path="price"),
period=timedelta(days=7),
)
# Keyed on the browser, so a watcher who has not logged in or filled in a form still has a watch.
trip_fares_watching = StreamAttributeGroup(
name="trip_fares_watching",
version=1,
attribute_key=domain_userid,
owner="you@example.com",
attributes=[watched_route_7d, routes_priced_7d, watched_depart_7d, last_fare_seen],
)// In the job that opens fare watches in your CRM
const traveler = await signals.getServiceAttributes({
name: "trip_fares_watching",
attribute_key: "domain_userid",
identifier: domainUserId,
});
// { watched_route_7d: "ORD-LIS", routes_priced_7d: { "ORD-LIS": 3 }, watched_depart_7d: "2026-11-14", last_fare_seen: 412.0 }
// Your CRM holds the email, if they left one, under your consent rules. Signals holds none.
const contact = crm.contactFor(domainUserId);
const pricedThisRoute = traveler.routes_priced_7d[traveler.watched_route_7d] ?? 0;
if (pricedThisRoute >= 3) {
crm.watchFare({
browser: domainUserId,
to: contact,
route: traveler.watched_route_7d,
depart: traveler.watched_depart_7d,
alertBelow: traveler.last_fare_seen,
});
}