AIOStreamsAIOStreams
Reference

Config Expression Language

Complete reference for config variants and the CEL instruction set used to adjust a configuration on the fly.

A variant is a named script that adjusts your configuration at request time. Variants live inside one config and are selected by the install URL, so a single UUID can serve several differently-behaving addons.

The scripts are written in the Config Expression Language (CEL). Where the Stream Expression Language is evaluated against a list of streams, CEL is declarative: a list of edits applied to your configuration, top to bottom.


Variants or a parent config?

Parent/child configVariant
Configs to set upTwo, each with its own UUIDOne
Written asMerge strategies chosen in the UIA few lines of CEL
Seeing the resultAfter saving, by looking at the streamsA diff, before you save
Whose config it isTheirs, with their own credentialsYours, viewed through a lens

Prefer a variant where you can. Most differences are a line or two, the preview shows the diff before you save, and there is no second configuration to create and keep track of.

Sharing does not settle it either way. Giving your config to someone whose TV cannot handle Dolby Vision is a variant: add excludedVisualTags "DV Only" and hand them /v/no-dv. They get your config exactly, minus the one thing their hardware cannot do, and it stays one config when you change it.

Use a parent/child config when the second configuration has to belong to somebody else — a child config is editable by whoever holds it and carries their own credentials, which you never see, where a variant only ever shows your config through a lens you wrote. The other reason is preference: a parent config is configured by clicking, at the cost of not being able to preview what a merge produces.


Quick start

In Miscellaneous → Variants (Advanced mode), add a variant with the id phone.

Write the script:

set addonName = "AIOStreams (Phone)"
set excludedResolutions = ["2160p", "1440p"]
set size.global.movies = [0, 8000000000]
set resultLimits.global = 10

Hit preview to see exactly which fields change, then save.

On Save/Install, pick the variant. Every install link gains /v/phone:

https://your-instance/stremio/<uuid>/<password>/v/phone/manifest.json

Where the selector goes

Selector location on the install page switches between two forms:

https://your-instance/stremio/<uuid>/<password>/v/phone/manifest.json
https://your-instance/stremio/<uuid>/<password>/manifest.json?v=phone

They do the same thing and both work everywhere, so the path form is the default: it is the one that survives a client rebuilding its request URLs from a base you gave it rather than following the manifest URL. A client keeping the query string is what the other form needs, and not all of them do.

The Search and Newznab/Torznab APIs take the query form only, as &v=, since they are single endpoints rather than a base a client extends.


Selecting variants

Comma-separate to combine. They apply left to right, so later instructions win:

.../v/phone,rd2/manifest.json

An unknown or disabled id is an error, surfaced as an error stream, rather than a silent fall back to the base config. Ids are not case-sensitive and repeats are ignored.

The selector also works on ChillLink and the Seanime extension URLs.

The configure page always edits the base config, even when a variant is selected.


Conditional activation

A variant can also decide for itself. Give it an activation condition and it applies whenever that condition matches the incoming request, with nothing added to the URL: one install serves a phone a different config from a TV. Conditions are checked on every request, whichever link it arrived on, so a match applies on top of whatever that link selected.

The condition is a stream expression that evaluates to true or false, using a limited set of functions:

includes(userAgent, 'android')

Naming a variant in the URL applies it whether or not its condition matches. What applies, in what order covers the case where both happen.

Selecting a conditional variant applies it to every request, not only those its condition matches. A "Real-Debrid down" variant picked on the install page keeps Real-Debrid disabled.

What a condition can read

NameIs
userAgentThe client's User-Agent header, or an empty string. On Jellyfin, the app's name, version and device are added to the end
resourcestream, manifest.json, catalog, meta, subtitles, search, ...
typemovie, series, ... where the request has one
idThe requested id, e.g. tt0111161:1:2
query('name')A query parameter, or an empty string
header('name')A request header, or an empty string
health('id')Whether a health check is currently passing
includes(a, b)Whether a contains b, ignoring case
matches(value, 'expr')Whether a regular expression matches. Follows this instance's regex rules

Finding the right userAgent is guesswork, so the Variants tab lists the last five that actually reached your configuration. Only stream, catalogue, meta, subtitle and Jellyfin requests are counted.

Health checks

A health check is a URL this instance calls to decide whether something is up. Define them in Miscellaneous → Health Checks, then read one as health('id'):

