// Follow a user
$timeline = $feedsClient->feed('timeline', 'john');
$timeline->getOrCreateFeed(
new GeneratedModels\GetOrCreateFeedRequest(userID: 'john')
);
$response = $feedsClient->follow(
new GeneratedModels\FollowRequest(
source: 'timeline:john',
target: 'user:tom'
)
);
// Follow a stock
$response = $feedsClient->follow(
new GeneratedModels\FollowRequest(
source: 'timeline:john',
target: 'stock:apple'
)
);
// Follow with more fields
$response = $feedsClient->follow(
new GeneratedModels\FollowRequest(
source: 'timeline:john',
target: 'stock:apple',
pushPreference: 'all',
custom: (object)['reason' => 'investment']
)
);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.
For most use cases, prefer getOrCreateFollow 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.
Trying to follow a feed that is already followed will result in an error. Use getOrCreateFollow for an idempotent single-follow call, or getOrCreateFollows for the batch variant.
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_usersare ignored and still receiveuser not foundif referenced users are missing. - Missing users only:
create_userscreates 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_usersistrue, user IDs are derived from the feed IDs insourceandtarget(for example,timeline:aliceinfersalice,user:bobinfersbob).
For a single follow call, set create_users on the request body.
$response = $feedsClient->follow(
new GeneratedModels\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.
$response = $feedsClient->getOrCreateFollows(
new GeneratedModels\GetOrCreateFollowsRequest(
createUsers: true,
follows: [
new GeneratedModels\FollowInput(
source: 'timeline:alice',
target: 'user:bob'
),
new GeneratedModels\FollowInput(
source: 'timeline:alice',
target: 'user:charlie'
)
]
)
);Unfollow
When unfollowing a feed, all previous activities of that feed are removed from the timeline.
For most use cases, prefer getOrCreateUnfollow 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 endpoint for an idempotent single-unfollow call, or getOrCreateUnfollows for the batch variant.
$response = $feedsClient->unfollow('timeline:john', 'user:tom');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.
$response = $feedsClient->updateFollow(
new GeneratedModels\UpdateFollowRequest(
source: 'timeline:' . $sourceFeedId,
target: 'user:' . $targetFeedId,
pushPreference: 'none',
followerRole: 'my_custom_feed_follower_role',
custom: (object)['note' => 'Updated follow']
)
);Querying Follows
$myTimeline = $feedsClient->feed('timeline', 'john');
$myTimeline->getOrCreateFeed(
new GeneratedModels\GetOrCreateFeedRequest(userID: 'john')
);
// Do I follow a list of feeds
$response = $feedsClient->queryFollows(
new GeneratedModels\QueryFollowsRequest(
filter: (object)[
'source_feed' => 'timeline:john',
'target_feed' => (object)['$in' => ['user:sara', 'user:adam']]
]
)
);
echo json_encode($response->getData()->follows);
$userFeed = $feedsClient->feed('user', 'john');
$userFeed->getOrCreateFeed(
new GeneratedModels\GetOrCreateFeedRequest(userID: 'john')
);
// Paginating through followers for a feed
$firstPage = $feedsClient->queryFollows(
new GeneratedModels\QueryFollowsRequest(
filter: (object)['target_feed' => 'user:john'],
limit: 20
)
);
// Next page
$secondPage = $feedsClient->queryFollows(
new GeneratedModels\QueryFollowsRequest(
filter: (object)['target_feed' => 'user:john'],
limit: 20,
next: $firstPage->getData()->next
)
);
// Filter by source - feeds that I follow
$sourceFollows = $feedsClient->queryFollows(
new GeneratedModels\QueryFollowsRequest(
filter: (object)['source_feed' => 'timeline:john'],
limit: 20
)
);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.
// Sara needs to configure the feed with visibility = followers for enabling follow requests
$saraFeed = $feedsClient->feed('user', 'sara');
$saraFeed->getOrCreateFeed(
new GeneratedModels\GetOrCreateFeedRequest(
userID: 'sara',
data: new GeneratedModels\FeedInput(visibility: 'followers')
)
);
// Adam requesting to follow the feed
$adamTimeline = $feedsClient->feed('timeline', 'adam');
$adamTimeline->getOrCreateFeed(
new GeneratedModels\GetOrCreateFeedRequest(userID: 'adam')
);
$followRequest = $feedsClient->follow(
new GeneratedModels\FollowRequest(
source: 'timeline:adam',
target: 'user:sara'
)
);
echo $followRequest->getData()->follow->status; // pending
// Sara accepting
$acceptResponse = $feedsClient->acceptFollow(
new GeneratedModels\AcceptFollowRequest(
source: 'timeline:adam',
target: 'user:sara',
followerRole: 'feed_member' // optional
)
);
// or rejecting the request
$rejectResponse = $feedsClient->rejectFollow(
new GeneratedModels\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 feednone(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
// 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 | 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.
// Get follow suggestions for a user
$suggestions = $feedsClient->getFollowSuggestions(
new GeneratedModels\GetFollowSuggestionsRequest(
feedGroupId: 'user',
limit: 10,
userId: 'john'
)
);
echo "Algorithm used: " . $suggestions->getData()->algorithmUsed . "\n";
echo "Duration: " . $suggestions->getData()->duration . "\n";
foreach ($suggestions->getData()->suggestions as $suggestion) {
echo "Suggested feed: " . $suggestion->fid . "\n";
echo "Name: " . $suggestion->name . "\n";
echo "Description: " . $suggestion->description . "\n";
echo "Follower count: " . $suggestion->followerCount . "\n";
echo "Recommendation score: " . $suggestion->recommendationScore . "\n";
echo "Reason: " . $suggestion->reason . "\n";
echo "Algorithm scores: " . json_encode($suggestion->algorithmScores) . "\n";
}Response Fields
The follow suggestions response includes:
suggestions: Array of suggested feeds to followfeed: Feed identifiername: Feed namedescription: Feed descriptionvisibility: Feed visibility settingmember_count: Number of membersfollower_count: Number of followersfollowing_count: Number of feeds this feed followscreated_at: When the feed was createdupdated_at: When the feed was last updatedrecommendation_score: Combined recommendation score (0-1)reason: Human-readable reason for the suggestionalgorithm_scores: Individual algorithm scores
algorithm_used: The algorithm used to generate suggestionsduration: 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:
- Individual Algorithm Scores: Each algorithm calculates a score from 0.0 to 1.0
- Weighted Combination: Scores are combined using configurable weights
- Normalization: Final scores are normalized to ensure fair comparison
- Filtering: Only feeds with positive combined scores are included
- 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.
// 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.
// Batch create follows
$response = $feedsClient->getOrCreateFollows(
new GeneratedModels\GetOrCreateFollowsRequest(
follows: [
new GeneratedModels\FollowInput(
source: 'timeline:john',
target: 'user:tom',
// Optional
pushPreference: 'all',
custom: (object)['reason' => 'investment']
),
new GeneratedModels\FollowInput(
source: 'timeline:john',
target: 'stock:apple'
)
]
)
);
echo "Created follows: " . count($response->getData()->created) . "\n";
echo "Total follows: " . count($response->getData()->follows) . "\n";
// Batch remove follows
$unfollowResponse = $feedsClient->getOrCreateUnfollows(
new GeneratedModels\GetOrCreateUnfollowsRequest(
follows: [
new GeneratedModels\FollowInput(
source: 'timeline:john',
target: 'user:tom'
)
]
)
);
echo "Follows that were removed: " . count($unfollowResponse->getData()->follows) . "\n";