Computed block
Some endpoints add a computed object to every item they return: a few values derived from the item's own unified fields by fixed arithmetic. Nothing is fetched, estimated or modelled. When an input is missing the value is null, never a made-up 0, so you can tell "unknown" from "zero".
{
"content": { "text": "If you're in line to vote for @bsky38.com, please stay in line! Polls close in just under 5 hours.", "createdAt": "2026-09-21T18:04:06Z", "language": "en" },
"author": { "username": "bsky.app", "displayName": "Bluesky", "...": "..." },
"engagement": { "likes": 768, "comments": 100, "shares": 96, "bookmarks": 40 },
"computed": { "engagement_rate": null, "language": "en" }
}This is bluesky.post_thread as recorded from the live API. The post has likes, comments and shares but neither a view count nor its author's follower count in its own record, so engagement_rate is null; the platform provides the language.
The fields#
| Field | Type | What it is |
|---|---|---|
engagement_rate | number or null | Interactions as a percentage of reach: (likes + comments + shares) / divisor × 100, rounded to 4 decimals. |
estimated_reach | number or null | The observed view count, engagement.views, when it is a number of 0 or more. Nothing more: unique reach is not modelled. The name matches what the field is commonly called elsewhere. |
language | string or null | A two-letter ISO 639-1 code: the one the platform provides in content.language, else a conservative guess from the item's text. |
An endpoint declares which of the three it adds; the block holds exactly those keys, present on every item even when every value is null.
Exact rules#
engagement_rate#
- The three counts are
engagement.likes,engagement.commentsandengagement.shares. Anullamong them counts as 0 as long as at least one is a number. When all three arenull, the rate isnull. - The divisor is
engagement.viewswhen it is a number greater than 0; elseauthor.followerswhen it is a number greater than 0; else there is no divisor and the rate isnull. rate = (likes + comments + shares) / divisor × 100, rounded to 4 decimals.- A result above 100 is clamped to
100; a negative one (a negative count) is clamped to0. Either clamp adds a sentence todata._warnings.
Most single posts have no follower count in their own record and no view count, so their rate is null by design, as in the example above. Profile-like endpoints do not declare engagement_rate at all. Where a platform reports views on a post, views are the divisor; where it does not but the record carries the author's followers, followers are.
estimated_reach#
engagement.views when it is a number greater than or equal to 0, else null. It is only declared on endpoints whose platform reports views.
language#
- If
content.languageis a string, its language part is normalised to two lower-case letters (en-USbecomesen) and used as is. - Otherwise a deterministic guess is made over
content.text, thencontent.title, thenauthor.bio, taking the first that gives a clear answer. Text shorter than 8 characters is skipped. If at least 80% of the letters are in one script that implies a language, that language is returned: Japanese (kana), Korean (Hangul), Chinese (Han only, at least 4 characters), Thai, Hebrew, Greek, Hindi (Devanagari) and Arabic. For Latin and Cyrillic text, function words are counted for English, Spanish, French, German, Italian, Portuguese, Dutch, Turkish, Indonesian, and Russian or Ukrainian; the winner needs at least 3 matching words and twice the runner-up's count. - Anything else, including short, mixed-script or ambiguous text, is
null. A null is preferred to a wrong code.
No network and no model are involved, so the guess is the same every time for the same text. In the recorded hackernews.search rows on the search page, the story titled "Why Discord is switching from Go to Rust" gets "language": "en" this way (Hacker News does not report a language; the title has three English function words), while a title with fewer than three gets null.
Warnings#
data._warnings is a list of strings that is present only when there is something to say, next to computed. Today it carries the clamps:
engagement_rate clamped to 100 (computed 300: interactions exceed the divisor), when a post gathers more interactions than its author has followers.engagement_rate clamped to 0 (computed -12.5: a count is negative).
The number in the sentence is the unclamped value. Treat the list as informational; its wording may change.
Which endpoints have it#
30 endpoints today (engagement_rate on 19, estimated_reach on 5, language on 30). This table is generated from the endpoint declarations; features.computed in GET /v1/platforms and x-computed in /openapi.json are the live list, and each endpoint page shows an Also works with line.
| Endpoint | Result | Computed fields |
|---|---|---|
bluesky.post_thread | item | engagement_rate, language |
bluesky.posts | list | engagement_rate, language |
bluesky.posts_by_uri | item | engagement_rate, language |
bluesky.profile | item | language |
devto.article | item | engagement_rate, language |
devto.article_by_path | item | engagement_rate, language |
devto.profile | item | language |
devto.search | list | engagement_rate, language |
github.issue | item | engagement_rate, language |
github.profile | item | language |
gitlab.user | item | language |
hackernews.item | item | engagement_rate, language |
hackernews.search | list | engagement_rate, language |
hackernews.user | item | language |
instagram.post | item | engagement_rate, estimated_reach, language |
instagram.profile | item | language |
lemmy.post | item | engagement_rate, language |
mastodon.status | item | engagement_rate, language |
reddit.post | item | engagement_rate, language |
reddit.search_posts | list | engagement_rate, language |
reddit.user_about | item | language |
stackexchange.question | item | engagement_rate, language |
tiktok.profile | item | language |
tiktok.video | item | engagement_rate, estimated_reach, language |
x.search | list | engagement_rate, language |
x.tweet | item | engagement_rate, language |
x.user | item | language |
youtube.channel | item | estimated_reach, language |
youtube.channel_by_handle | item | estimated_reach, language |
youtube.video_details | item | engagement_rate, estimated_reach, language |
On a list endpoint the block is on every element of data; on an item endpoint it is on the item. It is also present on these endpoints' rows in a batch, a multi-platform search and a URL lookup, since those run the same endpoints.
Edge cases#
- It is never cached and never stale. The block is computed when the response is served, from the data being served; a cached copy gets a fresh block over its (possibly stale) fields.
- It does not change the fields it reads.
engagementandcontent.languageare untouched;computedis added beside them. nullmeans unknown. A missing view count does not become a reach of 0, and an unreadable text does not become"en".- Rates can be above what you expect. With followers as the divisor, a post that went further than its author's audience is clamped to 100 and warned about, rather than reported as 300%.
What it costs#
Nothing. The block adds no credits to any endpoint, batch or search, and it does not affect caching.
Try it: Engagement rate calculator