# 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).
>

```csharp label="C#"
// Follow a user
var response = await _feedsV3Client.FollowAsync(
    new FollowRequest
    {
        Source = $"user:{_testFeedId}",
        Target = $"user:{_testFeedId2}"
    }
);
```

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.

```csharp label="C#"
await _feedsV3Client.FollowAsync(
    new FollowRequest
    {
        Source = "timeline:alice",
        Target = "user:bob",
        CreateUsers = true
    }
);
```

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

```csharp label="C#"
await _feedsV3Client.GetOrCreateFollowsAsync(
    new GetOrCreateFollowsRequest
    {
        CreateUsers = true,
        Follows = new List<FollowInput>
        {
            new FollowInput { Source = "timeline:alice", Target = "user:bob" },
            new FollowInput { Source = "timeline:alice", Target = "user:charlie" }
        }
    }
);
```

## 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.

```csharp label="C#"
var unfollowResponse = await _feedsV3Client.UnfollowAsync(
    $"user:{_testFeedId}",
    $"user:{_testFeedId3}",
    new { user_id = _testUserId }
);
```

## 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).

```csharp label="C#"
await _feedsV3Client.UpdateFollowAsync(new UpdateFollowRequest
{
    Source = $"timeline:{sourceFeedId}",
    Target = $"user:{targetFeedId}",
    PushPreference = "none",
    FollowerRole = "feed_member_viewer",
    Custom = new Dictionary<string, object> { ["note"] = "Updated follow" },
});
```

## Querying Follows

```csharp label="C#"
// Query follows
var response = await _feedsV3Client.QueryFollowsAsync(
    new QueryFollowsRequest
    {
        Limit = 10
    }
);
```

### 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.

```csharp label="C#"
// Sara needs to configure the feed with visibility = followers for enabling follow requests
await _feedsV3Client.GetOrCreateFeedAsync(
    FeedGroupID: "user",
    FeedID: "sara",
    request: new GetOrCreateFeedRequest
    {
        UserID = "sara",
        Data = new FeedInput { Visibility = "followers" }
    }
);

// Adam requesting to follow the feed
await _feedsV3Client.GetOrCreateFeedAsync(
    FeedGroupID: "timeline",
    FeedID: "adam",
    request: new GetOrCreateFeedRequest { UserID = "adam" }
);

// Create follow request from adamTimeline to saraFeed
var followResponse = await _feedsV3Client.FollowAsync(
    new FollowRequest
    {
        Source = "timeline:adam",
        Target = "user:sara"
    }
);
Console.WriteLine(followResponse.Follow.Status); // pending

// Sara accepting
await _feedsV3Client.AcceptFollowAsync(
    new AcceptFollowRequest
    {
        Source = "timeline:adam",
        Target = "user:sara",
        FollowerRole = "feed_member_viewer" // optional
    }
);

// or rejecting the request
await _feedsV3Client.RejectFollowAsync(
    new RejectFollowRequest
    {
        Source = "timeline:adam",
        Target = "user:sara"
    }
);
```

## 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.

```csharp label="C#"
// Get follow suggestions for a user
var response = await _feedsV3Client.GetFollowSuggestionsAsync(
    new GetFollowSuggestionsRequest
    {
        FeedGroupId = "user",
        Limit = 10,
        UserId = "john"
    }
);

Console.WriteLine($"Algorithm used: {response.AlgorithmUsed}");
Console.WriteLine($"Duration: {response.Duration}");

foreach (var suggestion in response.Suggestions)
{
    Console.WriteLine($"Suggested feed: {suggestion.Fid}");
    Console.WriteLine($"Name: {suggestion.Name}");
    Console.WriteLine($"Description: {suggestion.Description}");
    Console.WriteLine($"Follower count: {suggestion.FollowerCount}");
    Console.WriteLine($"Recommendation score: {suggestion.RecommendationScore}");
    Console.WriteLine($"Reason: {suggestion.Reason}");
    Console.WriteLine($"Algorithm scores: {JsonSerializer.Serialize(suggestion.AlgorithmScores)}");
}
```

### 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.

```csharp label="C#"
// Batch create follows
var response = await _feedsV3Client.GetOrCreateFollowsAsync(
    new GetOrCreateFollowsRequest
    {
        Follows = new List<FollowInput>
        {
            new FollowInput
            {
                Source = "timeline:john",
                Target = "user:tom",
                // Optional
                PushPreference = "all",
                Custom = new Dictionary<string, object>
                {
                    { "reason", "investment" }
                }
            },
            new FollowInput
            {
                Source = "timeline:john",
                Target = "stock:apple"
            }
        }
    }
);

Console.WriteLine($"Created follows: {response.Created.Count}");
Console.WriteLine($"Total follows: {response.Follows.Count}");

// Batch remove follows
var unfollowResponse = await _feedsV3Client.GetOrCreateUnfollowsAsync(
    new GetOrCreateUnfollowsRequest
    {
        Follows = new List<FollowInput>
        {
            new FollowInput
            {
                Source = "timeline:john",
                Target = "user:tom"
            }
        }
    }
);

Console.WriteLine($"Follows that were removed: {unfollowResponse.Follows.Count}");
```

---

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