# Follow and Unfollow

## Follow

The source feed should have a group that has "following" activity selector enabled, for example the built-in `timeline` group. The target feed should have a group that has "current" activity selector enabled, for example the built-in `user` group.

>
> **Tip:** For most use cases, prefer [`getOrCreateFollow`](#idempotent-follow--unfollow) over `follow`. It returns the existing follow row when the pair is already followed instead of erroring, so retries and double-clicks are safe. Use `follow` only when you specifically want a duplicate-follow to fail.
>

>
> **Info:** This section covers the live `follow` API call, which derives the creator of the source and target feeds automatically. If you're bulk importing follows with the CLI importer instead, `source_created_by_id` and `target_created_by_id` are required fields — see [Importing Follows](https://getstream.io/activity-feeds/docs/node/importing-data-feeds/#follows).
>

```kotlin label="Kotlin"
// Follow a user
val timeline = client.feed(group = "timeline", id = "john")
timeline.follow(FeedId(group = "user", id = "tom"))

// Follow a stock
timeline.follow(FeedId(group = "stock", id = "apple"))

// Follow with more fields
timeline.follow(
    targetFid = FeedId(group = "stock", id = "apple"),
    custom = mapOf("reason" to "investment")
)
```

```java label="Java"
// Follow a user
FollowRequest followRequest =
    FollowRequest.builder()
        .source(USER_FEED_TYPE + testUserId)
        .target(USER_FEED_TYPE + testUserId2)
        .build();

SingleFollowResponse response = feeds.follow(followRequest).execute().getData();
```

Trying to follow a feed that is already followed will result in an error. Use [`getOrCreateFollow`](#idempotent-follow--unfollow) for an idempotent single-follow call, or [`getOrCreateFollows`](#idempotent-follow--unfollow) for the batch variant.

>
> **Info:** You do not need to call `getOrCreate` on the source or target feed before following. If either feed does not exist yet, the feed is created automatically when you call follow. This applies to both the single follow and batch follow endpoints.
>

### Auto-creating users

`follow`, `followBatch`, and `getOrCreateFollows` support an opt-in `create_users` flag (default: `false`).

- Server-side only: client-side callers that set `create_users` are ignored and still receive `user not found` if referenced users are missing.
- Missing users only: `create_users` creates users that do not exist yet; it does not update existing users.
- Best-effort behavior: missing-user creation happens before follow creation and is not transactional with the follow write. A user can be created even if a later follow validation fails.
- User ID inference: when `create_users` is `true`, user IDs are derived from the feed IDs in `source` and `target` (for example, `timeline:alice` infers `alice`, `user:bob` infers `bob`).

For a single `follow` call, set `create_users` on the request body.

```java label="Java"
FollowRequest request = FollowRequest.builder()
    .source("timeline:alice")
    .target("user:bob")
    .createUsers(true)
    .build();

feeds.follow(request).execute();
```

For `followBatch` / `getOrCreateFollows`, set `create_users` only at the top level. `follows[i].create_users` is rejected.

```java label="Java"
GetOrCreateFollowsRequest followsRequest = GetOrCreateFollowsRequest.builder()
    .createUsers(true)
    .follows(Arrays.asList(
        FollowInput.builder().source("timeline:alice").target("user:bob").build(),
        FollowInput.builder().source("timeline:alice").target("user:charlie").build()
    ))
    .build();

feeds.getOrCreateFollows(followsRequest).execute();
```

## Unfollow

When unfollowing a feed, all previous activities of that feed are removed from the timeline.

>
> **Tip:** For most use cases, prefer [`getOrCreateUnfollow`](#idempotent-follow--unfollow) over `unfollow`. It silently no-ops when the follow does not exist instead of erroring, so retries and double-clicks are safe. Use `unfollow` only when you specifically want unfollowing a non-existent follow to fail.
>

Trying to unfollow a feed that is not followed, will result in an error. You can also use the [`getOrCreateUnfollow`](#idempotent-follow--unfollow) endpoint for an idempotent single-unfollow call, or [`getOrCreateUnfollows`](#idempotent-follow--unfollow) for the batch variant.

```kotlin label="Kotlin"
val timeline = client.feed(group = "timeline", id = "john")
timeline.unfollow(FeedId(group = "user", id = "tom"))
```

```java label="Java"
UnfollowRequest unfollowRequest = UnfollowRequest.builder().build();
UnfollowResponse unfollowResponse =
    feeds
        .unfollow(USER_FEED_TYPE + testUserId, USER_FEED_TYPE + testUserId2, unfollowRequest)
        .execute()
        .getData();
```

## Update follow

You can update an existing follow relationship (for example to change push preference, custom data, or on server-side the follower role):

The endpoint performs a partial update: only the fields you include in the request are changed, and each of those fields is completely overwritten.

`follower_role` is **server-side only**: a client-side request that includes it is rejected. It can be either built-in follower role, `feed_follower` (the default) or `feed_member_viewer`, or any custom role you created for your app. It selects the permissions the follower is evaluated against: on `visible` and `followers` feeds, `feed_member_viewer` keeps reading, reactions and votes but drops commenting and querying a feed's follows and members.

A feed group can also set `default_follower_role`, which gives every new follow of a feed in that group its role automatically, so you do not need an `updateFollow` call per follow. An explicit `follower_role` here still wins over the group default. See [Default follower role](https://getstream.io/activity-feeds/docs/node/feed-groups/#default-follower-role).

On every level except `public`, where anyone can post regardless, neither **built-in** role can post activities into a feed the follower does not own. To give followers contributor access on those levels, either add them as members or assign a custom role that you granted `add-activities` at that visibility level. See [Custom follower roles](https://getstream.io/activity-feeds/docs/node/feed-and-activity-visibility/#custom-follower-roles). Note that `feed_member_viewer` is not uniformly more restrictive: on `members` and `private` feeds it _grants_ read access that `feed_follower` does not have, which is how you admit a read-only audience to a restricted feed. See [Follower permission tiers](https://getstream.io/activity-feeds/docs/node/feed-and-activity-visibility/#follower-permission-tiers).

```kotlin label="Kotlin"
val timeline = client.feed(group = "timeline", id = sourceFeedId)
timeline.updateFollow(
    targetFid = FeedId(group = "user", id = targetFeedId),
    pushPreference = UpdateFollowRequest.PushPreference.None,
    custom = mapOf("note" to "Updated follow")
)
```

```java label="Java"
UpdateFollowRequest request = UpdateFollowRequest.builder()
    .source("timeline:" + sourceFeedId)
    .target("user:" + targetFeedId)
    .pushPreference("none")
    .followerRole("feed_member_viewer")
    .custom(Map.of("note", "Updated follow"))
    .build();

feeds.updateFollow(request).execute();
```

## Querying Follows

```kotlin label="Kotlin"
// Do I follow a list of feeds
// My feed is timeline:john
val followQuery = FollowsQuery(
    filter = Filters.and(
        FollowsFilterField.sourceFeed.equal("timeline:john"),
        FollowsFilterField.targetFeed.`in`("user:sara", "user:adam")
    )
)
val followList = client.followList(query = followQuery)
val page1: Result<List<FollowData>> = followList.get()
val page2: Result<List<FollowData>> = followList.queryMoreFollows()
val page1And2 = followList.state.follows

// Paginating through followers for a feed
// My feed is timeline:john
val followerQuery = FollowsQuery(
    filter = FollowsFilterField.targetFeed.equal("timeline:john")
)
val followerList = client.followList(query = followerQuery)
val followerPage1: Result<List<FollowData>> = followerList.get()
```

```java label="Java"
// Query follows
QueryFollowsRequest request = QueryFollowsRequest.builder().limit(10).build();
QueryFollowsResponse response = feeds.queryFollows(request).execute().getData();
```

### Follows Queryable Built-In Fields

| name          | type                                              | description                                  | supported operations                | example                                                       |
| ------------- | ------------------------------------------------- | -------------------------------------------- | ----------------------------------- | ------------------------------------------------------------- |
| `source_feed` | string or list of strings                         | The feed ID that is following                | `$in`, `$eq`                        | `{ source_feed: { $eq: 'messaging:general' } }`               |
| `target_feed` | string or list of strings                         | The feed ID being followed                   | `$in`, `$eq`                        | `{ target_feed: { $in: [ 'sports:news', 'tech:updates' ] } }` |
| `status`      | string or list of strings                         | The follow status                            | `$in`, `$eq`                        | `{ status: { $in: [ 'accepted', 'pending', 'rejected' ] } }`  |
| `created_at`  | string, must be formatted as an RFC3339 timestamp | The time the follow relationship was created | `$eq`, `$gt`, `$gte`, `$lt`, `$lte` | `{ created_at: { $gte: '2023-12-04T09:30:20.45Z' } }`         |

## Follow Requests

Some apps require the user's approval for following them.

Accepting a request can also set the follower's role, which decides the permissions that follower is evaluated against. That field is **server-side only**: a client-side `acceptFollow` that includes it is rejected. It can be either built-in follower role, `feed_follower` (the default) or `feed_member_viewer`, or a [custom role](https://getstream.io/activity-feeds/docs/node/feed-and-activity-visibility/#custom-follower-roles) you created for your app. Follow requests only arise on `followers`-visibility feeds, and neither built-in can post there, so approving a request with one of those grants read access; it does not let the follower post to the feed.

```kotlin label="Kotlin"
// Sara needs to configure the feed with visibility = followers for enabling follow requests
val saraFeed = saraClient.feed(
    query = FeedQuery(
        group = "user",
        id = "sara",
        data = FeedInputData(visibility = FeedVisibility.Followers)
    )
)
saraFeed.getOrCreate()

// Adam requesting to follow the feed
val adamTimeline = adamClient.feed(group = "timeline", id = "adam")
adamTimeline.getOrCreate()

val followRequest = adamTimeline.follow(saraFeed.fid) // user:sara
println(followRequest.getOrNull()?.status) // FollowStatus.Pending

// Sara accepting
saraFeed.acceptFollow(
    sourceFid = adamTimeline.fid, // timeline:adam
    role = "feed_member" // optional
)
// or rejecting the request
saraFeed.rejectFollow(adamTimeline.fid) // timeline:adam

```

```java label="Java"
// Sara needs to configure the feed with visibility = followers for enabling follow requests
GetOrCreateFeedRequest saraFeedRequest = GetOrCreateFeedRequest.builder()
    .userID("sara")
    .data(FeedInput.builder()
        .visibility("followers")
        .build())
    .build();
feeds.getOrCreateFeed("user", "sara", saraFeedRequest).execute();

// Adam requesting to follow the feed
GetOrCreateFeedRequest adamFeedRequest = GetOrCreateFeedRequest.builder()
    .userID("adam")
    .build();
feeds.getOrCreateFeed("timeline", "adam", adamFeedRequest).execute();

// Create follow request from adamTimeline to saraFeed
FollowRequest followRequest = FollowRequest.builder()
    .source("timeline:adam")
    .target("user:sara")
    .build();
SingleFollowResponse followResponse = feeds.follow(followRequest).execute().getData();
System.out.println(followResponse.getFollow().getStatus()); // pending

// Sara accepting
AcceptFollowRequest acceptRequest = AcceptFollowRequest.builder()
    .source("timeline:adam")
    .target("user:sara")
    .followerRole("feed_member_viewer") // optional
    .build();
feeds.acceptFollow(acceptRequest).execute();

// or rejecting the request
RejectFollowRequest rejectRequest = RejectFollowRequest.builder()
    .source("timeline:adam")
    .target("user:sara")
    .build();
feeds.rejectFollow(rejectRequest).execute();
```

## Push Preferences on Follow

Understanding the difference between `push_preference`, `skip_push` and `create_notification_activity`:

When following a feed, you can set `push_preference` to control push notifications for future activities from that feed:

- **`all`** - Receive push notifications for all activities from the followed feed
- **`none`** (default) - Don't receive push notifications for activities from the followed feed

The `skip_push` controls whether the follow action itself triggers a notification.

The `create_notification_activity` controls whether the follow action creates an activity on the source feed author's notification feed.

**Note**: You usually don't want to set `skip_push` and `create_notification_activity` true at the same time, for more information [see the Push Overview page](https://getstream.io/docs/platform/push-notifications/#direct-push-notifications)

```js
// Scenario 1: Follow a user and receive notifications for their future activities
await timeline.follow("user:alice", {
  push_preference: "all", // You'll get push notifications for Alice's future posts
});

// Scenario 2: Follow a user but don't get notifications for their activities
await timeline.follow("user:bob", {
  push_preference: "none", // You won't get push notifications for Bob's future posts
});

// Scenario 3: Follow a user silently
await timeline.follow("user:charlie", {
  skip_push: true, // Charlie won't get a "you have a new follower" notification
  push_preference: "all", // But you'll still get notifications for Charlie's future posts
});

// Scenario 4: Silent follow with no future notifications
await timeline.follow("user:diana", {
  skip_push: true, // Diana won't know you followed her
  push_preference: "none", // And you won't get notifications for her posts
});

// Scenario 5: Follow a user and create notification activity for Charile
await timeline.follow("user:charlie", {
  skip_push: true, // Charlie won't get a "you have a new follower" notification
  create_notification_activity: true, // Charlie's notification feed will have a new activity
  push_preference: "all", // But you'll still get notifications for Charlie's future posts
});
```

## Built-in fields of follows

### FollowResponse

| Name | Type | Description | Constraints |
| --- | --- | --- | --- |
| `created_at` | `number` | When the follow relationship was created | Required |
| `custom` | `object` | Custom data for the follow relationship | - |
| `follower_role` | `string` | Role of the follower (source user) in the follow relationship, as stored. A reserved name, or a role your app no longer defines, is reported as stored but evaluated as 'feed_follower'. | Required |
| `push_preference` | `string (all, none)` | Push preference for notifications. One of: all, none | Required |
| `request_accepted_at` | `number` | When the follow request was accepted | - |
| `request_rejected_at` | `number` | When the follow request was rejected | - |
| `source_feed` | `FeedResponse` | Source feed object | Required |
| `status` | `string (accepted, pending, rejected)` | Status of the follow relationship. One of: accepted, pending, rejected | Required |
| `target_feed` | `FeedResponse` | Target feed object | Required |
| `updated_at` | `number` | When the follow relationship was last updated | Required |

## Follow Suggestions

Stream provides intelligent follow suggestions to help users discover feeds they might want to follow based on their activity and social graph.

**Note:** The maximum limit for follow suggestions is 50. If a higher limit is requested, it will be automatically capped at 50.

```kotlin label="Kotlin"
// Get follow suggestions for a user
val feed = client.feed(group = "user", id = "john")
val suggestions = feed.queryFollowSuggestions(limit = 10)

suggestions.getOrThrow().forEach { suggestion ->
    println("Suggested feed: ${suggestion.feed.fid}")
    println("Recommendation score: ${suggestion.recommendationScore}")
    println("Reason: ${suggestion.reason}")
}
```

```java label="Java"
// Get follow suggestions for a user
GetFollowSuggestionsRequest request = GetFollowSuggestionsRequest.builder()
    .feedGroupId("user")
    .limit(10)
    .userId("john")
    .build();

GetFollowSuggestionsResponse response = feeds.getFollowSuggestions(request).execute().getData();

System.out.println("Algorithm used: " + response.getAlgorithmUsed());
System.out.println("Duration: " + response.getDuration());

for (FollowSuggestion suggestion : response.getSuggestions()) {
    System.out.println("Suggested feed: " + suggestion.getFid());
    System.out.println("Name: " + suggestion.getName());
    System.out.println("Description: " + suggestion.getDescription());
    System.out.println("Follower count: " + suggestion.getFollowerCount());
    System.out.println("Recommendation score: " + suggestion.getRecommendationScore());
    System.out.println("Reason: " + suggestion.getReason());
    System.out.println("Algorithm scores: " + suggestion.getAlgorithmScores());
}
```

### Response Fields

The follow suggestions response includes:

- **`suggestions`**: Array of suggested feeds to follow
  - `feed`: Feed identifier
  - `name`: Feed name
  - `description`: Feed description
  - `visibility`: Feed visibility setting
  - `member_count`: Number of accepted members (pending and declined invitations are not counted)
  - `follower_count`: Number of followers
  - `following_count`: Number of feeds this feed follows
  - `created_at`: When the feed was created
  - `updated_at`: When the feed was last updated
  - `recommendation_score`: Combined recommendation score (0-1)
  - `reason`: Human-readable reason for the suggestion
  - `algorithm_scores`: Individual algorithm scores
- **`algorithm_used`**: The algorithm used to generate suggestions
- **`duration`**: Request processing time

### Algorithm Types

Stream's follow suggestions use a sophisticated multi-algorithm approach with weighted scoring:

- **`popularity`** (Weight: 0.3): Based on follower count and engagement
  - Calculates normalized follower count relative to the most popular feed in your app
  - Score = min(follower_count / max_follower_count, 1.0)
  - Helps surface trending and popular content

- **`friend-of-friend`** (Weight: 0.7): Based on social connections and mutual follows
  - Analyzes how many of your followed feeds also follow the suggested feed
  - Score = mutual_follows / your_total_follows
  - Leverages social proof and network effects

- **`combined`**: Uses multiple algorithms with weighted scoring
  - Final score = (popularity_score × 0.3 + friend_of_friend_score × 0.7) / total_weight
  - Provides balanced recommendations combining popularity and social relevance

**Note:** Additional algorithms will be added in future releases to provide even more sophisticated recommendations.

### Scoring System

The recommendation system uses a sophisticated scoring mechanism:

1. **Individual Algorithm Scores**: Each algorithm calculates a score from 0.0 to 1.0
2. **Weighted Combination**: Scores are combined using configurable weights
3. **Normalization**: Final scores are normalized to ensure fair comparison
4. **Filtering**: Only feeds with positive combined scores are included
5. **Ranking**: Results are sorted by combined score in descending order

### Features

- Excludes feeds already followed by the user
- Excludes user's own feeds
- Sophisticated algorithm to find feeds to follow

## Idempotent follow & unfollow

`getOrCreateFollow` / `getOrCreateUnfollow` are the idempotent single-pair variants of `follow` / `unfollow`. Calling them on a pair that is already followed (or already not followed) does not error — the response's `created` / `deleted` boolean tells you whether this call actually changed state.

`getOrCreateFollows` / `getOrCreateUnfollows` are the batch variants, accepting up to 100 pairs per call. Their response includes a `created` list with the subset of follows newly inserted by this call.

```js label="JavaScript — single"
// Idempotent follow: returns the existing follow if one exists, otherwise creates it.
const { follow, created } = await client.feeds.getOrCreateFollow({
  source: "timeline:alice",
  target: "user:bob",
});
console.log(created ? "newly created" : "already existed", follow);

// Idempotent unfollow: no error if the follow does not exist.
const { follow: removed, deleted } = await client.feeds.getOrCreateUnfollow({
  source: "timeline:alice",
  target: "user:bob",
});
console.log(deleted ? "removed" : "was not following", removed);
```

## Batch follow & unfollow

`getOrCreateFollows`/`getOrCreateUnfollows` endpoints allow creating a maximum of 100 follow/unfollow at once.

These are idempotent endpoints (as opposed to `follow` and `unfollow`), trying to follow/unfollow a feed that's already/not yet followed won't cause errors.

```kotlin label="Kotlin"
// Batch follow
val requests = listOf(
  FollowRequest(
    source = timeline.fid.rawValue,
    target = fid1.rawValue,
  ),
  FollowRequest(
    source = timeline.fid.rawValue,
    target = fid2.rawValue,
  ),
)
client.getOrCreateFollows(FollowBatchRequest(requests))
  .onSuccess { batchData ->
    println("Created follows: ${batchData.created}")
    println("Follows: ${batchData.follows}")
  }

// Batch unfollow
val unfollowPairs = listOf(
  UnfollowPair(
    source = timeline.fid.rawValue,
    target = fid1.rawValue,
  ),
  UnfollowPair(
    source = timeline.fid.rawValue,
    target = fid2.rawValue,
  ),
)
client.getOrCreateUnfollows(UnfollowBatchRequest(unfollowPairs))
  .onSuccess { follows ->
    println("Follows that were removed: $follows")
  }
```

```java label="Java"
// Batch create follows
GetOrCreateFollowsRequest followsRequest = GetOrCreateFollowsRequest.builder()
    .follows(Arrays.asList(
        FollowInput.builder()
            .source("timeline:john")
            .target("user:tom")
            // Optional
            .pushPreference("all")
            .custom(Map.of("reason", "investment"))
            .build(),
        FollowInput.builder()
            .source("timeline:john")
            .target("stock:apple")
            .build()
    ))
    .build();

GetOrCreateFollowsResponse response = feeds.getOrCreateFollows(followsRequest).execute().getData();

System.out.println("Created follows: " + response.getCreated().size());
System.out.println("Total follows: " + response.getFollows().size());

// Batch remove follows
GetOrCreateUnfollowsRequest unfollowsRequest = GetOrCreateUnfollowsRequest.builder()
    .follows(Arrays.asList(
        FollowInput.builder()
            .source("timeline:john")
            .target("user:tom")
            .build()
    ))
    .build();

GetOrCreateUnfollowsResponse unfollowResponse = feeds.getOrCreateUnfollows(unfollowsRequest).execute().getData();

System.out.println("Follows that were removed: " + unfollowResponse.getFollows().size());
```

---

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