Custom Formatter
Complete reference for formatting stream names and descriptions using variables, modifiers, and conditionals.
The Custom Formatter gives you full control over how each stream's name and description are displayed in Stremio (and other clients). You can live-preview your format on the configuration page using the Preview button.
Access a variable using:
{variableName.propertyName}Modifiers are chained with :::
{variableName.propertyName::modifier}Variables
Config
| Variable | Type | Description |
|---|---|---|
{config.addonName} | string | The name of the AIOStreams instance (Branding → Addon name; ADDON_NAME) |
Stream
Source
| Variable | Type | Description |
|---|---|---|
{stream.type} | string | Type: debrid, usenet, http, live, youtube, p2p |
{stream.proxied} | boolean | Whether the stream is proxied (e.g. MediaFlow) |
{stream.library} | boolean | Whether the file is already in your "library" e.g. debrid account |
{stream.indexer} | string | Source indexer |
{stream.message} | string | Additional status message |
{stream.infoHash} | string | Torrent info hash |
File
| Variable | Type | Description |
|---|---|---|
{stream.filename} | string | Filename of the stream or media file |
{stream.folderName} | string | Folder name (usually only specific addons) |
{stream.size} | number | File size in bytes |
{stream.folderSize} | number | Folder/torrent size in bytes |
{stream.bitrate} | number | Bitrate in bits per second |
{stream.duration} | number | Media duration in seconds |
{stream.container} | string | File container format (e.g. mkv, mp4) |
{stream.extension} | string | File extension (e.g. .mkv, .iso) |
Video
| Variable | Type | Description |
|---|---|---|
{stream.quality} | string | Quality tag (e.g. Bluray, WEB-DL) |
{stream.resolution} | string | Video resolution (e.g. 1080p, 2160p) |
{stream.visualTags} | string[] | Visual tags (e.g. HDR, DV, HDR10+) |
{stream.encode} | string | Encoding format (e.g. HEVC, AVC, AV1) |
{stream.network} | string | Source network (e.g. Netflix, Disney+) |
{stream.hasChapters} | boolean | Whether the file contains chapters |
Audio
| Variable | Type | Description |
|---|---|---|
{stream.audioTags} | string[] | Audio tags (e.g. Atmos, DTS-HD MA, DD+) |
{stream.audioChannels} | string[] | Audio channels (e.g. 5.1, 7.1) |
Languages
| Variable | Type | Description |
|---|---|---|
{stream.languages} | string[] | Languages extracted from filename or audio track info |
{stream.languageEmojis} | string[] | languages as emoji flags |
{stream.languageCodes} | string[] | languages as ISO 639 codes |
{stream.smallLanguageCodes} | string[] | languages as small-caps codes |
{stream.uLanguages} | string[] | languages filtered to those you have configured (preferred, required, or included but not excluded) |
{stream.uLanguageEmojis} | string[] | uLanguages as emoji flags |
{stream.uLanguageCodes} | string[] | uLanguages as ISO 639 codes |
{stream.uSmallLanguageCodes} | string[] | uLanguages as small-caps codes |
{stream.dubbed} | boolean | Whether the release is dubbed |
Subtitles
| Variable | Type | Description |
|---|---|---|
{stream.subtitles} | string[] | Embedded subtitle languages (when known — see note below) |
{stream.subtitleEmojis} | string[] | subtitles as emoji flags |
{stream.subtitleCodes} | string[] | subtitles as ISO 639 codes |
{stream.smallSubtitleCodes} | string[] | subtitles as small-caps codes |
{stream.uSubtitles} | string[] | subtitles filtered to those you have configured (preferred, required, or included but not excluded) |
{stream.uSubtitleEmojis} | string[] | uSubtitles as emoji flags |
{stream.uSubtitleCodes} | string[] | uSubtitles as ISO 639 codes |
{stream.uSmallSubtitleCodes} | string[] | uSubtitles as small-caps codes |
{stream.subbed} | boolean | Whether the release has subtitles |
languages vs subtitles
languages and subtitles behave differently depending on what information is available for a stream.
When accurate media info is available, languages contains audio track languages and subtitles contains embedded subtitle languages. This is the case for:
- Debrid results from built-in and service-wrapped addons that have
media_infofrom StremThru (crowdsourced/probed via FFmpeg) - Torznab/Newznab results from indexers that provide separate audio/subtitle metadata
- nekoBT results (all results have accurate audio/subtitle info)
- Torrentio anime results (subtitles field may be populated)
In all other cases, subtitles is empty and languages contains every language found in the filename — including subtitle-only languages (e.g. a filename containing Eng.Sub will add English to languages, not subtitles). This is a known limitation of filename-only parsing.
Release
| Variable | Type | Description |
|---|---|---|
{stream.title} | string | Media title extracted from filename |
{stream.year} | string | Year extracted from filename |
{stream.date} | string | Date extracted from filename |
{stream.releaseGroup} | string | Name of the release group |
{stream.editions} | string[] | Special editions (e.g. Director's Cut) |
{stream.repack} | boolean | Whether the release is a repack |
{stream.regraded} | boolean | Whether the content is regraded |
{stream.uncensored} | boolean | Whether the content is uncensored |
{stream.unrated} | boolean | Whether the content is unrated |
{stream.upscaled} | boolean | Whether the content has been upscaled |
Season / Episode
| Variable | Type | Description |
|---|---|---|
{stream.seasonPack} | boolean | true if part of a season pack |
{stream.seasons} | number[] | Detected season numbers |
{stream.formattedSeasons} | string | Formatted season string (e.g. S01 or S01-05) |
{stream.folderSeasons} | number[] | Seasons from folder name (when different from filename) |
{stream.formattedFolderSeasons} | string | Formatted seasons from folder name |
{stream.episodes} | number[] | Detected episode numbers |
{stream.formattedEpisodes} | string | Formatted episode string (e.g. E01 or E01-05) |
{stream.folderEpisodes} | number[] | Episodes from folder name (when different from filename) |
{stream.formattedFolderEpisodes} | string | Formatted episodes from folder name |
{stream.seasonEpisode} | string[] | Pre-formatted season/episode strings (e.g. ['S01', 'E05']) |
P2P / Tracker
| Variable | Type | Description |
|---|---|---|
{stream.seeders} | number | Torrent seeder count |
{stream.private} | boolean | true if from a private tracker |
{stream.freeleech} | boolean | true if the torrent is freeleech |
{stream.age} | string | Human-readable age since release |
{stream.ageHours} | number | Age in hours |
Anime
| Variable | Type | Description |
|---|---|---|
{stream.seadex} | boolean | Whether listed as best/alt on SeaDex |
{stream.seadexBest} | boolean | Whether listed as a best release on SeaDex |
Scoring
| Variable | Type | Description |
|---|---|---|
{stream.regexMatched} | string | Name of the highest-priority matched preferred regex |
{stream.rankedRegexMatched} | string[] | All matched Ranked Regex Filter names (sorted) |
{stream.regexScore} | number | Score from matched Regex Filter |
{stream.nRegexScore} | number | Regex score normalised to 0–100 |
{stream.seScore} | number | Score from matched Stream Expression sort rule |
{stream.nSeScore} | number | Stream Expression score normalised to 0–100 |
{stream.seMatched} | string | Name of the Preferred Stream Expression that matched (from first comment) |
{stream.rseMatched} | string[] | Names of all Ranked Stream Expressions that matched |
Service
| Variable | Type | Description |
|---|---|---|
{service.id} | string | Service identifier (e.g. realdebrid) |
{service.shortName} | string | Abbreviated name (e.g. RD) |
{service.name} | string | Full name (e.g. Real-Debrid) |
{service.cached} | boolean | Whether the stream is cached |
Addon
| Variable | Type | Description |
|---|---|---|
{addon.presetId} | string | The preset ID the addon was generated from |
{addon.name} | string | Display name of the addon |
{addon.manifestUrl} | string | The addon's manifest URL |
Metadata
| Variable | Type | Description |
|---|---|---|
{metadata.queryType} | string | Media type being queried (movie, series, anime.series, or anime.movie) |
{metadata.title} | string | Title of the media being queried |
{metadata.runtime} | number | Movie runtime in minutes |
{metadata.episodeRuntime} | number | Episode runtime in minutes |
{metadata.genres} | string[] | List of genres |
{metadata.year} | number | Release year |
Debug
| Variable | Type | Description |
|---|---|---|
{debug.json} | string | Raw JSON of the stream data |
{debug.jsonf} | string | Pretty-printed JSON of the stream data |
Modifiers
Modifiers are applied left to right, and each one acts on the result of the last:
{stream.filename::lower::truncate(20)}Field and modifier names are case-insensitive, so {stream.filename} and {Stream.FileName} are the same. Whitespace inside the braces is ignored.
Applying a modifier to a field that has no value renders nothing, so {stream.size::bytes} is empty rather than an error when the size is unknown. Applying one to the wrong type is an error and is shown inline, e.g. {stream.filename::bytes} renders {unknown_string_modifier(bytes)}.
Any Type
| Modifier | Description |
|---|---|
::default('text') | Use text when the value is missing |
::string | Convert to string |
{stream.quality::default('Unknown')} -> BluRay, or Unknown when absent
{stream.library::string} -> trueA quoted value can be used wherever a field is expected, which is useful for piping fixed text through modifiers:
{'n/a'::upper} -> N/A
{'ready'::smallcaps} -> ʀᴇᴀᴅʏString Modifiers
| Modifier | Description |
|---|---|
::upper | Convert to UPPERCASE |
::lower | Convert to lowercase |
::smallcaps | Convert letters to ꜱᴍᴀʟʟ ᴄᴀᴘꜱ |
::subscript | Convert digits to ₀₁₂₃₄₅₆₇₈₉ |
::superscript | Convert digits to ⁰¹²³⁴⁵⁶⁷⁸⁹ |
::translate('from', 'to') | Map characters by position |
::title | Title Case (capitalise first letter of each word) |
::replace('find', 'replaceWith') | Replace all occurrences of find |
::remove('a', 'b', …) | Delete every occurrence of each argument |
::truncate(N) | Truncate to N characters and append … |
::date('pattern') | Reformat a date — see Date patterns |
::length | Return length of the string |
::reverse | Reverse the string |
::base64 | Encode as base64 |
::smallcaps only affects letters, so it combines with the digit modifiers:
{stream.resolution::smallcaps} -> 2160ᴘ
{stream.resolution::subscript::smallcaps} -> ₂₁₆₀ᴘ::translate maps each character of from to the character at the same position in to. Characters without a counterpart are left unchanged, so a shorter to never drops text:
{stream.quality::translate('Bl','Яⅼ')} -> ЯⅼuRayNumber Modifiers
| Modifier | Description |
|---|---|
::bytes / ::bytes10 | Format as base-10 bytes (KB, MB, GB) |
::sbytes / ::sbytes10 | Concise base-10 byte format |
::bytes2 | Format as base-2 bytes (KiB, MiB, GiB) |
::sbytes2 | Concise base-2 byte format |
::rbytes / ::rbytes10 | Like ::bytes but rounded |
::rbytes2 | Like ::bytes2 but rounded |
::bitrate | Format as bitrate (Kbps, Mbps) |
::rbitrate | Like ::bitrate but rounded |
::sbitrate | Concise bitrate (e.g. 5.2 Mbps) |
::time | Format milliseconds as 1h:23m:45s |
::time('pattern') | Format milliseconds — see Duration patterns |
::star | Star rating ★ out of 5 from a 0–100 score |
::pstar | Padded star rating (always 5 stars e.g. ★★★☆☆) |
::hex | Encode to hexadecimal |
::octal | Encode to octal |
::binary | Encode to binary |
Date & Duration Patterns
::date('…') and ::time('…') take a pattern string, so you can build any style you like instead of picking from a fixed list.
Both share the same rules:
%Xis a token, replaced with a value. Everything else is literal text.%-Xis the unpadded form of%X(7instead of07).[...]marks an optional group: it disappears when every token inside it is zero. Use it to hide empty units.%%,%[and%]emit a literal%,[and].- An unrecognised token is left as-is, so typos show up in the output.
Duration patterns
Used by ::time('…') on {stream.duration}.
| Token | Description | Example |
|---|---|---|
%H | Hours, zero padded | 01 |
%-H | Hours | 1 |
%M | Minutes, zero padded | 23 |
%-M | Minutes | 23 |
%S | Seconds, zero padded | 45 |
%-S | Seconds | 45 |
The largest unit in the pattern carries the overflow, so a pattern with no %H reads minutes as the total:
| Pattern | 1h 23m 45s | 45s |
|---|---|---|
'%H:%M:%S' | 01:23:45 | 00:00:45 |
'%-Hh %-Mm' | 1h 23m | 0h 0m |
'[%-Hh ]%-Mm' | 1h 23m | 0m |
'[%-Hh ]%-Mm[ %-Ss]' | 1h 23m 45s | 0m 45s |
'%-M min' | 83 min | 0 min |
Date patterns
Used by ::date('…') on {stream.date}. Dates are read in UTC; anything that isn't a valid YYYY-MM-DD is passed through untouched.
| Token | Description | Example |
|---|---|---|
%Y | Full year | 2023 |
%y | Two-digit year | 23 |
%m | Month number, zero padded | 07 |
%-m | Month number | 7 |
%B | Month name | July |
%b | Short month name | Jul |
%d | Day of month, zero padded | 04 |
%-d | Day of month | 4 |
%o | Day of month with ordinal suffix | 4th |
%A | Weekday name | Tuesday |
%a | Short weekday name | Tue |
| Pattern | 2023-07-04 |
|---|---|
'%Y-%m-%d' | 2023-07-04 |
'%-d %b %Y' | 4 Jul 2023 |
'%B %o, %Y' | July 4th, 2023 |
'%d/%m/%y' | 04/07/23 |
'%b %Y' | Jul 2023 |
'%A' | Tuesday |
Patterns chain with the other modifiers as usual:
{stream.date::date('%B %o, %Y')::upper} -> JULY 4TH, 2023%o already includes the day number, so write %B %o (July 4th) — not %B %-d%o, which would render July 44th.
Array Modifiers
| Modifier | Description |
|---|---|
::join('separator') | Join elements with separator |
::slice(start, end) | Return a section (end is optional) |
::length | Number of elements |
::first | First element |
::last | Last element |
::random | Single random element |
::sort | Sort (alphabetical for strings, numerical for numbers) |
::rsort | Reverse sort |
::lsort | Lexicographic sort (case-sensitive) |
::reverse | Reverse order |
Conditional Modifiers
Conditionals evaluate to true or false and control what text is shown:
{variable.property::conditionalModifier["trueString"||"falseString"]}Example: Show seeders only if greater than 1:
{stream.seeders::>1["Seeders: {stream.seeders}"||""]}| Modifier | Description | Types |
|---|---|---|
istrue | Value is true | boolean |
isfalse | Value is false | boolean |
exists | Not null, undefined, empty string, or empty array | string, array, any |
in('a', 'b', …) | Value is one of the listed options | string, number, boolean, string[] |
$X | Starts with X / first array element is X | string, string[] |
^X | Ends with X / last array element is X | string, string[] |
~X | Contains X | string, string[] |
=X | Exactly equal to X | string, number |
>=X | Greater than or equal to X | number |
<=X | Less than or equal to X | number |
>X | Greater than X | number |
<X | Less than X | number |
in saves repeating a field across several = comparisons, and matches if any element of an array is listed:
{service.id::in('torbox','realdebrid')["🟩"||"⬜"]}
{stream.languages::in('english')["🇬🇧"||""]}A boolean field needs no modifier at all, and accepts an optional third branch for when the value is missing rather than false:
{service.cached["Cached"||"Uncached"||"Unknown"]}
cached -> Cached
not cached -> Uncached
unknown -> UnknownWithout a third branch, a missing value renders nothing. istrue and isfalse both evaluate to false for a missing value, so use the third branch when the three states need distinguishing.
Nesting
A conditional can contain another conditional inside either branch. Quotes belonging to the inner one must be escaped with \":
{stream.resolution::exists["{stream.quality::exists[\"{stream.resolution} {stream.quality}\"||\"{stream.resolution}\"]}"||"Unknown"]}
2160p + BluRay -> 2160p BluRay
2160p only -> 2160p
neither -> UnknownNesting is limited to five levels deep. Beyond that the branch is emitted as plain text.
Optional Groups
{? ... ?} renders only when every field inside it has a value. If any is missing, the whole group disappears, including its literal text.
{?📅 {stream.age} ?}
age known -> 📅 30d
age unknown -> (nothing, not even the icon)Groups nest, so a separator can depend on its own field:
{?[{stream.resolution}{? · {stream.quality}?}]?}
2160p + BluRay -> [2160p · BluRay]
2160p only -> [2160p]
neither -> (nothing)A group tests whether the field has a value, not whether the rendered text looks empty. {?{stream.type::replace('debrid','')}?} still renders, because stream.type was present — the modifier just blanked it. This matches ::exists.
Conditionals
Chain multiple conditions together using and, or, or xor:
{var1::cond1::or::var2::cond2["trueString"||"falseString"]}Conditions are evaluated left to right: (x and y or z) → ((x and y) or z).
| Operator | Description |
|---|---|
and | Both expressions must be true |
or | At least one expression must be true |
xor | Exactly one expression must be true |
Example: Show seeders only when the stream is either uncached or a P2P torrent:
{service.cached::isfalse::or::stream.type::=p2p::and::stream.seeders::>0["Seeders: {stream.seeders}"||""]}Tools
Use formatting tools with {tools.toolName}:
| Tool | Description |
|---|---|
{tools.newLine} | Add a newline at this position |
{tools.removeLine} | Remove the entire line wherever found |
{tools.newLine} works in the formatter preview but how it renders in Stremio
is platform-dependent. Many platforms do not respect newlines in the name
field. Avoid building your name template around {tools.newLine}.
Chillio
For Chillio, the name template maps to the ChillLink title field. The description template is split line-by-line into metadata strings — each line becomes a separate metadata entry.
Examples
Community-created formats are shared on the Discord Server.
You can also view the built-in formatter definitions at:
packages/core/src/formatters/predefined.ts

