Skip to content

Activity selectors

Activity selectors give you control over which data is shown in a feed. For example you can decide to show only popular activities, or activities that fit the current user's interests.

Info:

Each activity selector selects the first 1000 activities that match its selection criteria.

If cutoff_window is not set (and the selector has no built-in default — see the parameter tables below), no time-based filter is applied. The selector returns the latest 1000 matching activities regardless of when they were created — effectively cutoff_window = infinity, capped only by the 1000-activity limit.

The minimum accepted value for cutoff_window is 1h. Supported units are s, m, h, d, w, y. cutoff_window cannot be set together with cutoff_time.

This page details what kind of activity selectors are supported by the Stream API, and how can you configure them.

You can create custom feed groups or update the built-in groups with your own activity selector configuration.

Selector types

Current Feed Selector

Shows activities from the current feed. This is the selector used when reading a user feed.

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Newest first No
filter object, see Filters Additional filter conditions - No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected - No

This selector does not accept a feed group scope. It already reads exactly one feed, so scoping it to a set of feed groups is either a no-op or a request for "only the items here that were also cross-posted elsewhere", and it is rejected with a 400.

Scope

The feed we're currently reading

Example

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Following Feed Selector

Shows activities from followed feeds and activities directly added to the feed being read. This is the selector used when reading a timeline feed.

Parameters

Name Type Description Default Required
filter object, see Filters Additional filter conditions - No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected - No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No

Scope

1 000 latest added activities from feeds followed by the feed we're currently reading, and activities posted to the current feed.

Example

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Selects popular activities from public and visible feeds.

Popularity is computed by the following formula:

activity.popularity = reactions + comments * 2 + bookmarks * 3 + shares * 3;

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Newest first No
filter object, see Filters Additional filter conditions - No
min_popularity number (only positive numbers are accepted) Minimum popularity an activity should have to be selected 5 No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected 7d (last 7 days) No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No

Scope

Any feed with public or visible visibility level.

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Proximity Activity Selector

Shows activities based on geographic proximity

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Newest first No
filter object, see Filters Additional filter conditions - No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected - No
min_popularity number (only positive numbers are accepted) Minimum popularity an activity should have to be selected 0 No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No

Scope

Any feed with public or visible visibility level.

Example

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Interest Activity Selector

Selects activities that match the logged-in user's interests. Interests are automatically calculated for each user by Stream API based on which activities the user interacts with. You can also seed and manage them through the API, see the User interests guide.

Info:

Interests are based on activity topics. Topics are stored in the interest_tags field of an activity. It can be computed automatically using activity processors or set when creating an activity.

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Newest first No
filter object, see Filters Additional filter conditions - No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected 2d (last 2 days) No
min_popularity number (only positive numbers are accepted) Minimum popularity an activity should have to be selected 0 No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No

Scope

Feeds that are visible to the logged-in user.

Examples

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Query Activity Selector

Selects activities using the provided filter query.

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Newest first No
filter object, see Filters Additional filter conditions - No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected 7d (last 7 days) No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No

Scope

Feeds that are visible to the logged-in user.

Example

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Follow Suggestion Activity Selector

Selects activities from feeds that are suggested to the user based on follow suggestions. This selector uses Stream's intelligent follow suggestion algorithm to discover relevant content from feeds the user might want to follow.

Info:

This selector requires an authenticated user. It will return empty results for anonymous users.

Parameters

Name Type Description Default Required
sort array, see Sort Sort options Popularity first No
cutoff_window string, duration like 2h or 5d Activities older than this window won't be selected - No
min_popularity number (only positive numbers are accepted) Minimum popularity an activity should have to be selected 5 No
activities_per_feed number (1-100) Number of activities to select from each suggested feed 2 No
max_suggested_feeds number (1-20) Maximum number of suggested feeds to consider 10 No
min_feed_score number (0.0-1.0) Minimum recommendation score for a feed to be included in suggestions 0.3 No
feed_groups object, see Feed group scope Limit selection to an included or excluded set of feed groups - No
Info:

Fan-out limits. To protect read performance, the follow_suggestion selector bounds how far it fans out:

  • max_suggested_feeds has an effective ceiling of 20. A value above 20 runs at 20 (it is clamped, not rejected).
  • max_suggested_feeds × activities_per_feed must not exceed 200. Creating or updating a feed group or view with a larger product is rejected with a 400. Lower either value until the product is ≤ 200 — for example max_suggested_feeds: 20 with activities_per_feed: 10 (product 200) is allowed, but 20 × 20 = 400 is not.

