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.

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

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsNewest firstNo
filterobject, see FiltersAdditional filter conditions-No
cutoff_windowstring, duration like 2h or 5dActivities 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

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

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

NameTypeDescriptionDefaultRequired
filterobject, see FiltersAdditional filter conditions-No
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected-No
feed_groupsobject, see Feed group scopeLimit 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

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

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

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsNewest firstNo
filterobject, see FiltersAdditional filter conditions-No
min_popularitynumber (only positive numbers are accepted)Minimum popularity an activity should have to be selected5No
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected7d (last 7 days)No
feed_groupsobject, see Feed group scopeLimit selection to an included or excluded set of feed groups-No

Scope

Any feed with public or visible visibility level.

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

Proximity Activity Selector

Shows activities based on geographic proximity

Parameters

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsNewest firstNo
filterobject, see FiltersAdditional filter conditions-No
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected-No
min_popularitynumber (only positive numbers are accepted)Minimum popularity an activity should have to be selected0No
feed_groupsobject, see Feed group scopeLimit selection to an included or excluded set of feed groups-No

Scope

Any feed with public or visible visibility level.

Example

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

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.

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

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsNewest firstNo
filterobject, see FiltersAdditional filter conditions-No
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected2d (last 2 days)No
min_popularitynumber (only positive numbers are accepted)Minimum popularity an activity should have to be selected0No
feed_groupsobject, see Feed group scopeLimit selection to an included or excluded set of feed groups-No

Scope

Feeds that are visible to the logged-in user.

Examples

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

Query Activity Selector

Selects activities using the provided filter query.

Parameters

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsNewest firstNo
filterobject, see FiltersAdditional filter conditions-No
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected7d (last 7 days)No
feed_groupsobject, see Feed group scopeLimit selection to an included or excluded set of feed groups-No

Scope

Feeds that are visible to the logged-in user.

Example

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

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.

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

Parameters

NameTypeDescriptionDefaultRequired
sortarray, see SortSort optionsPopularity firstNo
cutoff_windowstring, duration like 2h or 5dActivities older than this window won't be selected-No
min_popularitynumber (only positive numbers are accepted)Minimum popularity an activity should have to be selected5No
activities_per_feednumber (1-100)Number of activities to select from each suggested feed2No
max_suggested_feedsnumber (1-20)Maximum number of suggested feeds to consider10No
min_feed_scorenumber (0.0-1.0)Minimum recommendation score for a feed to be included in suggestions0.3No
feed_groupsobject, see Feed group scopeLimit selection to an included or excluded set of feed groups-No

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

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivitySelectors = new List<ActivitySelectorConfig>
    {
        new ActivitySelectorConfig
        {
            Type = "follow_suggestion",
            MinPopularity = 10,
            CutoffWindow = "10d",
            Params = new Dictionary<string, object>
            {
                { "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:

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:

nametypedescriptionsupported operationsexample
idstring or list of stringsThe ID of the activity$in, $eq{ id: { $in: [ 'abc', 'xyz' ] } }
filter_tagslist of stringsTags 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:

nametypedescriptionsupported operationsexample
idstring or list of stringsThe ID of the activity$in, $eq{ id: { $in: [ 'abc', 'xyz' ] } }
activity_typestring or list of stringsThe type of the activity$in, $eq{ activity_type: { $in: [ 'abc', 'xyz' ] } }
user_idstring or list of stringsThe ID of the user who created the activity$in, $eq{ user_id: { $in: [ 'abc', 'xyz' ] } }
textstringThe text content of the activity$eq, $q, $autocomplete{ text: { $q: 'popularity' } }
search_dataobjectThe extra metadata for search indexing$contains, $path_exists{ search_data: { $contains: { 'category': 'sports', 'status': 'active' } } }
interest_tagslist of stringsTags for user interests$eq, $contains{ interest_tags: { $in: [ 'sports', 'music' ] } }
filter_tagslist of stringsTags for filtering$eq, $contains, $in{ filter_tags: { $in: [ 'categoryA', 'categoryB' ] } }
created_atstring, must be formatted as an RFC3339 timestampThe time the activity was created$eq, $gt, $lt, $gte, $lte{ created_at: { $gte: '2023-12-04T09:30:20.45Z' } }
popularitynumberThe popularity score of the activity$eq, $ne, $gt, $lt, $gte, $lte{ popularity: { $gte: 70 } }
nearobjectGEO point and a distance (in km) to search for activities within$eq{ near: { $eq: { lat: 40.0, lng: -74.0, distance: 200 } } }
within_boundsobjectGEO 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"
var filter = = new
{
  and = new[]
  {
    new { filter_tags = new[] { "green" } },
    new { filter_tags = new[] { "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.

NameTypeDescription
includelist of stringsSelect only activities that live in a feed belonging to one of these feed groups
excludelist of stringsSelect 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.

var updateResponse = await _feedsV3Client.UpdateFeedGroupAsync("myid", new UpdateFeedGroupRequest
{
    ActivitySelectors = new List<ActivitySelectorConfig>
    {
        new()
        {
            Type = "popular",
            FeedGroups = new FeedGroupScope { Exclude = new List<string> { "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:

DirectionRuleEffect
includeSelected when at least one of the activity's feeds is in an included groupNarrow. A new feed group is invisible until you add it
excludeDropped only when every one of the activity's feeds is in an excluded groupBroad. 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_suggestionResult
include contains userAccepted, no effect
include without userRejected with a 400
exclude contains userRejected with a 400
exclude without userAccepted, 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.

var createResponse = await _feedsV3Client.CreateFeedGroupAsync(new CreateFeedGroupRequest
{
    ID = feedGroupId,
    DefaultVisibility = "public",
    ActivityProcessors = new List<ActivityProcessorConfig>
    {
        new() { Type = "default" }
    }
});

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.

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