not health('rd-up')
FieldDefaultMeaning
URLCalled with GET, or HEAD if you prefer
Expected status2xxA code (200), a class (2xx) or a range (200-299)
JSON pathA dotted path into a JSON body, e.g. debrid.rd
Expected valueCompared to the value at that path. Empty means any truthy value
Body containsText the response body must contain, ignoring case
Interval300sHow long a result is reused before it is checked again
Timeout3000msHow long to wait for a response
If the check failsdownWhat a timeout or unreachable URL counts as

health() is not limited to variant conditions. It works in group conditions, the dynamic addon fetching exit condition, stream expression filters and rankings, and the precache and preload selectors, so you can also skip a group or drop a service's results while it is down.

Every check a configuration defines is resolved once per request, so one request sees one answer everywhere. Results are cached for the interval you set and shared with everyone whose check names the same URL, which means a service that is down is not re-probed on every request. Once a result is older than the interval, the request that notices is served the old answer and the refresh happens behind it, so a slow endpoint never holds up a stream request twice.

Things worth knowing

  • A condition that fails counts as no match. A broken expression, an unreachable check, a typo: the variant does not apply and the request is served as it otherwise would be. Only a variant named in the URL is an error when it cannot be applied.
  • Automatic variants do not change your addon id. They share the install URL, which is the point. They are still part of the cache key, so two clients on one URL never see each other's results.
  • The manifest could be cached by your client, and Stremio syncs installs between devices, so a condition works best on things the manifest does not describe: filters, sorting, formatting, proxying and which service a stream is played through. Writing to presets, services, catalogModifications or mergedCatalogs changes which catalogues and resources the addon offers, and addonName, addonLogo, addonBackground and addonDescription change how it presents itself. Those are still served correctly on every request, but a client that cached the manifest at install time may keep showing what it saw then.

What applies, in what order

Several variants can apply to one request: every one you selected, and every one whose condition matched. They are layered onto a single copy of your configuration, in this order:

  1. Variants whose condition matched, in the order they are listed in Miscellaneous → Variants.
  2. Variants named in the URL, left to right.

Inside a script, instructions run top to bottom, and use variant runs that variant's instructions at the point it appears. The last write to a field wins, so set replaces whatever an earlier variant put there, while add, remove and insert build on it. Prefer those in a variant meant to combine with others.

RequestApplies
/v/phone,rd2phone, then rd2
No selector, phone and rd-down both matchwhichever is listed first, then the other
/v/no-dv while rd-down matchesrd-down, then no-dv
/v/rd-down while rd-down also matchesonce only, in its position in the URL

A request may run 5000 instructions across everything it applies, including those use variant reaches. One that runs out stops there and says so.

A variant that use variant pulls in runs again even if it already applied on its own. Most instructions land the same way twice, so this is usually invisible.


Syntax

One instruction per line. # starts a comment, except inside a string. Object and array literals may span lines.

# Two comments and a multi-line literal
set resultLimits.global = 10

merge deduplicator = {
  "enabled": true,
  "keys": ["filename", "infoHash"]
}

Instructions

InstructionEffect
set <path> = <value>Assign a value. Missing intermediate objects are created.
merge <path> = { ... }Deep merge. Objects recurse, arrays are replaced, a null member deletes that key.
unset <path>Delete a key, or remove a list element when the path ends in an index or selector.
clear <path>Empty a list or object.
add <path> <value>, ...Append to a list, skipping values already present. Creates the list if absent.
prepend <path> <value>, ...The same, inserted at the front.
insert <path> = <value>, ...Insert at the position the path names. The value lands there and the rest shift down.
insert after <path> = <value>The same, placed just past the addressed entry. insert before spells out the default.
insert <n> after <path> = <value>n places from the addressed entry: 2 after leaves one entry in between. before counts backwards.
remove <path> <value>, ...Remove matching values from a list.
remove <path>With no values, the path itself selects the elements to remove.
enable <path> / disable <path>Shorthand for set <path>.enabled = true / false.
use formatter <name>Load one of your saved formatters.
use variant <id>Apply another variant's instructions at this point.

Values

Strings (single or double quoted), numbers, true, false, null, arrays and objects. Strings support \n, \t, \r, \b, \f, \\, \", \', \/ and \uXXXX.

