# Ranking

Stream allows you to configure your own ranking method to determine the order of activities inside a feed.

<Admonition type="info">

Ranked feeds are not available for free plans. Contact support after [upgrading your account](https://getstream.io/activity-feeds/pricing/) to enable ranked feeds for your organization.

</Admonition>

An example ranking config is shown below:

```js label="Node.js"
const response = await serverClient.feeds.createFeedGroup({
  id: "mytimeline",
  ranking: { type: "expression", score: "decay_linear(time) * popularity" },
  activity_selectors: [
    {
      type: "following",
    },
  ],
});
```

You can also update built-in feed groups with your own ranking config:

```js label="Node.js"
await client.feeds.updateFeedGroup({
  id: "<id of feed group to update>",
  // Fields to update
});
```

Supported types for configuration:

| name         | description                                                                      |
| ------------ | -------------------------------------------------------------------------------- |
| `recency`    | Ranks activities based on their timestamp, with newer activities appearing first |
| `expression` | Uses a custom mathematical expression to calculate scores for ranking activities |
| `interest`   | `expression` ranking extended with interest weights                              |

Read on to learn the syntax for `expression` and `interest` based ranking.

For high-traffic feeds where every reader sees the same order, [hot feed cache](https://getstream.io/activity-feeds/docs/node/hot-feed-cache/) can serve a shared cached result. It supports recency and expression ranking (including external ranking) but not `interest` ranking or expressions that use per-user state such as `is_read` and `is_seen`.

The result of the ranking expression should be a number (called score). Activities in the feed will be ordered by score (highest score first).

Example of a simple ranking expression, order activities by popularity:

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity",
  },
});
```

<Admonition type="info">

Custom ranking is applied after [activity selectors](https://getstream.io/activity-feeds/docs/node/activity-selectors/) filtered activities. Each activity selector selects a maximum of 1000 activities. If you combine selectors, 1000 is multiplied by the number of selectors you have. You can use a maximum of 3 selectors in a group/view.

</Admonition>

## Ranking aggregated feed groups

If your feed also uses [aggregation](https://getstream.io/activity-feeds/docs/node/aggregation/), you can rank aggregated groups by score instead of the default `updated_at` ordering.

Set `aggregation.score_strategy` together with ranking:

```json
{
  "aggregation": {
    "format": "{{ type }}_{{ time.strftime('%Y-%m-%d') }}",
    "score_strategy": "max"
  },
  "ranking": {
    "type": "expression",
    "score": "popularity * 2 + comment_count"
  }
}
```

Supported `score_strategy` values:

| value | behavior                                       |
| ----- | ---------------------------------------------- |
| `sum` | Sum of all member activity scores in the group |
| `max` | Highest member activity score in the group     |
| `avg` | Average of member activity scores in the group |

<Admonition type="info">

`score_strategy` requires `aggregation.format`. If ranking or aggregation is not configured, groups fall back to the default aggregated ordering (`updated_at` descending for non-story feeds, `created_at` descending for stories).

</Admonition>

### Aggregated score sorting and pagination

When `score_strategy` is configured, aggregated groups are sorted by:

1. `score` descending
2. `updated_at` descending (tiebreaker, most recently active group first)

Pagination for scored aggregated groups uses ranked-feed style offset pagination (cursor includes offset, limit, and ranking version). If ranking config changes between page requests, the cursor expires and you should restart pagination from page 1.

### Aggregated score in responses

When score-based aggregation ranking is enabled, `aggregated_activities[].score` contains the computed group score:

```json
{
  "aggregated_activities": [
    {
      "group": "reaction_2026-03-30",
      "score": 42.5
    }
  ]
}
```

## Ranking expression syntax

### Activity data

It's not possible to access all fields of an `ActivityResponse` inside the ranking context. Below you'll find the list of supported fields:

| name               | description                                                                                                                                                                                                                                                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `time`             | UNIX timestamp of when the activity was created                                                                                                                                                                                                                                                                                                                      |
| `popularity`       | The popularity score of an activity (formula: `activity.popularity = reactions + comments * 2 + bookmarks * 3 + shares * 3`)                                                                                                                                                                                                                                         |
| `reaction_count`   | The sum of all reactions                                                                                                                                                                                                                                                                                                                                             |
| `reaction_counts`  | Reaction counts by type, you can access specific types with dot notation (for example, `reaction_counts.like`)                                                                                                                                                                                                                                                       |
| `comment_count`    | How many comments the activity has                                                                                                                                                                                                                                                                                                                                   |
| `bookmark_count`   | How many bookmarks the activity has                                                                                                                                                                                                                                                                                                                                  |
| `share_count`      | How many shares the activity has (Increased when an activity is set as the `parent_id` of another activity)                                                                                                                                                                                                                                                          |
| `custom`           | The `custom` field of the `activity`, nested fields can be accessed with `.` notation, for example: `custom.topic`                                                                                                                                                                                                                                                   |
| `selector_source`  | Which activity selector provided this activity (e.g., `"following"`, `"popular"`, `"interest"`). Only set when using multiple activity selectors. See [Ranking by Selector Source](#ranking-by-selector-source) for usage examples.                                                                                                                                  |
| `interest_score`   | Only available when using `interest` type ranking. A number between 0 and 1 reflecting how much a given activity matches the user's interests. See [Interest weights](#interest-weights) to see how a user's interests are computed/configured.                                                                                                                      |
| `preference_score` | Available when using `expression` or `interest` type ranking. A number reflecting how much a given activity matches the user's preferences based on activity feedback (show more/less). Activities with `interest_tags` that have strong dislike are filtered out. See [Activity feedback](https://getstream.io/activity-feeds/docs/node/activity-feedback/) for more information. |
| `is_read`          | Boolean. Available when `track_read` is enabled on a custom feed group or the built-in `notification` group (including flat). `true` if the activity (or its aggregated group) has been marked as read. Uses time-based invalidation. **Enterprise plan only.** See [Ranking by Read/Seen Status](#ranking-by-readseen-status).                                      |
| `is_seen`          | Boolean. Available when `track_seen` is enabled on a custom feed group or the built-in `notification` group (including flat). Same time-based invalidation as `is_read`. **Enterprise plan only.** See [Ranking by Read/Seen Status](#ranking-by-readseen-status).                                                                                                   |

It's possible to set default values if you expect that some data might be undefined for some activities. However it's not possible to set default values to built-in fields like `popularity` or `share_count`.

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity * (custom.isBoosted ? 1.5 : 1)",
    defaults: {
      custom: {
        isBoosted: false,
      },
    },
  },
});
```

