-
-
Notifications
You must be signed in to change notification settings - Fork 690
Templates
A practical reference for writing AIOStreams configuration templates — covering all supported input types, dynamic expression syntax, and validation rules.
- Template Structure
- metadata Fields
- Service Handling
- Template Inputs
- Dynamic Expressions
- Template Placeholders
- Validation Rules
- Sharing Templates
A template is a JSON object with two top-level keys:
{
"metadata": { ... },
"config": { ... }
}config is a partial UserData object — only the keys you include are applied. Everything absent is left unchanged in the user's existing config. All dynamic expressions inside config are evaluated at load time, before the config is merged.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (1–100) |
No | Unique identifier. Auto-generated UUID if omitted. Use namespaced form: author.my-template. |
name |
string (1–100) |
Yes | Display name in the template browser. |
description |
string (1–1000) |
Yes | Supports Markdown (links, bold, lists). |
author |
string (1–20) |
Yes | Author name or handle. |
source |
"builtin" | "custom" | "external"
|
No | Defaults to "builtin". Use "external" for user-imported remote templates. |
version |
semver string | No | Defaults to "1.0.0". Used for auto-update comparisons. |
category |
string (1–20) |
Yes | Shown in the browser filter bar (e.g. "Debrid", "Usenet"). |
services |
ServiceId[] |
No | Controls service selection screen — see Service Handling. |
serviceRequired |
boolean |
No | Whether a service must be selected before applying. |
setToSaveInstallMenu |
boolean |
No | Redirects the UI to the Save & Install menu after loading. Defaults to true. |
sourceUrl |
URL string | No | Remote URL the template was fetched from — enables auto-update checks. |
inputs |
InputDefinition[] |
No | User-fillable options shown before loading. See Template Inputs. |
The services field in metadata controls whether (and how) the user is asked to select a debrid service before the template is applied.
| Scenario | Behaviour |
|---|---|
services not set |
All services are shown in the selection screen |
services: [] |
Service selection is skipped entirely |
services: ["realdebrid", "torbox", ...] |
Only the listed services are shown |
Single service + serviceRequired: true
|
Service selection is skipped; user is prompted for that service's credentials directly |
serviceRequired absent or false
|
A "Skip" button is shown on the service selection screen |
At load time, the selected services are available in conditions and interpolation via services.<serviceId>.
The bare services reference (no service ID) is also supported in all expression contexts. It resolves to truthy when at least one service has been selected and falsy when none have — making it the canonical way to distinguish between debrid mode (a service is active) and P2P / no-service mode (the user skipped service selection). See __if and __switch for usage.
Inputs are defined in metadata.inputs as an array of objects. They are shown to the user in a dialog before the template loads. Values are then accessible throughout config via inputs.<id>.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
Yes | Identifier used in inputs.<id> references throughout the config. |
name |
string |
Yes | Label shown in the UI. |
description |
string |
Yes | Help text below the label. Supports Markdown. |
type |
see Input Types | Yes | Controls the UI widget. |
required |
boolean |
No | Prevents proceeding if the field is empty. |
default |
any | No | Pre-filled value. Previous imports also pre-fill remembered values. |
options |
{ value, label }[] |
For select / multi-select
|
List of choices. |
showInSimpleMode |
boolean |
No | Set to false to hide in Simple (Noob) mode. Defaults to true. |
advanced |
boolean |
No | Shorthand for showInSimpleMode: false — hidden in Simple mode. |
constraints |
{ min?, max?, forceInUi? } |
No | Min/max length (strings) or min/max value (numbers). forceInUi enforces in the widget. |
__if |
string |
No | Hides the input when the condition is false. Only services.<id> is supported. See Conditional Inputs. |
intent |
see Alert | For alert
|
Controls the colour and icon of an alert banner. Defaults to info. |
socials |
{ id, url }[] |
For socials
|
List of social links to render. See Socials for valid id values. |
| Type | Widget | Notes |
|---|---|---|
string |
Text input | |
password |
Masked input | For API keys and credentials — value is not stored in plaintext client-side after import. |
number |
Number input | Returns a number; blank/unset returns undefined. |
boolean |
Toggle | Returns true or false. |
select |
Dropdown | Requires options. Returns the selected value string. |
select-with-custom |
Dropdown + text | Same as select but adds a "Custom" option revealing a free-text input. |
multi-select |
Multi-select | Requires options. Returns an array of selected value strings. |
url |
URL text input | Client-side format validation. |
alert |
Styled banner | Displays a coloured info/warning/error banner. Uses name as title, description as body (Markdown). Captures no value. Requires intent. See Alert. |
socials |
Icon row | Renders a horizontal row of social media icon links. Uses socials field. Captures no value. See Socials. |
subsection |
Button → modal | Groups related sub-inputs behind a button. See Subsections below. |
A subsection input renders as a button that opens a modal containing its subOptions. Each sub-option is itself a full InputDefinition. Values for sub-options are stored as a nested object and accessed with dot notation:
inputs.<subsectionId>.<subOptionId>
This applies in __if conditions, __switch references, and {{}} interpolation.
{
"id": "proxy",
"name": "Proxy Settings",
"description": "Optional proxy configuration.",
"type": "subsection",
"required": false,
"subOptions": [
{
"id": "url",
"name": "Proxy URL",
"description": "Full URL including protocol.",
"type": "url",
"required": false
},
{
"id": "id",
"name": "Proxy Service",
"description": "The proxy service to use.",
"type": "select",
"required": false,
"options": [{ "value": "custom", "label": "Custom" }]
}
]
}Used in config as {{inputs.proxy.url}} or "__if": "inputs.proxy.url".
An alert input renders a styled banner — useful for displaying important notes, warnings, or requirements to the user before they configure options. It captures no value.
{
"id": "notice",
"name": "Important",
"description": "This template requires an active RealDebrid subscription.",
"type": "alert",
"intent": "warning"
}The intent field controls the colour and icon:
| Intent | Appearance |
|---|---|
info |
Blue, filled background |
success |
Green, filled background |
warning |
Orange, filled background |
alert |
Red, filled background |
info-basic |
Neutral card with blue icon |
success-basic |
Neutral card with green icon |
warning-basic |
Neutral card with orange icon |
alert-basic |
Neutral card with red icon |
Defaults to info when intent is omitted. The filled variants (info, warning, etc.) use a tinted background; the -basic variants use a neutral card with a coloured icon only — less visually intrusive.
Note
id is still required by the schema but is not displayed and has no effect on the rendered banner. required, default, and options are ignored for this type.
A socials input renders a horizontal row of social icon links. Use it to credit yourself or link to support pages at the end of the inputs dialog. It captures no value.
The socials field is an array of { id, url } objects. Supported id values:
website · github · discord · ko-fi · patreon · buymeacoffee · github-sponsors · donate
{
"id": "credits",
"name": "Credits",
"description": "Support the author",
"type": "socials",
"socials": [
{ "id": "github", "url": "https://github.com/yourname" },
{ "id": "ko-fi", "url": "https://ko-fi.com/yourname" },
{ "id": "discord", "url": "https://discord.gg/invite" }
]
}Note
name and description are required by the schema but are not rendered. required, default, options, and constraints are ignored for this type.
All expressions are evaluated at load time, after the user fills in inputs and selects services. The resolved config is then merged into the user's data.
Three mechanisms are available:
Any input can include a "__if" field. If the condition evaluates to false, the input is hidden entirely from the inputs dialog — the user never sees it and its value is treated as unset.
Only services conditions are supported here — the bare services (any service selected) or services.<id> (specific service selected). This is intentional: services are selected in the step before template inputs are shown, so the service state is fully known by the time the inputs dialog renders. Cross-input conditions (inputs.*) are not supported.
{
"id": "rdApiKey",
"name": "Real-Debrid API Key",
"description": "Your RealDebrid API key.",
"type": "password",
"required": true,
"__if": "services.realdebrid"
}Compound and / or / xor conditions and negation (!) all work, as long as every operand is a services.<id> reference:
{ "__if": "services.realdebrid or services.alldebrid" }
{ "__if": "!services.torbox" }Note
Inputs hidden by __if are excluded from required-field validation - a hidden required input will not block the user from proceeding.
Use {{inputs.<id>}}, {{services.<id>}}, or {{services.<id>.<key>}} inside any string value to substitute values at load time.
{
"addonLogo": "{{inputs.logoUrl}}",
"preferredLanguages": ["{{inputs.languages}}", "Original", "Unknown"]
}Type preservation: When the entire string is a single {{...}} token, the raw value is returned — not stringified. This means an array input used as "{{inputs.languages}}" inside a preferredLanguages array will be spread out into the parent array, not inserted as a nested array. A boolean stays a boolean. A number stays a number.
"preferredLanguages": [
"{{inputs.languages}}",
"Original",
"Dual Audio",
"Multi",
"Dubbed",
"Unknown"
]If inputs.languages is ["French", "German"], this resolves to:
["French", "German", "Original", "Dual Audio", "Multi", "Dubbed", "Unknown"]Multiple tokens in one string: Concatenation mode — values are stringified. Useful for building dynamic expressions or comments:
"expression": "/*{{inputs.languagePassthrough}} Passthrough*/ passthrough(slice(language(..., '{{inputs.languagePassthrough}}'), 0, 5), 'title', 'excluded')"{{services}} — the bare token (no service ID) resolves to the array of all currently selected service IDs. As a sole {{...}} token it returns the array directly (e.g. ["torbox", "realdebrid"]); embedded in a larger string it joins the IDs with commas (e.g. "torbox,realdebrid"). When no services are selected it returns an empty array / empty string.
"enabledServices": "{{services}}"→ ["torbox"] when TorBox is the only selected service; [] when no services are selected.
{{services.<id>}} resolves to the string "true" or "false" depending on whether the service is selected. As a sole token it returns the boolean directly.
{{services.<id>.<key>}} — Credential refs: Use a two-segment service ref to inject a credential the user entered during service setup into a config field. The placeholder is preserved through template processing and resolved at the final save step, after all inputs are collected.
{
"type": "newznab",
"options": {
"url": "https://search.torbox.app/prowlarr",
"apiKey": "{{services.torbox.apiKey}}"
}
}The <key> must match the credential field name as stored in the service config (e.g. apiKey, token). If the credential has not been entered, the field resolves to an empty string "".
Credential refs are intentionally not evaluated by
__if/__switchconditions — they are always preserved as literal strings until the final resolution step. Useservices.<id>(without a key) in conditions to check whether a service is selected.
Undefined references resolve to an empty string "".
Add "__if": "<condition>" to any object inside an array. If the condition is false, the object is removed from the array entirely. If true, the __if key is stripped and the rest of the object is kept.
{
"excludedStreamExpressions": [
{
"__if": "inputs.torboxTier == nonPro",
"expression": "/*TB Non-Pro Download Limit*/ size(uncached(streams), '200GB')",
"enabled": true
},
{
"__if": "inputs.optionalFilters includes dvOnlyNonRemux",
"expression": "/*DV Only Non-Remux*/ negate(...)",
"enabled": true
}
]
}[!INFO]
__ifonly works on objects inside arrays; it has no effect at the top level of an object.
| Form | Example | Meaning |
|---|---|---|
| Bare reference | inputs.enableFeature |
Truthy if value is not false, null, "", or [] — 0 is truthy
|
| Negation | !inputs.enableFeature |
Logical NOT of the bare reference |
| Equality | inputs.tier == premium |
String value equals "premium"
|
| Inequality | inputs.tier != none |
String value does not equal "none"
|
| Array includes | inputs.optionalFilters includes dvPassthrough |
Array contains the string "dvPassthrough"
|
Numeric >
|
inputs.count > 5 |
Numeric value is strictly greater than 5 |
Numeric >=
|
inputs.count >= 5 |
Numeric value is greater than or equal to 5 |
Numeric <
|
inputs.count < 10 |
Numeric value is strictly less than 10 |
Numeric <=
|
inputs.count <= 10 |
Numeric value is less than or equal to 10 |
| Any service (debrid) | services |
True when any service is selected (debrid mode) |
| No service (P2P) | !services |
True when no service is selected (P2P / no-debrid mode) |
| Service check | services.realdebrid |
User has realdebrid enabled |
| Negated service | !services.realdebrid |
User does NOT have realdebrid enabled |
| Nested subsection | inputs.proxy.url |
Truthy if the url sub-option inside proxy is filled |
Compound and
|
inputs.flag and inputs.tier == pro |
Both sub-conditions must be true (highest precedence) |
Compound xor
|
inputs.a xor inputs.b |
Exactly an odd number of sub-conditions must be true |
Compound or
|
services.torbox or services.realdebrid |
At least one sub-condition must be true (lowest precedence) |
Notes:
- The left-hand side must start with
inputs.orservices., or be the bareservicestoken. - The bare
servicestoken (no dot, no ID) is truthy when any service is selected and falsy when none are. Useservicesfor debrid mode and!servicesfor P2P mode. - The right-hand side of an operator expression is always treated as a plain string.
-
servicesandservices.*do not support operator forms (==,!=,includes, numeric) — only bare truthiness and compound operators. - Negation (
!) applies to the entire single condition, including operator results. - In compound forms, each sub-expression carries its own
!prefix; the compound itself cannot be negated. - Compound operator splitting is boundary-aware: words like
"and","or","xor"that appear inside a value (e.g.inputs.genre == action and adventure) are not treated as compound separators.
Practical examples:
{ "__if": "inputs.resultLimit" }→ Included only if the user entered a result limit (non-empty, non-false). Note: a value of 0 is truthy.
{ "__if": "inputs.optionalFilters includes sdrPassthrough" }→ Included only if the user selected sdrPassthrough in the multi-select.
{ "__if": "inputs.languagePassthrough != none" }→ Included for any language except "none" (the disabled sentinel value for a select input).
{ "__if": "inputs.tier == pro" }→ Included only when the user picks the "pro" tier option.
Join multiple sub-conditions with and, or, or xor. Each sub-expression is a full single condition (including its own optional !).
Precedence (high → low): and > xor > or
This follows standard logic conventions, so a or b and c means a or (b and c).
{ "__if": "inputs.flag and inputs.tier == pro" }→ Only when the flag is enabled and tier is "pro".
{ "__if": "services.torbox or services.realdebrid" }→ When the user has either TorBox or Real-Debrid enabled.
{ "__if": "inputs.flag and !services.torbox" }→ When flag is set but TorBox is not selected.
{ "__if": "inputs.tier == pro or inputs.tier == premium" }→ When tier is either "pro" or "premium" (useful when != would be overly permissive).
Use >, >=, <, <= to compare a number input against a fixed threshold. The left-hand side must be inputs.<key>. String values are coerced to a number; non-numeric values (including undefined) evaluate to false.
{ "__if": "inputs.resultLimit > 0" }→ Only when the user has entered a positive limit.
{ "__if": "inputs.resultLimit >= 1 and inputs.resultLimit <= 10" }→ Range check — value must be between 1 and 10 inclusive.
{ "__if": "!inputs.quality < 4" }→ Negated numeric: true when quality is not less than 4 (i.e. ≥ 4).
For simple "was this filled in" checks on number inputs, the bare form
inputs.resultLimitalready handlesundefinedand empty string. Use numeric comparisons when the specific magnitude matters.
The same __if + __value syntax that filters array items also works at the object-property level. When { "__if": "cond", "__value": X } is the value of a config key, the key is conditionally included or removed:
-
Condition true → the key is set to
X(recursed as normal —{{}}, nested__if, etc. all apply) - Condition false → the key is absent from the applied config, leaving the user's existing value untouched
{
"formatter": {
"__if": "!inputs.retainFormatter",
"__value": { "id": "tamtaro" }
},
"presets": {
"__if": "inputs.includeAddons",
"__value": [
{ "type": "torrentio", "enabled": true },
{ "__if": "services.torbox", "type": "torbox-search", "enabled": true }
]
}
}- When
retainFormatterisfalse→formatteris set to{ "id": "tamtaro" } - When
retainFormatteristrue→formatteris absent from the applied config (user's formatter is kept) - When
includeAddonsistrue→presetsis set to the addon list (with service-conditional items evaluated inside) - When
includeAddonsisfalse→presetsis absent (user's addon list is kept)
This is the recommended pattern for "opt-in" config sections — fields the template provides only when the user explicitly asks for them.
Replaces an entire object with a different object depending on the value of an input. Used for things like choosing a formatter, swapping a preset's options block, etc.
{
"__switch": "inputs.<id>",
"cases": {
"<value1>": { ...replacement... },
"<value2>": { ...replacement... }
},
"default": { ...fallback... }
}-
__switchcan referenceinputs.<id>(nested viainputs.<subsectionId>.<subOptionId>also works),services.<id>(boolean: whether a specific service is selected), or the bare"services"(resolves to the comma-joined string of all selected service IDs — empty string""when no services are selected). -
caseskeys are matched as strings against the stringified input value. -
defaultis used when no case matches. Strongly recommended — omitting it results innull. - Replacement objects are recursed into —
__if,__switch, and{{}}insidecasesall work.
Example — choosing a formatter based on user input:
"formatter": {
"__switch": "inputs.formatterStyle",
"cases": {
"torrentio": { "id": "torrentio" },
"gdrive": { "id": "gdrive" }
},
"default": {
"id": "prism"
}
}When the user picks "torrentio", the formatter becomes { "id": "torrentio" }. Any unmatched selection resolves to the default.
Example — service-conditional preset options:
{
"presets": [
{
"__if": "inputs.includeDebridio",
"type": "debridio",
"instanceId": "abc",
"enabled": true,
"options": {
"name": "Debridio"
}
}
]
}Example — debrid vs P2P branching with bare services:
Use __switch: "services" with "" as the P2P case key. The resolved value is the comma-joined service ID string, so "" reliably matches the no-service (P2P) state:
"preferredStreamTypes": {
"__switch": "services",
"cases": {
"": ["p2p"]
},
"default": ["cached", "usenet"]
}When no service is selected, the user is in P2P mode and only p2p stream types are preferred. When any service is selected (debrid mode), cached and usenet are preferred.
This pattern extends naturally to nested switches — the outer __switch: "services" splits debrid vs P2P, and each branch can then independently __switch on a user input:
"sortCriteria": {
"__switch": "services",
"cases": {
"": {
"__switch": "inputs.sortPref",
"cases": {
"speed": { "global": [{ "by": "seeders", "direction": "asc" }] }
},
"default": { "global": [{ "by": "seeders", "direction": "desc" }] }
}
},
"default": {
"__switch": "inputs.sortPref",
"cases": {
"quality": { "global": [{ "by": "resolution", "direction": "desc" }] },
"speed": { "global": [{ "by": "size", "direction": "asc" }] }
},
"default": { "global": [{ "by": "size", "direction": "desc" }] }
}
}Note on
caseskeys: In__switch: "services", the resolved value is the comma-joined service ID string (e.g."torbox","torbox,realdebrid"). The only reliably matchable key is""for the no-service (P2P) case. Use__if: "services"or__if: "!services"on individual array items if you need per-item debrid/P2P gating instead.
Add "__value": <string | string[]> to an object inside an array to inject values directly into the parent array rather than inserting the object itself. Combine with "__if" for conditional injection.
- String value → inserts one item
- Array value → spreads all items into the parent array
-
{{inputs.<id>}}interpolation works in both forms
{
"excludedVisualTags": [
"3D",
{ "__if": "inputs.excludeDV", "__value": "DV" },
{ "__if": "inputs.excludeHdr", "__value": ["HDR", "HDR10", "HDR10+"] }
]
}If excludeDV is true and excludeHdr is false, this resolves to ["3D", "DV"].
If both are true, it resolves to ["3D", "DV", "HDR", "HDR10", "HDR10+"].
__value without __if is always injected:
{
"excludedVisualTags": [
{ "__value": ["H-OU", "H-SBS"] },
{ "__if": "inputs.excludeAi", "__value": "AI" }
]
}This is the recommended pattern for user-controlled membership in flat string[] fields (e.g. excludedVisualTags, preferredResolutions). It scales to any number of independent boolean inputs without the combinatorial explosion of __switch.
__valueis only meaningful inside arrays. Using it outside an array context has no defined behaviour.
Place { "__remove": true } as the value of any config key to unconditionally remove that key from the applied config. The key will not be present in the merged result, leaving the user's existing value untouched.
{
"formatter": { "__remove": true }
}This is most useful as a __switch case value to make "don't touch this field" a selectable outcome:
"formatter": {
"__switch": "inputs.formatterChoice",
"cases": {
"retain": { "__remove": true },
"tamtaro": { "id": "tamtaro" },
"prism": { "id": "prism" }
},
"default": { "id": "tamtaro" }
}When the user picks "retain", the formatter key is absent from the applied config — their current formatter is left unchanged. Any other choice sets the formatter to the specified value.
For the common two-option case ("use template's value" vs. "keep mine"),
__if+__valueis simpler:{ "__if": "!inputs.retainFormatter", "__value": { "id": "tamtaro" } }Use
__removeinside__switchwhen you need three or more selectable outcomes, one of which is "leave unchanged".
For fields the user should fill in themselves after loading (without an inputs dialog), use these sentinel strings:
| Placeholder string | Meaning |
|---|---|
"<template_placeholder>" |
Required — the field must be filled in |
"<required_template_placeholder>" |
Same as above, explicit alias |
"<optional_template_placeholder>" |
Optional — the field can be left blank |
{
"tmdbApiKey": "<template_placeholder>",
"rpdbApiKey": "<optional_template_placeholder>"
}The frontend highlights these values as unfilled after the template is applied.
When a template is imported or viewed, validation runs automatically and flags issues.
-
metadata.name,description,author,categorymust be non-empty strings. -
metadata.versionmust be a valid semver string (1.0.0). -
metadata.sourcemust be"builtin","custom", or"external". -
__ifcondition must be a non-empty string. -
__ifnamespace must beinputsorservices. -
__switchreference must start withinputs.orservices..
-
{{inputs.<id>}}token references an ID not declared inmetadata.inputs. -
__ifreferences aninputs.<id>not inmetadata.inputs. -
__switchreferences an input ID not inmetadata.inputs. -
__switchis missing acasesobject (always resolves todefault).
Condition (in __if):
inputs.<id> → truthy check (0 is truthy; undefined/null/''/false/[] are falsy)
!inputs.<id> → falsy check
inputs.<id> == value → equality
inputs.<id> != value → inequality
inputs.<id> includes value → array contains
inputs.<id> > n → numeric greater-than
inputs.<id> >= n → numeric greater-than-or-equal
inputs.<id> < n → numeric less-than
inputs.<id> <= n → numeric less-than-or-equal
services → any service selected (debrid mode)
!services → no service selected (P2P / no-debrid mode)
services.<serviceId> → specific service enabled
!services.<serviceId> → specific service not enabled
inputs.<sub>.<subOpt> → nested subsection value
Compound conditions (precedence: and > xor > or):
<expr> and <expr> → both must be true
<expr> or <expr> → at least one must be true
<expr> xor <expr> → odd number must be true
!inputs.a and inputs.b → each sub-expression carries its own !
Object replacement (in any object position):
{ "__switch": "inputs.<id>", "cases": { ... }, "default": { ... } }
{ "__switch": "services", "cases": { "": <p2p value> }, "default": <debrid value> }
→ "" case matches when no services are selected (P2P mode)
Conditional array value injection (inside arrays only):
{ "__value": "string" } → always inject one item
{ "__value": ["a", "b"] } → always spread items
{ "__if": "...", "__value": "string" } → inject one item if condition passes
{ "__if": "...", "__value": ["a", "b"] } → spread items if condition passes
Conditional object key (object property level):
{ "key": { "__if": "cond", "__value": X } }
→ condition true: key is set to X (recursed)
→ condition false: key is absent from applied config
Unconditional key removal:
{ "key": { "__remove": true } } → key always absent from applied config
(also valid as a __switch case value)
String interpolation (in any string value):
"{{services}}" → array of selected service IDs (sole token)
or comma-joined string (multi-token)
"{{inputs.<id>}}" → raw value (type-preserving if sole token)
"{{services.<id>}}" → true/false (boolean if sole token, string otherwise)
"{{services.<id>.<key>}}" → credential ref: resolved to the user's entered
credential value at final save; empty string if missing
"prefix_{{inputs.<id>}}_suffix" → concatenated string
Instance hosters can add their own templates via either the templates folder in the data directory or via the TEMPLATE_URLS environment variable.
The data directory is ./data by default but will change if you have your DATABASE_URI set to a different folder and are using SQLite.
In the docker container, this will be at /app/data/templates. Templates provided here will show above the "built-in" templates.
TEMPLATE_URLS is a JSON format array of URLs that point to template JSON files. These are fetched at startup and refreshed periodically.
Users can then choose from these templates within the AIOStreams interface. Additionally, users can import templates via the template browser from:
- A URL pointing to a template file or an array of template objects.
- A local template file on their device.
You can send a user directly to the template import flow by appending query parameters to any AIOStreams URL. When the page loads, the template modal opens automatically and begins importing from the provided URL.
| Parameter | Required | Description |
|---|---|---|
template |
Yes | URL pointing to a template JSON file (single object or array of objects). |
templateId |
No | The metadata.id of a specific template to auto-select from the fetched file. |
https://your-aiostreams-instance.com/stremio/configure?template=https://example.com/my-template.json
With a specific template pre-selected from a multi-template file:
https://your-aiostreams-instance.com/stremio/configure?template=https://example.com/templates.json&templateId=author.my-template
Security note: Users are shown a warning when a template is imported via deep link. Remind users in your documentation to only import templates from sources they trust — deep links can come from anywhere.