set addonName = "Living room"
set resultLimits.global = 10
set excludeUncached = true
set sortCriteria.global = [
  { "key": "cached", "direction": "desc" },
  { "key": "resolution", "direction": "desc" },
  { "key": "size", "direction": "desc" }
]

Paths

A path starts with a configuration field and drills in.

SegmentMeaningExample
.nameAn object propertydeduplicator.enabled
[0]A list element by positionsortCriteria.global[0]
[-1]Counting back from the endsortCriteria.global[-1]
[*]Every element of a listpresets[*].options.timeout
[*]Every value of an objectsortCriteria[*]
[key=value]Elements whose property equals a valueservices[id=realdebrid]
[key!=value]Elements whose property does not equal itpresets[type!=torrentio]
[key*=value]Elements whose property contains the textrankedRegexPatterns[name*="german"]
[key!*=value]Elements whose property does not contain itpresets[type!*=torrent]
[=value]Plain list entries, comparing the entry itselfexcludedKeywords[="cam"]

A selector may match several elements, in which case the instruction applies to all of them: disable presets[type=torrentio] disables every Torrentio instance.

[*] is the one segment that also walks an object, which is how a single instruction reaches all twelve sort lists: add sortCriteria[*] { "key": "resolution", "direction": "desc" } appends the same criterion to every one you have defined. The [key=value] selectors stay list-only, since an object's members are addressed by name.

= and != are exact and case-sensitive. *= and !*= are substring matches that ignore case.

Lists of plain values

Drop the key to compare the entry itself. This is how you reach a list of strings such as excludedKeywords or syncedExcludedRegexUrls, where there is no property to name. All four operators work this way:

remove excludedKeywords[="CAM"]           # exactly "CAM", nothing else
remove excludedKeywords "CAM"             # the same thing, shorter
remove excludedKeywords[*="cam"]          # "CAM", "hdcam" and "camrip"
remove syncedExcludedRegexUrls[*="example.com"]   # every URL from one host

Targeting one entry in a list

Different lists identify their entries differently.

Services are keyed by id:

set services[id=realdebrid].credentials.apiKey = "SECOND_ACCOUNT_KEY"

Addons have an instanceId, but it is a short random hex string such as 8ae, so prefer the addon's type or the name you gave it. A selector key may be dotted, which is how you reach the name inside options:

disable presets[type=torrentio]                  # every Torrentio instance
disable presets[options.name*="4K"]              # by the name shown in the UI
disable presets[instanceId=8ae]                  # one exact instance