Scope

Activities from user feeds that are suggested based on the follow suggestion algorithm. The selector always suggests feeds from the "user" feed group, regardless of which feed group is being viewed.

Example

create_response = self.client.feeds.create_feed_group(
    id="myid",
    activity_selectors=[
        {
            "type": "follow_suggestion",
            "min_popularity": 10,
            "cutoff_window": "10d",
            "params": {
                "activities_per_feed": 3,
                "max_suggested_feeds": 15,
                "min_feed_score": 0.4,
            },
        }
    ],
)

Selector parameters

Supported values for sort and filter objects.

Sort

The following sort options are available:

Info:

Please note that sort options are only used to determine which activities are selected. The final activity order is determined by ranking.

Fields:

  • created_at
  • popularity

Direction: 1 or -1

Filters

The following filter options are available for the following selector:

name type description supported operations example
id string or list of strings The ID of the activity $in, $eq { id: { $in: [ 'abc', 'xyz' ] } }
filter_tags list of strings Tags for filtering $eq, $contains, $in { filter_tags: { $in: [ 'categoryA', 'categoryB' ] } }

The following filter options are available for the current, popular, interest, proximity and query selectors:

name type description supported operations example
id string or list of strings The ID of the activity $in, $eq { id: { $in: [ 'abc', 'xyz' ] } }
activity_type string or list of strings The type of the activity $in, $eq { activity_type: { $in: [ 'abc', 'xyz' ] } }
user_id string or list of strings The ID of the user who created the activity $in, $eq { user_id: { $in: [ 'abc', 'xyz' ] } }
text string The text content of the activity $eq, $q, $autocomplete { text: { $q: 'popularity' } }
search_data object The extra metadata for search indexing $contains, $path_exists { search_data: { $contains: { 'category': 'sports', 'status': 'active' } } }
interest_tags list of strings Tags for user interests $eq, $contains { interest_tags: { $in: [ 'sports', 'music' ] } }
filter_tags list of strings Tags for filtering $eq, $contains, $in { filter_tags: { $in: [ 'categoryA', 'categoryB' ] } }
created_at string, must be formatted as an RFC3339 timestamp The time the activity was created $eq, $gt, $lt, $gte, $lte { created_at: { $gte: '2023-12-04T09:30:20.45Z' } }
popularity number The popularity score of the activity $eq, $ne, $gt, $lt, $gte, $lte { popularity: { $gte: 70 } }
near object GEO point and a distance (in km) to search for activities within $eq { near: { $eq: { lat: 40.0, lng: -74.0, distance: 200 } } }
within_bounds object GEO bounds to search for activities within $eq { within_bounds: { $eq: { ne_lat: 40.0, ne_lng: -115.0, sw_lat: 32.0, sw_lng: -125.0 } } }

When filtering by filter_tags, a plain array (or $eq) uses AND-logic: the activity must contain all of the specified tags. Use $in if you want OR-logic, where the activity must contain any of the specified tags.

The filter syntax also supports $or and $and:

# Get all the activities where filter tags contain both "green" and "orange"
filter = {
  "$and": [
    {"filter_tags": ["green"]},
    {"filter_tags": ["orange"]}
  ]
}

It's also possible to provide filters when reading a feed. When providing filter to read a feed, group-level filters are ignored.

Feed group scope

feed_groups limits what a selector is allowed to discover to a set of feed groups. Set either include or exclude, never both.

Name Type Description
include list of strings Select only activities that live in a feed belonging to one of these feed groups
exclude list of strings Select activities from every feed group except these

Both lists hold feed group IDs (campaigns, user), not full feed IDs (campaigns:123).

Select only from the campaigns and teams groups:

{ "type": "popular", "feed_groups": { "include": ["campaigns", "teams"] } }

Select from everything except the drafts group:

{ "type": "popular", "feed_groups": { "exclude": ["drafts"] } }

The scope is optional, and a selector without it selects exactly what it selected before this field existed. It is supported on every selector except current_feed, which already reads a single feed.

update_response = self.client.feeds.update_feed_group(
    id="myid",
    activity_selectors=[
        {
            "type": "popular",
            "feed_groups": {"exclude": ["drafts"]},
        }
    ],
)

Cross-posted activities

An activity can be posted to several feeds at once, and those feeds can belong to different feed groups. The two directions treat that case differently:

