Docs
View as Markdown

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

json
{
  "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#

FieldTypeWhat it is
engagement_ratenumber or nullInteractions as a percentage of reach: (likes + comments + shares) / divisor × 100, rounded to 4 decimals.
estimated_reachnumber or nullThe 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.
languagestring or nullA 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#

  1. The three counts are engagement.likes, engagement.comments and engagement.shares. A null among them counts as 0 as long as at least one is a number. When all three are null, the rate is null.
  2. The divisor is engagement.views when it is a number greater than 0; else author.followers when it is a number greater than 0; else there is no divisor and the rate is null.
  3. rate = (likes + comments + shares) / divisor × 100, rounded to 4 decimals.
  4. A result above 100 is clamped to 100; a negative one (a negative count) is clamped to 0. Either clamp adds a sentence to data._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#

  1. If content.language is a string, its language part is normalised to two lower-case letters (en-US becomes en) and used as is.
  2. Otherwise a deterministic guess is made over content.text, then content.title, then author.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.
  3. 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.

EndpointResultComputed fields
bluesky.post_threaditemengagement_rate, language
bluesky.postslistengagement_rate, language
bluesky.posts_by_uriitemengagement_rate, language
bluesky.profileitemlanguage
devto.articleitemengagement_rate, language
devto.article_by_pathitemengagement_rate, language
devto.profileitemlanguage
devto.searchlistengagement_rate, language
github.issueitemengagement_rate, language
github.profileitemlanguage
gitlab.useritemlanguage
hackernews.itemitemengagement_rate, language
hackernews.searchlistengagement_rate, language
hackernews.useritemlanguage
instagram.postitemengagement_rate, estimated_reach, language
instagram.profileitemlanguage
lemmy.postitemengagement_rate, language
mastodon.statusitemengagement_rate, language
reddit.postitemengagement_rate, language
reddit.search_postslistengagement_rate, language
reddit.user_aboutitemlanguage
stackexchange.questionitemengagement_rate, language
tiktok.profileitemlanguage
tiktok.videoitemengagement_rate, estimated_reach, language
x.searchlistengagement_rate, language
x.tweetitemengagement_rate, language
x.useritemlanguage
youtube.channelitemestimated_reach, language
youtube.channel_by_handleitemestimated_reach, language
youtube.video_detailsitemengagement_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. engagement and content.language are untouched; computed is added beside them.
  • null means 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