From b99abba48a3b950813a83a6374f2b4fcf3460f59 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 27 Aug 2026 15:29:26 +0200 Subject: [PATCH 1/6] docs: add release note for Signals ML feature attributes Announces the three attribute-definition additions that support building ML model features on Signals attributes: the time_since_last and time_since_first aggregations, the date_part modifier on timestamp properties, and the optional event filter. The reference documentation for all three shipped in #1909; this is the follow-up release note for it. Co-Authored-By: Claude Opus 5 --- .../index.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 release-notes/new-in-signals-attributes-for-ml-features/index.md diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md new file mode 100644 index 000000000..414c166cd --- /dev/null +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -0,0 +1,59 @@ +--- +title: "New in Signals: attributes built for ML features" +description: "Recency and tenure aggregations, date part modifiers on timestamp properties, and an optional event filter make it possible to express common ML model features as Signals attributes." +date: "2026-08-27" +category: + - "Product news" +components: + - "Signals" + - "AI tools" +--- +Signals recently gained a [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/), which turns the attribute groups you already serve from into a labeled training table in your warehouse. That closed the gap between training and serving: the same attribute definition produces the feature your model trains on and the feature it scores against. + +It also put more weight on the definitions themselves. If a feature your model needs can't be expressed as a Signals attribute, it can't come from Signals at either end, and you're back to hand-written SQL for training and a separate code path at inference time. + +Three additions to attribute definitions close some of the more common gaps. Recency and tenure are now aggregations rather than something you compute yourself, timestamps can be reduced to the part of the calendar that carries the signal, and the event filter is optional so an attribute can span every event type. + +## Measure recency and tenure + +How long ago something happened is one of the more predictive inputs available to a behavioral model, and until now it wasn't expressible as an attribute. You could count a user's page views or take the last value they produced, but "how long since they last visited" meant reading a raw timestamp out and doing the arithmetic in your own code. + +Two aggregations now do it directly: + +* `time_since_last` measures the duration since the most recent matching event, which is the recency signal behind re-engagement, churn, and session-freshness features. +* `time_since_first` measures the duration since the earliest matching event, which gives you tenure: cohort age, onboarding progress, and lifecycle stage. + +Both take a `time_unit` of `s`, `min`, `h`, or `d`, and return a fractional duration in that unit. They always measure against the event's `derived_tstamp`, so there's no property to select. + +The value is computed when you read the attribute, not when the event arrives, so it reflects the time elapsed at the moment of retrieval rather than a number that goes stale in the Profiles Store. A model scoring a visitor mid-session sees how long that visitor has actually been idle. Out-of-order events can't push the result below zero, as it's clamped at 0. + +## Extract the part of a timestamp that carries signal + +A raw timestamp is close to useless as a model feature, because every value is distinct. What tends to matter is the cyclical part: the hour of the day, the day of the week, or how many separate days a user has been active. + +Timestamp properties now take an optional `date_part` modifier, which transforms the value before it's aggregated. Two families are available: + +| Family | Date parts | Output | +| --- | --- | --- | +| Extract | `hour_of_day`, `day_of_week`, `month_of_year` | Integer for the cyclical component | +| Truncate | `active_day`, `active_week`, `active_month` | Date string truncated to the boundary | + +Because the transform happens before aggregation, the existing aggregations compose with it to give you the feature you want from the same property. A `most_frequent` over `hour_of_day` is a peak-hour feature. A `category_count` over the same property is a full hour-of-day histogram. An `approx_count_distinct` over `active_day` counts how many distinct days a user showed up. + +The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself works the same way. It's also available in criteria, which lets you filter the events feeding an attribute by calendar position: weekdays only, or a single month. + +## Calculate attributes across all event types + +An attribute previously had to name at least one event schema. That's the right constraint for a feature about a specific behavior, but it gets in the way of the whole-pipeline features that models often want, where enumerating every schema is both tedious and wrong the moment someone adds a new event. + +The event filter is now optional. Leave it empty, or pass `events=[]` in the Python SDK, and the attribute is calculated from every event Signals processes, including event types added after you published it. + +This makes a general engagement counter a single definition rather than a list of schemas to maintain, and it means an attribute over an atomic property picks that property up wherever it appears. A first-touch `mkt_medium` attribute captures the value from whichever event carried it, with no need to work out in advance which schemas those might be. + +Because an empty filter is indistinguishable from one you forgot to fill in, it has to be set deliberately. A request that sends an empty event list is read as matching all events rather than rejected, so it's worth confirming that's what you meant. + +## Get started + +All three are available in Console and in the Python SDK, for both stream and batch attribute groups. Upgrade to `snowplow-signals` version 0.4.8 or later to define them from the SDK. + +See the documentation on [defining attributes](/docs/signals/attributes/attributes/) for the full reference, including which aggregations accept a date part and which attribute types each date part family requires. To use these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). From 7a77a517f4393fcd099105782b72d41bfdcc8406 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 27 Aug 2026 15:53:38 +0200 Subject: [PATCH 2/6] docs: tighten release note prose against the style guide Applies the human-voice guidance: drops the preview paragraph that restated all three sections, removes unsourced claims about how predictive recency is and how useless raw timestamps are, replaces the "part of the calendar that carries the signal" heading and phrasing, and leads each section with what shipped rather than a problem/solution beat. Also converts the two-item bullet list to prose and aligns the closing heading with the ML training datasets note. Co-Authored-By: Claude Opus 5 --- .../index.md | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md index 414c166cd..38c5be9c5 100644 --- a/release-notes/new-in-signals-attributes-for-ml-features/index.md +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -8,52 +8,47 @@ components: - "Signals" - "AI tools" --- -Signals recently gained a [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/), which turns the attribute groups you already serve from into a labeled training table in your warehouse. That closed the gap between training and serving: the same attribute definition produces the feature your model trains on and the feature it scores against. +The [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) turns the attribute groups you already serve from into a labeled training table in your warehouse. The same attribute definition then produces the feature your model trains on and the feature it scores against at inference time. -It also put more weight on the definitions themselves. If a feature your model needs can't be expressed as a Signals attribute, it can't come from Signals at either end, and you're back to hand-written SQL for training and a separate code path at inference time. - -Three additions to attribute definitions close some of the more common gaps. Recency and tenure are now aggregations rather than something you compute yourself, timestamps can be reduced to the part of the calendar that carries the signal, and the event filter is optional so an attribute can span every event type. +That makes the definitions themselves the limit. If a feature your model needs can't be written as a Signals attribute, it can't come from Signals at either end, which leaves you maintaining SQL for training and separate code at inference time. Three additions to attribute definitions widen what you can express. ## Measure recency and tenure -How long ago something happened is one of the more predictive inputs available to a behavioral model, and until now it wasn't expressible as an attribute. You could count a user's page views or take the last value they produced, but "how long since they last visited" meant reading a raw timestamp out and doing the arithmetic in your own code. - -Two aggregations now do it directly: +Two new aggregations measure how long ago an event happened. `time_since_last` measures the duration since the most recent matching event, for features about re-engagement, churn, or whether a session has gone quiet. `time_since_first` measures the duration since the earliest matching event, which gives you cohort age, onboarding progress, and lifecycle stage. -* `time_since_last` measures the duration since the most recent matching event, which is the recency signal behind re-engagement, churn, and session-freshness features. -* `time_since_first` measures the duration since the earliest matching event, which gives you tenure: cohort age, onboarding progress, and lifecycle stage. +Both take a `time_unit` of `s`, `min`, `h`, or `d` and return a fractional duration in that unit. They always measure against the event's `derived_tstamp`, so there's no property to select. -Both take a `time_unit` of `s`, `min`, `h`, or `d`, and return a fractional duration in that unit. They always measure against the event's `derived_tstamp`, so there's no property to select. +Signals computes the value when you read the attribute rather than when the event arrives, so it reflects the time elapsed at the moment of retrieval instead of a stored number that ages. A model scoring a visitor mid-session sees how long that visitor has actually been idle. Out-of-order events can't produce a negative result, as the value is clamped at 0. -The value is computed when you read the attribute, not when the event arrives, so it reflects the time elapsed at the moment of retrieval rather than a number that goes stale in the Profiles Store. A model scoring a visitor mid-session sees how long that visitor has actually been idle. Out-of-order events can't push the result below zero, as it's clamped at 0. +Previously you could count a user's page views or read the timestamp of their last one, but converting that timestamp into an elapsed duration meant doing the arithmetic in your own code, at both training and inference time. -## Extract the part of a timestamp that carries signal +## Apply a date part to a timestamp -A raw timestamp is close to useless as a model feature, because every value is distinct. What tends to matter is the cyclical part: the hour of the day, the day of the week, or how many separate days a user has been active. +Timestamp properties now take an optional `date_part` modifier, which transforms the value before it's aggregated. A raw timestamp is hard to use as a model feature because every value is distinct. What a model can use is the hour of the day, the day of the week, or a count of the separate days a user has been active. -Timestamp properties now take an optional `date_part` modifier, which transforms the value before it's aggregated. Two families are available: +There are two families: | Family | Date parts | Output | | --- | --- | --- | | Extract | `hour_of_day`, `day_of_week`, `month_of_year` | Integer for the cyclical component | | Truncate | `active_day`, `active_week`, `active_month` | Date string truncated to the boundary | -Because the transform happens before aggregation, the existing aggregations compose with it to give you the feature you want from the same property. A `most_frequent` over `hour_of_day` is a peak-hour feature. A `category_count` over the same property is a full hour-of-day histogram. An `approx_count_distinct` over `active_day` counts how many distinct days a user showed up. +Because the transform happens before aggregation, the existing aggregations combine with it to produce different features from the same property. A `most_frequent` over `hour_of_day` gives you a peak hour. A `category_count` over the same property gives you an hour-of-day histogram. An `approx_count_distinct` over `active_day` counts how many separate days a user showed up. -The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself works the same way. It's also available in criteria, which lets you filter the events feeding an attribute by calendar position: weekdays only, or a single month. +The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself behaves the same way. It's also available in criteria, which lets you filter the events feeding an attribute by calendar position: weekdays only, or a single month. ## Calculate attributes across all event types -An attribute previously had to name at least one event schema. That's the right constraint for a feature about a specific behavior, but it gets in the way of the whole-pipeline features that models often want, where enumerating every schema is both tedious and wrong the moment someone adds a new event. +The event filter is now optional. Leave it empty, or pass `events=[]` in the Python SDK, and Signals calculates the attribute from every event it processes, including event types you add after publishing. -The event filter is now optional. Leave it empty, or pass `events=[]` in the Python SDK, and the attribute is calculated from every event Signals processes, including event types added after you published it. +An attribute previously had to name at least one event schema. That fits a feature about one specific behavior, but it made whole-pipeline features awkward, because you had to list every schema and then remember to update the list whenever someone added an event. -This makes a general engagement counter a single definition rather than a list of schemas to maintain, and it means an attribute over an atomic property picks that property up wherever it appears. A first-touch `mkt_medium` attribute captures the value from whichever event carried it, with no need to work out in advance which schemas those might be. +A general engagement counter is now a single definition rather than a list of schemas to maintain. An attribute over an atomic property also picks that property up wherever it appears, so a first-touch `mkt_medium` attribute captures the value from whichever event carried it, without you working out in advance which schemas those are. -Because an empty filter is indistinguishable from one you forgot to fill in, it has to be set deliberately. A request that sends an empty event list is read as matching all events rather than rejected, so it's worth confirming that's what you meant. +One thing to know: an empty filter looks the same as one you meant to fill in and didn't. A request sending an empty event list creates a match-all attribute rather than returning a validation error, so it's worth checking that's what you intended. -## Get started +## Getting started All three are available in Console and in the Python SDK, for both stream and batch attribute groups. Upgrade to `snowplow-signals` version 0.4.8 or later to define them from the SDK. -See the documentation on [defining attributes](/docs/signals/attributes/attributes/) for the full reference, including which aggregations accept a date part and which attribute types each date part family requires. To use these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). +The documentation on [defining attributes](/docs/signals/attributes/attributes/) has the full reference, including which aggregations accept a date part and which attribute types each date part family requires. For using these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). From 01d7e079e48fdf6e73f4252000bed0871cc400a9 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 27 Aug 2026 15:58:40 +0200 Subject: [PATCH 3/6] docs: lead the release note with examples and cut prose Restructures each section around a short Python example instead of explaining the mechanics in prose, and replaces the date_part explanation with a table showing what each aggregation over hour_of_day returns. Down from 900 to 650 words. All three examples and the aggregation table were verified by constructing them against snowplow-signals 0.4.8. Co-Authored-By: Claude Opus 5 --- .../index.md | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md index 38c5be9c5..72418569d 100644 --- a/release-notes/new-in-signals-attributes-for-ml-features/index.md +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -8,47 +8,68 @@ components: - "Signals" - "AI tools" --- -The [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) turns the attribute groups you already serve from into a labeled training table in your warehouse. The same attribute definition then produces the feature your model trains on and the feature it scores against at inference time. +The [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) turns the attribute groups you already serve from into a labeled training table in your warehouse, so one definition produces both the feature your model trains on and the feature it scores against. -That makes the definitions themselves the limit. If a feature your model needs can't be written as a Signals attribute, it can't come from Signals at either end, which leaves you maintaining SQL for training and separate code at inference time. Three additions to attribute definitions widen what you can express. +That makes the definitions the limit. Three additions widen what you can express as an attribute. ## Measure recency and tenure -Two new aggregations measure how long ago an event happened. `time_since_last` measures the duration since the most recent matching event, for features about re-engagement, churn, or whether a session has gone quiet. `time_since_first` measures the duration since the earliest matching event, which gives you cohort age, onboarding progress, and lifecycle stage. +`time_since_last` and `time_since_first` return how long ago an event happened, in the `time_unit` you pick (`s`, `min`, `h`, or `d`). Minutes since the last page view: -Both take a `time_unit` of `s`, `min`, `h`, or `d` and return a fractional duration in that unit. They always measure against the event's `derived_tstamp`, so there's no property to select. +```python +Attribute( + name="minutes_since_last_page_view", + type="double", + aggregation="time_since_last", + time_unit="min", + events=[Event(vendor="com.snowplowanalytics.snowplow", name="page_view", version="1-0-0")], +) +``` -Signals computes the value when you read the attribute rather than when the event arrives, so it reflects the time elapsed at the moment of retrieval instead of a stored number that ages. A model scoring a visitor mid-session sees how long that visitor has actually been idle. Out-of-order events can't produce a negative result, as the value is clamped at 0. +Swap the aggregation for `time_since_first` and the same definition gives you tenure instead of recency: how long since the user's first page view, for cohort age or lifecycle stage. Recency feeds churn and re-engagement features, or an intervention rule such as "this session has gone quiet for more than five minutes." -Previously you could count a user's page views or read the timestamp of their last one, but converting that timestamp into an elapsed duration meant doing the arithmetic in your own code, at both training and inference time. +Both always measure against the event's `derived_tstamp`, so there's no `property` to set. Signals computes the value when you read the attribute, not when the event arrives, so a model scoring a visitor mid-session sees how long that visitor has actually been idle rather than a stored number that ages. Out-of-order events can't produce a negative result, as the value is clamped at 0. ## Apply a date part to a timestamp -Timestamp properties now take an optional `date_part` modifier, which transforms the value before it's aggregated. A raw timestamp is hard to use as a model feature because every value is distinct. What a model can use is the hour of the day, the day of the week, or a count of the separate days a user has been active. +A raw timestamp is hard to use as a feature because every value is distinct. The `date_part` modifier transforms it before aggregation, into either a cyclical integer (`hour_of_day`, `day_of_week`, `month_of_year`) or a truncated date string (`active_day`, `active_week`, `active_month`). -There are two families: +```python +AtomicProperty(name="derived_tstamp", date_part="hour_of_day") +``` -| Family | Date parts | Output | -| --- | --- | --- | -| Extract | `hour_of_day`, `day_of_week`, `month_of_year` | Integer for the cyclical component | -| Truncate | `active_day`, `active_week`, `active_month` | Date string truncated to the boundary | +Because the transform happens before aggregation, the aggregation you pair it with decides the feature: -Because the transform happens before aggregation, the existing aggregations combine with it to produce different features from the same property. A `most_frequent` over `hour_of_day` gives you a peak hour. A `category_count` over the same property gives you an hour-of-day histogram. An `approx_count_distinct` over `active_day` counts how many separate days a user showed up. +| Aggregation over `hour_of_day` | Result | +| --- | --- | +| `most_frequent` | The user's peak hour, as an `int32` such as `14` | +| `category_count` | An hour-of-day histogram of event counts | +| `unique_list` | The distinct hours the user was active | -The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself behaves the same way. It's also available in criteria, which lets you filter the events feeding an attribute by calendar position: weekdays only, or a single month. +Pair `active_day` with `unique_list` for the list of dates a user showed up, or with `approx_count_distinct` for a count of active days. Filtering to weekdays is `day_of_week` `in` `[1,2,3,4,5]`, following ISO 8601 where 1 is Monday. + +The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself behaves the same way. ## Calculate attributes across all event types -The event filter is now optional. Leave it empty, or pass `events=[]` in the Python SDK, and Signals calculates the attribute from every event it processes, including event types you add after publishing. +Pass `events=[]` and the attribute is calculated from every event Signals processes, including event types you add later. A total engagement counter over a rolling 30-day window: -An attribute previously had to name at least one event schema. That fits a feature about one specific behavior, but it made whole-pipeline features awkward, because you had to list every schema and then remember to update the list whenever someone added an event. +```python +Attribute( + name="n_events_30d", + type="int32", + events=[], + aggregation="counter", + period=timedelta(days=30), +) +``` -A general engagement counter is now a single definition rather than a list of schemas to maintain. An attribute over an atomic property also picks that property up wherever it appears, so a first-touch `mkt_medium` attribute captures the value from whichever event carried it, without you working out in advance which schemas those are. +Previously this meant listing every schema in your pipeline and updating the list whenever someone added an event. The same applies to attributes over atomic properties: a `first` aggregation on `mkt_medium` with no event filter captures first-touch marketing medium from whichever event carried it, with no need to work out in advance which schemas those are. -One thing to know: an empty filter looks the same as one you meant to fill in and didn't. A request sending an empty event list creates a match-all attribute rather than returning a validation error, so it's worth checking that's what you intended. +One thing to know: an empty filter looks the same as one you meant to fill in and didn't. Signals treats it as match-all rather than returning a validation error, so check that's what you intended. ## Getting started All three are available in Console and in the Python SDK, for both stream and batch attribute groups. Upgrade to `snowplow-signals` version 0.4.8 or later to define them from the SDK. -The documentation on [defining attributes](/docs/signals/attributes/attributes/) has the full reference, including which aggregations accept a date part and which attribute types each date part family requires. For using these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). +The documentation on [defining attributes](/docs/signals/attributes/attributes/) has the full reference, including every aggregation that accepts a date part and the attribute types each date part family requires. For using these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). From 396380e8930c925f911303338f8590f260e014b0 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 27 Aug 2026 16:11:18 +0200 Subject: [PATCH 4/6] docs: reframe the release note around ML feature types Rewrites the note to lead with the kinds of model features these additions enable rather than the mechanics of the API. Opens on the timing and habit questions a propensity model asks, groups the three changes as recency/tenure, seasonality/habit, and whole-pipeline features, and moves the detail into deep links to the relevant docs sections and examples. Drops the Python examples, which duplicated the documentation, and the dataset-builder framing from the opening sentence. Down to 575 words. Co-Authored-By: Claude Opus 5 --- .../index.md | 66 +++++-------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md index 72418569d..978322efc 100644 --- a/release-notes/new-in-signals-attributes-for-ml-features/index.md +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -1,6 +1,6 @@ --- title: "New in Signals: attributes built for ML features" -description: "Recency and tenure aggregations, date part modifiers on timestamp properties, and an optional event filter make it possible to express common ML model features as Signals attributes." +description: "Recency, tenure, and time-of-day attributes, plus attributes that span every event type, cover more of the feature set a behavioral model needs." date: "2026-08-27" category: - "Product news" @@ -8,68 +8,32 @@ components: - "Signals" - "AI tools" --- -The [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) turns the attribute groups you already serve from into a labeled training table in your warehouse, so one definition produces both the feature your model trains on and the feature it scores against. +A propensity model is mostly a question about timing and habit. Is this visitor back sooner than usual, or have they gone quiet? Are they new, or have they been around for months? Do they shop on weekday evenings, and is this a weekday evening? How many separate days have they shown up at all? -That makes the definitions the limit. Three additions widen what you can express as an attribute. +Those features were awkward to build in Signals. You could count a user's page views and take the last value of a property, but recency, tenure, and time-of-day meant pulling raw timestamps out and doing the arithmetic yourself, in two places: once over history to train, and again at serving time to score. Since the [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) now generates training data from the same attribute definitions you serve from, anything you can't define as an attribute is work you maintain twice. -## Measure recency and tenure +Three additions close most of that gap. -`time_since_last` and `time_since_first` return how long ago an event happened, in the `time_unit` you pick (`s`, `min`, `h`, or `d`). Minutes since the last page view: +## Recency and tenure -```python -Attribute( - name="minutes_since_last_page_view", - type="double", - aggregation="time_since_last", - time_unit="min", - events=[Event(vendor="com.snowplowanalytics.snowplow", name="page_view", version="1-0-0")], -) -``` +[Time since aggregations](/docs/signals/attributes/attributes/#time-since-aggregations) answer "how long ago" directly, in seconds, minutes, hours, or days. `time_since_last` gives you recency, the signal behind churn, re-engagement, and whether a session has gone quiet. `time_since_first` gives you tenure: cohort age, onboarding progress, lifecycle stage. -Swap the aggregation for `time_since_first` and the same definition gives you tenure instead of recency: how long since the user's first page view, for cohort age or lifecycle stage. Recency feeds churn and re-engagement features, or an intervention rule such as "this session has gone quiet for more than five minutes." +Both are computed when you read the attribute, not when the event arrives, so a model scoring someone mid-session sees how long they have actually been idle. They also work in [intervention rules](/docs/signals/interventions/), which is how you act on "this visitor has been inactive for five minutes" while they are still on the page. -Both always measure against the event's `derived_tstamp`, so there's no `property` to set. Signals computes the value when you read the attribute, not when the event arrives, so a model scoring a visitor mid-session sees how long that visitor has actually been idle rather than a stored number that ages. Out-of-order events can't produce a negative result, as the value is clamped at 0. +## Seasonality and habit -## Apply a date part to a timestamp +A raw timestamp is close to unusable as a feature, since every value is unique. [Date parts](/docs/signals/attributes/attributes/#apply-a-date-part) reduce one to the part a model can learn from: hour of day, day of week, month of year, or the day, week, and month a user was active. -A raw timestamp is hard to use as a feature because every value is distinct. The `date_part` modifier transforms it before aggregation, into either a cyclical integer (`hour_of_day`, `day_of_week`, `month_of_year`) or a truncated date string (`active_day`, `active_week`, `active_month`). +Combined with the aggregations Signals already has, that covers a useful range. Peak shopping hour, most common active weekday, an hour-by-hour histogram of when someone engages, or a count of the distinct days they have shown up, which is a decent proxy for habit. Date parts also work in [criteria](/docs/signals/attributes/attributes/#filter-with-criteria), so you can build features from weekday traffic only. -```python -AtomicProperty(name="derived_tstamp", date_part="hour_of_day") -``` +## Whole-pipeline features -Because the transform happens before aggregation, the aggregation you pair it with decides the feature: +Some features are about a user's overall activity rather than one behavior, and those needed you to list every event schema in your pipeline and keep the list current. The [event filter is now optional](/docs/signals/attributes/attributes/#select-events): leave it empty and the attribute covers every event Signals processes, including event types you add later. -| Aggregation over `hour_of_day` | Result | -| --- | --- | -| `most_frequent` | The user's peak hour, as an `int32` such as `14` | -| `category_count` | An hour-of-day histogram of event counts | -| `unique_list` | The distinct hours the user was active | - -Pair `active_day` with `unique_list` for the list of dates a user showed up, or with `approx_count_distinct` for a count of active days. Filtering to weekdays is `day_of_week` `in` `[1,2,3,4,5]`, following ISO 8601 where 1 is Monday. - -The modifier works on atomic timestamp fields such as `derived_tstamp`, and on any event or entity property whose schema declares `format: date-time`, so a timestamp you track yourself behaves the same way. - -## Calculate attributes across all event types - -Pass `events=[]` and the attribute is calculated from every event Signals processes, including event types you add later. A total engagement counter over a rolling 30-day window: - -```python -Attribute( - name="n_events_30d", - type="int32", - events=[], - aggregation="counter", - period=timedelta(days=30), -) -``` - -Previously this meant listing every schema in your pipeline and updating the list whenever someone added an event. The same applies to attributes over atomic properties: a `first` aggregation on `mkt_medium` with no event filter captures first-touch marketing medium from whichever event carried it, with no need to work out in advance which schemas those are. - -One thing to know: an empty filter looks the same as one you meant to fill in and didn't. Signals treats it as match-all rather than returning a validation error, so check that's what you intended. +That makes total engagement counters a single definition, and it lets an attribute follow a property wherever it appears, such as capturing first-touch `mkt_medium` from whichever event carried it. Worth knowing: an empty filter looks the same as one you meant to fill in and didn't, and Signals reads it as match-all rather than flagging it. ## Getting started -All three are available in Console and in the Python SDK, for both stream and batch attribute groups. Upgrade to `snowplow-signals` version 0.4.8 or later to define them from the SDK. +All three are available in Console and in the Python SDK, for both stream and batch attribute groups. Upgrade to `snowplow-signals` version 0.4.8 or later to use them from the SDK. -The documentation on [defining attributes](/docs/signals/attributes/attributes/) has the full reference, including every aggregation that accepts a date part and the attribute types each date part family requires. For using these attributes as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). +The [defining attributes](/docs/signals/attributes/attributes/) page has runnable examples for each, including [time since last event](/docs/signals/attributes/attributes/#time-since-last-event), [most frequent hour of day](/docs/signals/attributes/attributes/#most-frequent-hour-of-day), and a [global event counter](/docs/signals/attributes/attributes/#global-event-counter). To use these as model features, see [creating ML training datasets](/docs/signals/ml-training-datasets/). From 34ea66517b5861b4daa0aa6ce74eabe957fd9c15 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Thu, 27 Aug 2026 16:14:59 +0200 Subject: [PATCH 5/6] docs: drop the intervention use case from time since aggregations Using time since aggregations in intervention rules isn't a recommended pattern and has edge cases, so the note shouldn't point people at it. Keeps the read-time computation point, which is what matters for model features. Co-Authored-By: Claude Opus 5 --- .../new-in-signals-attributes-for-ml-features/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md index 978322efc..dc5f129b8 100644 --- a/release-notes/new-in-signals-attributes-for-ml-features/index.md +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -18,7 +18,7 @@ Three additions close most of that gap. [Time since aggregations](/docs/signals/attributes/attributes/#time-since-aggregations) answer "how long ago" directly, in seconds, minutes, hours, or days. `time_since_last` gives you recency, the signal behind churn, re-engagement, and whether a session has gone quiet. `time_since_first` gives you tenure: cohort age, onboarding progress, lifecycle stage. -Both are computed when you read the attribute, not when the event arrives, so a model scoring someone mid-session sees how long they have actually been idle. They also work in [intervention rules](/docs/signals/interventions/), which is how you act on "this visitor has been inactive for five minutes" while they are still on the page. +Both are computed when you read the attribute, not when the event arrives, so a model scoring someone mid-session sees how long they have actually been idle rather than a value that aged in storage. ## Seasonality and habit From fdc33ea7c10b9018454ef92e79ed6030700315c3 Mon Sep 17 00:00:00 2001 From: Matus Tomlein Date: Fri, 28 Aug 2026 09:15:22 +0200 Subject: [PATCH 6/6] docs: address review feedback on the Signals attributes release note - Retitle to "recency, tenure, and time-of-day attributes" - Rename the "Whole-pipeline features" heading to "All event features" - Soften the claim about raw timestamps being unusable as features - Drop the unclear caveat about empty event filters - Use "user" rather than "visitor", per the style guide Reword the description so it no longer repeats the new title verbatim. Co-Authored-By: Claude Opus 5 --- .../index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/release-notes/new-in-signals-attributes-for-ml-features/index.md b/release-notes/new-in-signals-attributes-for-ml-features/index.md index dc5f129b8..928694e24 100644 --- a/release-notes/new-in-signals-attributes-for-ml-features/index.md +++ b/release-notes/new-in-signals-attributes-for-ml-features/index.md @@ -1,6 +1,6 @@ --- -title: "New in Signals: attributes built for ML features" -description: "Recency, tenure, and time-of-day attributes, plus attributes that span every event type, cover more of the feature set a behavioral model needs." +title: "New in Signals: recency, tenure, and time-of-day attributes" +description: "Time since aggregations, date part modifiers on timestamps, and an optional event filter cover more of the feature set a behavioral model needs." date: "2026-08-27" category: - "Product news" @@ -8,7 +8,7 @@ components: - "Signals" - "AI tools" --- -A propensity model is mostly a question about timing and habit. Is this visitor back sooner than usual, or have they gone quiet? Are they new, or have they been around for months? Do they shop on weekday evenings, and is this a weekday evening? How many separate days have they shown up at all? +A propensity model is mostly a question about timing and habit. Is this user back sooner than usual, or have they gone quiet? Are they new, or have they been around for months? Do they shop on weekday evenings, and is this a weekday evening? How many separate days have they shown up at all? Those features were awkward to build in Signals. You could count a user's page views and take the last value of a property, but recency, tenure, and time-of-day meant pulling raw timestamps out and doing the arithmetic yourself, in two places: once over history to train, and again at serving time to score. Since the [training dataset builder](/release-notes/new-in-signals-ml-training-datasets/) now generates training data from the same attribute definitions you serve from, anything you can't define as an attribute is work you maintain twice. @@ -22,15 +22,15 @@ Both are computed when you read the attribute, not when the event arrives, so a ## Seasonality and habit -A raw timestamp is close to unusable as a feature, since every value is unique. [Date parts](/docs/signals/attributes/attributes/#apply-a-date-part) reduce one to the part a model can learn from: hour of day, day of week, month of year, or the day, week, and month a user was active. +A raw timestamp is hard for a model to learn from, since every value is unique. [Date parts](/docs/signals/attributes/attributes/#apply-a-date-part) reduce one to the part a model can learn from: hour of day, day of week, month of year, or the day, week, and month a user was active. Combined with the aggregations Signals already has, that covers a useful range. Peak shopping hour, most common active weekday, an hour-by-hour histogram of when someone engages, or a count of the distinct days they have shown up, which is a decent proxy for habit. Date parts also work in [criteria](/docs/signals/attributes/attributes/#filter-with-criteria), so you can build features from weekday traffic only. -## Whole-pipeline features +## All event features Some features are about a user's overall activity rather than one behavior, and those needed you to list every event schema in your pipeline and keep the list current. The [event filter is now optional](/docs/signals/attributes/attributes/#select-events): leave it empty and the attribute covers every event Signals processes, including event types you add later. -That makes total engagement counters a single definition, and it lets an attribute follow a property wherever it appears, such as capturing first-touch `mkt_medium` from whichever event carried it. Worth knowing: an empty filter looks the same as one you meant to fill in and didn't, and Signals reads it as match-all rather than flagging it. +That makes total engagement counters a single definition, and it lets an attribute follow a property wherever it appears, such as capturing first-touch `mkt_medium` from whichever event carried it. ## Getting started