The editor autocompletes real instance ids after presets[instanceId=, annotated with each addon's name.

Ranked regex patterns and regex overrides carry a name:

set rankedRegexPatterns[name="HDR boost"].score = 500
remove rankedRegexPatterns[name*="cam"]

Stream expressions have no name field. Their display name lives inside the expression as a /* Name */ comment, so match the expression text with *=:

disable excludedStreamExpressions[expression*="Low seeders"]
disable rankedStreamExpressions[expression*="4K bonus"]

Position works too, but breaks when you reorder the list:

disable excludedStreamExpressions[0]

enable and disable set an enabled property. regexOverrides and selOverrides use an inverted disabled flag instead, so for those write set regexOverrides[name*="hdr"].disabled = true.


Inserting at a position

add and prepend reach the ends of a list. insert reaches everywhere else: the path names a position and the value lands there.

insert excludedStreamExpressions[5] = { "expression": "/*No 4K*/ resolution(streams, '2160p')", "enabled": true }

The new entry becomes number 5 and the one that was there moves down. insert before is the same instruction spelled out.

Positions shift when you reorder the list, so anchor on a selector instead. after is usually the one you want, because it reads in the order the list runs:

insert after excludedStreamExpressions[expression*="Torbox Download Limit"] = {
  "expression": "/*No 4K*/ resolution(streams, '2160p')",
  "enabled": true
}

Count further out when the neighbouring slot is not the place you mean. 1 after is the slot next to the entry, 2 after leaves one entry in between, and before counts the same way backwards. A count has to say which way it goes, so it always comes with one of the two keywords:

insert 2 after excludedStreamExpressions[expression*="Torbox Download Limit"] = { "expression": "/*No 4K*/ resolution(streams, '2160p')" }
insert 3 before rankedStreamExpressions[expression*="4K bonus"] = { "expression": "visualTag(streams, 'HDR')", "score": 100 }

[-1] keeps the meaning it has everywhere else, the last entry, so the two ends are:

insert excludedKeywords[0] = "cam"          # the front, where prepend puts it
insert after excludedKeywords[-1] = "cam"   # the end, where add puts it

Several values at once keep their order. Unlike add, nothing is skipped for being already present, which is what makes moving an entry work:

unset excludedKeywords[="cam"]
insert excludedKeywords[0] = "cam"

An insert names one place per list. A path that reaches several lists, such as presets[*].options.<list>, writes one value into each of them, the way add does. Within a list, a selector that matches several entries uses the first and says so in the editor preview. A position that falls outside a list is skipped rather than clamped, and a trailing [*] is rejected.


Recipes

Phone on mobile data. Cap the file size, skip 4K, return fewer results.

set addonName = "AIOStreams (Phone)"
set excludedResolutions = ["2160p", "1440p"]
set size.global.movies = [0, 8000000000]
set size.global.series = [0, 3000000000]
set resultLimits.global = 10

A second debrid account.

set services[id=realdebrid].credentials.apiKey = "SECOND_ACCOUNT_KEY"

Instant playback only. Cached results, no failover attempts.

set excludeUncached = true
set failover.enabled = false

A different formatter. use formatter copies one of your saved formatters into the custom slot and switches to it, so a whole template never has to go in the script.

use formatter "Minimal"

Or pick a built-in one:

set formatter.id = "minimalisticgdrive"

Debugging. Show the statistics streams and stop hiding addon errors.

set hideErrors = false
set statistics = { "enabled": true, "position": "top", "statsToShow": ["addon", "filter", "timing"] }

Only a couple of addons.

disable presets[*]
enable presets[type=comet]
enable presets[type=easynews]

A different language. Language values are the ones the filter menu offers, plus Original, Dual Audio, Multi, Dubbed and Unknown.

set requiredLanguages = ["German", "Multi", "Dual Audio"]
prepend preferredLanguages "German"

A different language, with confirmed media info only.

set requiredLanguages = ["German"]
add excludedStreamExpressions { "expression": "mediaInfoQuality(streams, 'unknown')", "enabled": true }

Reordering the sort. Put resolution above cache status for a machine that does not mind waiting.

set sortCriteria.global = [
  { "key": "resolution", "direction": "desc" },
  { "key": "cached", "direction": "desc" },
  { "key": "streamExpressionScore", "direction": "desc" },
  { "key": "size", "direction": "desc" }
]

Or slot one criterion into second place in every sort list you have defined, leaving the rest of each list alone:

insert sortCriteria[*][1] = { "key": "resolution", "direction": "desc" }

Layering. Either reference one variant from another:

use variant phone
add excludedVisualTags "3D"

or keep them independent and combine at install time with /v/phone,no3d. Either way, order decides which one wins where both write to the same field.


Limits

Operators can tune these; defaults shown.

LimitDefaultEnvironment variable
AvailabilityeveryoneVARIANT_ACCESS (all, trusted, none)
Variants per config10MAX_VARIANTS
Characters per script4000MAX_VARIANT_SCRIPT_LENGTH
Characters across all scripts20000MAX_VARIANT_TOTAL_SCRIPT_CHARACTERS
Instructions run per request5000MAX_VARIANT_TOTAL_INSTRUCTIONS
Elements one instruction writes200MAX_VARIANT_PATH_MATCHES
Health checks: availabilityeveryoneHEALTH_CHECK_ACCESS (all, trusted, none)
Health checks per config5MAX_HEALTH_CHECKS
Shortest health check interval60sHEALTH_CHECK_MIN_TTL
Longest health check timeout10000msHEALTH_CHECK_MAX_TIMEOUT
Health check response read64KBHEALTH_CHECK_MAX_BYTES
Private health check URLsallowedALLOW_PRIVATE_URLS

Behaviour notes

  • An instruction that matches nothing is skipped and only logged. Remove Real-Debrid later and a variant that swapped its credentials keeps working instead of breaking every install URL you handed out. The editor preview shows these as warnings while you write.
  • Syntax errors and forbidden fields are rejected on save, so they never reach a live request.
  • A variant's name is a label for the configuration UI and the install page. Use set addonName to change what your client shows.
  • Variants are not inherited from a parent config: they reference this config's own addon ids and saved formatter names. Neither are health checks.
  • Health check URLs cannot reach private addresses when the instance owner has turned those off, and can never point back at the instance itself.

On this page