### Data types

The following data types are supported in the ranking expression context:

| name    | example           |
| ------- | ----------------- |
| Boolean | `true`, `false`   |
| Integer | `36`, `0b101010`  |
| Float   | `0.2`             |
| String  | `"foo"`           |
| Array   | `["a", "b", "c"]` |
| Map     | `{foo: "bar"}`    |
| Nil     | `nil`             |

### Functions

The following functions can be used in ranking expressions:

| name                           | description                                                                                                                                                                                                                                                                                                     |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ln(x)                          | Natural logarithm function                                                                                                                                                                                                                                                                                      |
| log10(x)                       | Logarithm function base 10                                                                                                                                                                                                                                                                                      |
| sin(x)                         | Trigonometric sine function                                                                                                                                                                                                                                                                                     |
| cos(x)                         | Trigonometric cosine function                                                                                                                                                                                                                                                                                   |
| tan(x)                         | Trigonometric tangent function                                                                                                                                                                                                                                                                                  |
| asin(x)                        | Arcsine function                                                                                                                                                                                                                                                                                                |
| acos(x)                        | Arccosine function                                                                                                                                                                                                                                                                                              |
| atan(x)                        | Arctangent function                                                                                                                                                                                                                                                                                             |
| abs(x)                         | Absolute value                                                                                                                                                                                                                                                                                                  |
| min(a,b)                       | Minimum value                                                                                                                                                                                                                                                                                                   |
| max(a,b)                       | Maximum value                                                                                                                                                                                                                                                                                                   |
| trunc(x)                       | Truncates to the nearest integer value                                                                                                                                                                                                                                                                          |
| round(x)                       | Rounds to the nearest integer value                                                                                                                                                                                                                                                                             |
| decay_linear(t)                | Linear decay, see [Decay & Ranking](#decay--ranking) for more information                                                                                                                                                                                                                                       |
| decay_exp(t)                   | Exponential decay, see [Decay & Ranking](#decay--ranking) for more information                                                                                                                                                                                                                                  |
| decay_gauss(t)                 | Gaussian decay, see [Decay & Ranking](#decay--ranking) for more information                                                                                                                                                                                                                                     |
| rand_normal()                  | Returns a normally distributed number in the range [-inf, +inf] with standard normal distribution (stddev = 1, mean = 0)                                                                                                                                                                                        |
| rand_normal(a,b,σ,µ)           | Returns a normally distributed number in the range [a, b] with specific normal distribution (stddev = σ, mean = µ)                                                                                                                                                                                              |
| rand()                         | Returns a random number in the range [0, 1.0)                                                                                                                                                                                                                                                                   |
| to_unix_timestamp(t)           | Converts a time value to a UNIX timestamp                                                                                                                                                                                                                                                                       |
| dist(lat1,lng1,lat2,lng2,unit) | Returns the distance between the two points given by (lat1,lng1) and (lat2,lng2). By default the unit is in kilometers, but the unit can also be M for miles or N for nautical miles. This function can be combined with the external ranking parameters to rank activities based on a user's distance to them. |
| map_lookup(key, map)           | Look up value from a map by key, see [Arrays and Maps](#arrays-and-maps) for more information                                                                                                                                                                                                                   |
| sum_map_lookup(keys, map)      | Sum values from a map for multiple keys, see [Arrays and Maps](#arrays-and-maps) for more information                                                                                                                                                                                                           |
| in_array(needle, haystack)     | Check if a value exists in an array, see [Arrays and Maps](#arrays-and-maps) for more information                                                                                                                                                                                                               |

### Operators

The following operators can be used in ranking expressions:

- Arithmetic operators: `+`, `-`, `*`, `/`, `%` (modulus), `^`, `**` (exponent)

- Logical operators: `not`/`!`, `&&`/ `and`, `||`/`or`

- Conditional: `x ? y : z`, `??`, `if`/`else`

- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`

- Control order of evaluation with parentheses `()`

- Array/map operators: `[]` (index), `.`, `?.`, `in` (checks if an array/map has an item/key)

- String operators: `+` (concatenation), `contains`, `startsWith`, `endsWith`

This lets you construct a scoring algorithm like this:

```text
"score":"(a > 2 || (b > 4 && c > 3)) ? 1 : -1"
```

### Constants

The following constants and helpers are also available to use in ranking expressions:

| name           | description                        |
| -------------- | ---------------------------------- |
| `e` / `E`      | Euler's number                     |
| `pi` / `PI`    | π                                  |
| `current_time` | UNIX timestamp of the current time |

### Decay & Ranking

Stream supports linear, exponential, and Gaussian decay. For each decay function, we support 4 arguments: Origin, Scale, Offset, and Decay. You can pass either a timedelta string (such as 3d, 4w), or a numeric value.

![](https://getstream.imgix.net/images/docs/decay_explainer.png?auto=compress&fit=clip&w=800&h=600)

#### Parameters

| name      | description                                                                                                                                               | default |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| origin    | The best possible value. If the value is equal to the origin, the decay function will return 1.                                                           | now     |
| scale     | Determines how quickly the score drops from 1.0 to the decay value. If the scale is set to "3d", the score will be equal to the decay value after 3 days. | 5d      |
| offset    | Values below the offset will start to receive a lower score.                                                                                              | 0       |
| decay     | The score that a value at scale distance from the origin should receive.                                                                                  | 0.5     |
| direction | left, right or both. If right is specified, only apply the decay for the right part of the graph.                                                         | both    |

<Admonition type="info">

You can use s, m, h, d and w to specify seconds, minutes, hours, days and weeks respectively.

</Admonition>

The example below defines a simple_gauss with the following parameters:

- **scale**: 5 days

- **offset**: 1 day

- **decay**: 0.3

This means that an activity younger than 1 day (the offset) will return a score of 1. An activity that is exactly 6 days old (offset + scale) will get a score of 0.3 (the decay factor). The full example:

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    functions: {
      simple_gauss: {
        base: "decay_gauss",
        scale: "5d",
        offset: "1d",
        decay: "0.3",
      },
      popularity_gauss: {
        base: "decay_gauss",
        scale: "100",
        offset: "5",
        decay: "0.5",
      },
    },
    score: "simple_gauss(time)*popularity_gauss(popularity)",
  },
});
```

### Interest weights

Interest weights help customize the ranking to users' interests. If you want to create a ranking expression that relies on interest weights, use `interest` type ranking.

```js label="Node.js"
serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "interest",
    score: "interest_score * popularity",
  },
});
```

`interest_score` is computed by combining activity topics and the specific user's interests. The result is a number between 0-1.

#### Activity topics

The topic of an activity can be computed automatically using [activity processors](https://getstream.io/activity-feeds/docs/node/activity-processors/) or by [setting the `interest_tags` field](https://getstream.io/activity-feeds/docs/node/activities/#overview-of-all-activity-fields) via the Stream API.

#### User interests

A user's interests are computed automatically by the Stream API from the `interest_tags` on activities the user has reacted to. When ranking with `interest` type, the top five computed tags are used, each with a weight of `1.0`.

You can also pass `interest_weights` when reading the feed. These weights are **merged** with the computed interests rather than replacing them:

- Tags that appear only in the computed profile or only in `interest_weights` both contribute to `interest_score`.
- If the same tag appears in both, the value from `interest_weights` is used (an explicit override for that tag).
- If you omit `interest_weights` or pass an empty map, ranking uses only the computed interests.

<Tabs>

```js label="JavaScript"
await timeline.getOrCreate({
  interest_weights: {
    travel: 1,
    food: 0.5,
    exercise: -1,
  },
});
```

```js label="Node.js"
await timeline.getOrCreate({
  interest_weights: {
    travel: 1,
    food: 0.5,
    exercise: -1,
  },
  user_id: "<user id>",
});
```

</Tabs>

The range for interest weights is between -1 and 1. 1 means a user is very interested in a topic.

#### Preference score

`preference_score` reflects how much an activity matches the user's preferences based on activity feedback (show more/less). Activities with `interest_tags` that have strong dislike (based on user feedback) are filtered out from the feed. Other activities are ranked using `preference_score` which incorporates the user's activity feedback.

You can use `preference_score` in your ranking expressions with both `expression` and `interest` type ranking to boost activities that match user preferences:

```js label="Node.js"
// Using preference_score with expression type
serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "preference_score * popularity",
  },
});

// Using preference_score with interest type
serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "interest",
    score: "preference_score * interest_score * popularity",
  },
});
```

See [Activity feedback](https://getstream.io/activity-feeds/docs/node/activity-feedback/) for more information on how users can provide feedback that affects `preference_score`.

### Ranking by Selector Source

When using multiple [activity selectors](https://getstream.io/activity-feeds/docs/node/activity-selectors/), each activity is tagged with a `selector_source` field indicating which selector provided it. This enables you to prioritize activities from specific sources in your ranking expressions.

```js label="Node.js"
// Example 1: Simple boost - following activities get 2x multiplier
await serverClient.feeds.createFeedGroup({
  id: "timeline",
  activity_selectors: [
    { type: "following" },
    { type: "popular", min_popularity: 0 },
  ],
  ranking: {
    type: "expression",
    score: 'selector_source == "following" ? popularity * 2 : popularity',
    defaults: {
      selector_source: "", // Required when using selector_source in expressions
    },
  },
});

// Example 2: Multiple selectors with different bonuses
await serverClient.feeds.createFeedGroup({
  id: "timeline",
  activity_selectors: [
    { type: "following" },
    { type: "interest" },
    { type: "popular", min_popularity: 0 },
  ],
  ranking: {
    type: "expression",
    score: `
      (selector_source == "following" ? 10 : 0) + 
      (selector_source == "interest" ? 5 : 0) + 
      popularity
    `,
    defaults: {
      selector_source: "",
    },
  },
});
```

**Important notes:**

- **Defaults required:** You must provide `selector_source` in the `defaults` map (typically as an empty string `""`)
- **Duplicate activities:** If an activity matches multiple selectors, it keeps the `selector_source` of the **first selector** in your configuration
- **Valid sources:** `following`, `popular`, `interest`, `query`, `proximity`, `current_feed`, `follow_suggestion`

### Ranking by Read/Seen Status

When `track_read` or `track_seen` is enabled on a feed group, you can use `is_read` and `is_seen` as variables in ranking expressions. This works on **custom** feed groups — including flat timelines without aggregation — and the built-in `notification` feed, not only aggregated notification feeds. Aggregation and notification tracking are independent. See [Notification feeds](https://getstream.io/activity-feeds/docs/node/notification-feeds/#notification-configuration) for how to configure tracking.

<Admonition type="info">

Ranking by `is_read` / `is_seen` is only available on **Enterprise** plans. [Contact support](https://getstream.io/contact/) to enable this feature for your organization.

</Admonition>

For example, a custom flat feed group that ranks unread activities higher:

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "ranked_timeline",
  notification: {
    track_read: true,
    track_seen: true,
  },
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity - (is_read ? 100 : 0)",
  },
});
```

The same expressions work on the built-in `notification` feed or any aggregated feed with tracking enabled.

#### Example expressions

| Expression                                            | Effect                                   |
| ----------------------------------------------------- | ---------------------------------------- |
| `is_read ? 0 : 100`                                   | Unread activities ranked first           |
| `popularity - (is_read ? 10 : 0)`                     | Penalize read activities by 10 points    |
| `popularity - (is_read ? 10 : 0) - (is_seen ? 5 : 0)` | Penalize both read and seen activities   |
| `(is_seen ? 0 : 1000) + decay_linear(time, 48)`       | Unseen activities first, with time decay |

#### Aggregated feeds

For aggregated feeds, `is_read` and `is_seen` are resolved at the **group level**. All activities within an aggregated group share the same read/seen status. On **flat** feeds, they are resolved per activity.

<Admonition type="info">

Ranking by `is_read` / `is_seen` requires notification tracking (`track_read` / `track_seen`). If tracking is not enabled, these variables default to `false` in ranking expressions.

</Admonition>

### External data

External data lets you add dynamic data to your ranking expressions. External data is provided when reading the feed, so you can provide user-specific data here.

#### Accessing external data in ranking expressions

| name       | description                                                                                                                            |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `external` | The ranking configuration provided when reading the feed, nested fields can be accessed with `.` notation, for example: `external.foo` |

```js label="Node.js"
serverClient.feeds.createFeedGroup({
  id: "myid",
  ranking: {
    type: "expression",
    score:
      "popularity * external.popularity_multiplier + share_count * external.share_multiplier",
    defaults: {
      external: {
        // Provide default values if external data isn't provided when reading the feed
        popularity_multiplier: 1,
        share_multiplier: 1,
      },
    },
  },
});
```

#### Providing external data when reading the feed

<Tabs>

```js label="JavaScript"
await timeline.getOrCreate({
  external_ranking: {
    popularity_multiplier: 1, // normal weight for popularity
    share_multiplier: 100, // very high boost for share_count
  },
});
```

```js label="Node.js"
await timeline.getOrCreate({
  external_ranking: {
    popularity_multiplier: 1, // normal weight for popularity
    share_multiplier: 100, // very high boost for share_count
  },
  user_id: "<user id>",
});
```

</Tabs>

### Arrays and Maps

Stream provides three powerful functions for working with arrays and maps in ranking expressions. These functions are particularly useful for creating dynamic, personalized ranking algorithms that can respond to external data.

**map_lookup(key, map)**

Looks up a single value from a map by key. If the key doesn't exist, it returns `0.0`.

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity + map_lookup(stock, external.stock_boosts)",
    defaults: {
      stock: "",
      externals: {
        stock_boosts: {},
      },
    },
  },
});
```

Providing external data when reading the feed

<Tabs>

```js label="JavaScript"
await timeline.getOrCreate({
  external_ranking: {
    stock_boosts: {
      apple: 0.9,
      tesla: 0.5,
      microsoft: 0.3,
    },
  },
});
```

```js label="Node.js"
await timeline.getOrCreate({
  external_ranking: {
    stock_boosts: {
      apple: 0.9,
      tesla: 0.5,
      microsoft: 0.3,
    },
  },
  user_id: "<user id>",
});
```

</Tabs>

**sum_map_lookup(keys, map)**

Sums values from a map for multiple keys. This is useful when an activity has multiple tags or categories that should all contribute to the score.

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity + sum_map_lookup(stocks, external.stock_boosts)",
    defaults: {
      stocks: [],
      external: {
        stock_boosts: [],
      },
    },
  },
});
```

Providing external data when reading the feed

<Tabs>

```js label="JavaScript"
await timeline.getOrCreate({
  external_ranking: {
    stock_boosts: {
      apple: 10.0,
      tesla: 5.0,
      microsoft: 2.0,
    },
  },
});

// score = popularity + 10.0 + 5.0 = popularity + 15.0 (for activity with stocks: ['apple', 'tesla'])
```

```js label="Node.js"
await timeline.getOrCreate({
  external_ranking: {
    stock_boosts: {
      apple: 10.0,
      tesla: 5.0,
      microsoft: 2.0,
    },
  },
  user_id: "<user id>",
});

// score = popularity + 10.0 + 5.0 = popularity + 15.0 (for activity with stocks: ['apple', 'tesla'])
```

</Tabs>

**in_array(needle, haystack)**

Checks if a value exists in an array. Returns `1.0` if found, `0.0` if not found.

```js label="Node.js"
await serverClient.feeds.createFeedGroup({
  id: "myid",
  activity_selectors: [{ type: "following" }],
  ranking: {
    type: "expression",
    score: "popularity + in_array(stock, external.watchlist) * 10",
    defaults: {
      stock: "",
      externals: {
        watchlist: [],
      },
    },
  },
});
```

Providing external data when reading the feed

<Tabs>

```js label="JavaScript"
await timeline.getOrCreate({
  external_ranking: {
    watchlist: ["apple", "tesla", "microsoft"],
  },
});

// activities with stocks in the watchlist get a +10 boost, others get no boost.
```

```js label="Node.js"
await timeline.getOrCreate({
  external_ranking: {
    watchlist: ["apple", "tesla", "microsoft"],
  },
  user_id: "<user id>",
});

// activities with stocks in the watchlist get a +10 boost, others get no boost.
```

</Tabs>

#### Practical Examples

**Stock Trading Feed:**

```js
score: "popularity + sum_map_lookup(stocks, external.stock_boosts) + in_array(market_sector, external.favorite_sectors) * 5";
```

**Content Categorization:**

```js
score: "popularity + in_array(category, external.preferred_categories) * 3 + map_lookup(category, external.category_weights)";
```

**Multi-tag Content:**

```js
score: "popularity + sum_map_lookup(tags, external.tag_boosts) + in_array(priority, external.high_priority_tags) * 10";
```

## Inspecting ranking variables

When reading the feed, you can access the:

- score of each activity (this is the result of the ranking expression)
- score of each aggregated group (`aggregated_activities[].score`) when using `aggregation.score_strategy`
- reaction count of each activity (the sum of all reactions)
- standard fields like `popularity` can also be observed when reading the feed

These variables can help you understand the result of the ranking expression.

<Tabs>

```js label="JavaScript"
const feed = client.feed("timeline", "sara");
const response = await feed.getOrCreate();

console.log(response.activities[0].score);
console.log(response.activities[0].reaction_count);
console.log(response.activities[0].popularity);
```

```js label="Node.js"
const feed = client.feeds.feed("timeline", "sara");
const response = await feed.getOrCreate({ user_id: "sara" });

console.log(response.activities[0].score);
console.log(response.activities[0].reaction_count);
console.log(response.activities[0].popularity);
```

</Tabs>

Configuring ranking can be complex. Feel free to reach out to [support](https://getstream.io/contact/support/) if you have questions!

## Experimenting with ranking

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.

<Admonition type="info">

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

</Admonition>


---

This page was last updated at 2026-08-07T20:37:31.611Z.

For the most recent version of this documentation, visit [https://getstream.io/activity-feeds/docs/node/custom-ranking/](https://getstream.io/activity-feeds/docs/node/custom-ranking/).