Direction Rule Effect
include Selected when at least one of the activity's feeds is in an included group Narrow. A new feed group is invisible until you add it
exclude Dropped only when every one of the activity's feeds is in an excluded group Broad. A new feed group is discoverable right away

So an activity cross-posted to an excluded group and to a group that is not excluded is still selected.

This is why exclude is the safer form to reach for. An include list fails closed: a feed group created after the list was written is invisible to that selector until every list naming its siblings is updated, and nothing warns you that it is missing.

The following selector matches the delivering feed

On the following selector the scope is evaluated against the feed the activity was delivered from, rather than against every feed the activity was posted to. For an activity that arrived through a follow, that is the followed feed that fanned it out. For an activity added directly to the feed being read, it is that feed itself.

For a timeline this is usually the reading you want ("don't show me anything that came from drafts"), but it has two consequences:

  • A cross-posted activity can be admitted by a popular selector and dropped by a following selector in the same feed.
  • The feed's own group counts as a source. An include list that does not name the group of the feed being read drops the activities posted directly to that feed, and an exclude list that names that group drops them too. A timeline feed whose following selector carries { "include": ["user"] } shows what the user follows and nothing that was posted to the timeline itself. Add timeline to the list to keep those.

follow_suggestion only draws from the user group

The follow suggestion algorithm only ever suggests feeds in the user feed group, so a scope on that selector is either a no-op or a selector that can never return anything. The two shapes that can never return anything are rejected with a 400:

Scope on follow_suggestion Result
include contains user Accepted, no effect
include without user Rejected with a 400
exclude contains user Rejected with a 400
exclude without user Accepted, no effect

So an exclude list naming other groups stays valid on this selector, and one scope can be applied to every selector in a view. An include list has to name user alongside the other groups, or the selector has to be dropped from that view.

Limits and validation

  • One direction per selector. Sending both include and exclude is rejected with a 400.
  • At most 20 feed groups per list.
  • Entries cannot be blank, and cannot repeat within a list.
  • feed_groups on a current_feed selector is rejected with a 400.
  • On a follow_suggestion selector, a scope that rules out the user feed group is rejected with a 400, because that selector could then never return anything. See the section above.
  • Feed group IDs are not checked against the feed groups that exist, so an exclude list can name a group before you create it. The trade-off is that a typo in an include list is accepted and quietly narrows or empties that selector.

Performance

A scope that keeps most of the candidate activities is effectively free, and an exclude list that removes a small group can make the read slightly cheaper. A scope that rejects almost everything is not: the selector has to walk much further down the candidate set to fill a page, and the read gets slower while returning few activities, or none.

The direction is not what predicts the cost, the surviving volume is. A long exclude list that removes the groups holding most of the popular content is just as expensive as a narrow include list.

Keep scope lists short, and avoid scoping away the feed groups that hold most of the content the selector is meant to surface.

Hot feed cache

A feed group or view where any selector carries a feed_groups scope is not eligible for hot feed cache. Those reads fall back to a normal feed read, with no error.

Combining selectors

You can combine multiple selectors. The example below will include:

  • popular activities from public and visible feeds
  • activities from feeds the user follows
  • activities from feeds the user has access to, and match the user's interest

Each activity selector selects the first 1000 activities that match its selection criteria. We suggest taking adventage of the different config options for the selectors to make sure that the selectors return the most relevant activities. When combining selectors, every selector runs independently, so in this case 3 * 1000 activities can be selected.

Once selectors run, ranking is applied to the activities that are returned by the selectors. Even if you have multiple selectors, ranking only runs once.

You can't use more than 3 selectors in a feed group/view configuration.

create_response = self.client.feeds.create_feed_group(
    id=feed_group_id,
    default_visibility="public",
)

Real-time updates for activity selectors

For activities selected by current and following selectors WebSocket events are delivered to clients (as long as watch: true is used to read the feed). However, for activities selected by other selectors, WebSocket events are not delivered. This means you won't be receiving activity.new events, or updates if other users like/comment/etc. on the activity.

If you combine activity selectors, you'll be still receiving WebSocket events for activities selected by current and following selectors.

Experimenting with selectors

Feed groups let you define what activities should be included in the feed and the ranking to sort these activities.

By default all feeds in the given group will have the same settings. However, you might want to experiment with different selectors and rankings. Feed views let you do that by overriding the group's default settings.

Info:

Note that any write operation to feed groups/views can take up to 30 seconds to propagate to all API nodes.