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 config | Variant | |
|---|---|---|
| Configs to set up | Two, each with its own UUID | One |
| Written as | Merge strategies chosen in the UI | A few lines of CEL |
| Seeing the result | After saving, by looking at the streams | A diff, before you save |
| Whose config it is | Theirs, with their own credentials | Yours, 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 = 10Hit 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.jsonWhere 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=phoneThey 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.jsonAn unknown or disabled id is an error, surfaced as an error stream, rather than a silent fall back to the base config.
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.
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
| Instruction | Effect |
|---|---|
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. |
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.
| Segment | Meaning | Example |
|---|---|---|
.name | An object property | deduplicator.enabled |
[0] | A list element by position | sortCriteria.global[0] |
[-1] | Counting back from the end | sortCriteria.global[-1] |
[*] | Every element | presets[*].options.timeout |
[key=value] | Elements whose property equals a value | services[id=realdebrid] |
[key!=value] | Elements whose property does not equal it | presets[type!=torrentio] |
[key*=value] | Elements whose property contains the text | rankedRegexPatterns[name*="german"] |
[key!*=value] | Elements whose property does not contain it | presets[type!*=torrent] |
[=value] | Plain list entries, comparing the entry itself | excludedKeywords[="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.
= 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 hostTargeting 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 instanceThe 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.
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 = 10A 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 = falseA 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"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" }
]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.
Limits
Operators can tune these; defaults shown.
| Limit | Default | Environment variable |
|---|---|---|
| Availability | everyone | VARIANT_ACCESS (all, trusted, none) |
| Variants per config | 10 | MAX_VARIANTS |
| Characters per script | 4000 | MAX_VARIANT_SCRIPT_LENGTH |
| Characters across all scripts | 20000 | MAX_VARIANT_TOTAL_SCRIPT_CHARACTERS |
| Instructions per script | 100 | MAX_VARIANT_INSTRUCTIONS |
| Variants per request | 4 | MAX_ACTIVE_VARIANTS |
use variant nesting depth | 5 | MAX_VARIANT_DEPTH |
| Elements one instruction writes | 200 | MAX_VARIANT_PATH_MATCHES |
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 addonNameto 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.

