Create an Extension — .asx Pack Author Guide
Complete authoring guide for Anisurge .asx extension packs — declarative JSON pipeline authoring, step types, variable templates, conditional branching, validation, and publishing workflow.
Create an Extension — Author Guide
This guide walks through creating a new .asx extension pack for the Anisurge app. Packs are declarative JSON pipelines — there is no TypeScript, JavaScript, Kotlin, or any compiled runtime in the pack file. The Anisurge app's AsxPipelineRunner executes the steps natively.
Prerequisites
- Anisurge build ≥ 140 (for testing installations)
- GitHub account (for submitting packs to the official catalog)
- Browser developer tools or curl ability to inspect target website HTML, JSON APIs, and network requests
- Node.js 20+ (for running validation scripts)
- Understanding of JSON, regular expressions, and HTTP request/response flow
Step 1: Clone the Repository & Copy Boilerplate
git clone https://github.com/Anisurge/extensions.git
cd extensions
cp -r boilerplate/my-extension extensions/your-source-name
mv extensions/your-source-name/my-extension.asx extensions/your-source-name/your-source-name.asxThe boilerplate provides a minimal valid .asx structure:
{
"schemaVersion": 1,
"id": "my-extension",
"name": "My Extension",
"version": "0.1.0",
"versionCode": 1,
"pipeline": {
"resolve": []
}
}Step 2: Pack Metadata Fields
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
schemaVersion | integer | Yes | Schema version; must be 1 | Must equal AsxOfficial.SCHEMA_VERSION |
id | string | Yes | Unique identifier | Regex [a-z0-9-]+, must match folder name |
name | string | Yes | Human-readable display name | Used in UI lists |
version | string | Yes | Display version string | e.g. 1.2.0, shown in catalog |
versionCode | integer | Yes | Monotonic version for updates | Must increase on changes |
language | string | No | Language code | Default all |
nsfw | boolean | No | NSFW content flag | Default false, shows warning label |
description | string | No | Short description of the source | Displayed in catalog cards |
author | string | No | Creator name or handle | Shown in pack metadata |
homepage | string | No | Project homepage URL | |
iconUrl | string | No | 48×48 PNG icon URL | Displayed in install dialog and catalog |
minAppVersionCode | integer | No | Minimum app build required | Prevents install on incompatible versions |
capabilities | object | No | Feature flags | See below |
defaults.headers | object | No | Default HTTP headers | Merged into every http.get step |
pipeline.resolve | array | Yes | Pipeline step list | At least one step, must end with map.videos |
Capabilities Object
{
"capabilities": {
"subDub": true,
"resolveBy": ["anilistId", "malId"]
}
}subDub— Settrueif the source provides separate sub and dub streams. The app shows a language toggle.resolveBy— Specifies which identifiers the pipeline can resolve from. Default is["anilistId"].
Step 3: Runtime Context Variables
The app injects these context variables before the pipeline runs. Use them in step URLs and templates with the {{variable}} syntax:
| Variable | Type | Description | Example |
|---|---|---|---|
{{anilistId}} | integer | AniList media ID | 21 |
{{malId}} | integer | MyAnimeList ID (0 if unknown) | 35247 |
{{episode}} | integer | Episode number to resolve | 12 |
{{lang}} | string | Language preference | sub or dub |
{{title}} | string | Best-effort anime title | One Piece |
Intermediate Variables
Pipeline steps can save results into named variables using the saveAs field. These can then be referenced in subsequent steps:
{{pageHtml}} — Raw HTML from an http.get step
{{sources}} — Parsed JSON object from json.parse
{{sources.sources.file}} — Nested value from a json.get path
{{streamUrl}} — Extracted stream URL stringStep 4: Pipeline Steps Reference
http.get — Fetch Remote Content
Fetches a URL and saves the response body text to a variable. Supports custom headers merged from defaults.headers.
{
"step": "http.get",
"url": "https://example.com/api/{{anilistId}}/{{episode}}",
"headers": {
"Accept": "text/html",
"Referer": "https://example.com/"
},
"saveAs": "pageHtml",
"optional": false
}| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Target URL with optional {{variable}} templates |
headers | object | No | Per-request headers, merged on top of defaults.headers |
saveAs | string | Yes | Variable name to store the response body |
optional | boolean | No | If true, non-2xx responses don't fail the pipeline |
extract.regex — Regex Extraction
Extracts a substring using a JavaScript-compatible regular expression pattern and capture group.
{
"step": "extract.regex",
"from": "pageHtml",
"pattern": "data-id=\"(\\d+)\"",
"group": 1,
"saveAs": "dataId",
"onlyIfMissing": "dataId"
}| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source variable containing the text |
pattern | string | Yes | Regex pattern (escape backslashes for JSON) |
group | integer | No | Capture group index (0 = full match, 1 = first group) |
saveAs | string | Yes | Variable to store the matched text |
onlyIfMissing | string | No | Only run if this variable is empty/absent (deduplication) |
json.parse — Parse JSON Text
Parses a JSON-formatted string variable into a structured object for subsequent navigation.
{
"step": "json.parse",
"from": "sourcesBody",
"saveAs": "sources"
}| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source variable containing JSON text |
saveAs | string | Yes | Variable to store the parsed JSON object |
json.get — Navigate JSON Structure
Retrieves a value from a parsed JSON object by dot-separated path.
{
"step": "json.get",
"from": "sources",
"path": "sources.0.file",
"saveAs": "streamUrl"
}| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Variable containing a parsed JSON object |
path | string | Yes | Dot-separated path (array indices supported as 0, 1, etc.) |
saveAs | string | Yes | Variable to store the retrieved value |
map.videos — Produce Stream Output
The final step in every pipeline. Maps resolved variables into video stream objects for the mpv player.
{
"step": "map.videos",
"file": "{{sources.sources.0.file}}",
"quality": "{{sources.sources.0.label}}",
"headers": {
"Referer": "https://example.com/",
"User-Agent": "Mozilla/5.0"
},
"subtitles": {
"from": "sources.tracks",
"urlKey": "file",
"labelKey": "label"
},
"intro": "sources.intro",
"outro": "sources.outro"
}| Field | Type | Required | Description |
|---|---|---|---|
file | string | Yes | Final video stream URL (HLS .m3u8 or direct MP4) |
quality | string | No | Display label for quality (e.g. Auto, 1080p, 720p) |
headers | object | No | Headers required for playback (Referer, User-Agent, etc.) |
subtitles | object | No | Subtitle track configuration |
intro | string | No | Variable path for intro end timestamp (seconds) |
outro | string | No | Variable path for outro start timestamp (seconds) |
Subtitles Object
| Field | Type | Description |
|---|---|---|
from | string | Source variable containing subtitle tracks array |
urlKey | string | Key in each track object for the subtitle file URL |
labelKey | string | Key in each track object for the display label (e.g. English, Spanish) |
try — Conditional Fallback Branches
Allows ordered fallback strategies. Each branch is tried in sequence; the first branch that completes without error wins. Branches can have optional when conditions.
{
"step": "try",
"branches": [
{
"name": "resolve via AniList",
"when": { "op": "gt", "variable": "malId", "value": 0 },
"steps": [
{ "step": "http.get", "url": "https://api.example.com/anilist/{{anilistId}}/{{episode}}", "saveAs": "pageHtml" },
{ "step": "extract.regex", "from": "pageHtml", "pattern": "src=\"(https:[^\"]+\\.m3u8)\"", "group": 1, "saveAs": "streamUrl" }
]
},
{
"name": "fallback to MAL",
"steps": [
{ "step": "http.get", "url": "https://api.example.com/mal/{{malId}}/{{episode}}", "saveAs": "pageHtml" },
{ "step": "extract.regex", "from": "pageHtml", "pattern": "src=\"(https:[^\"]+\\.m3u8)\"", "group": 1, "saveAs": "streamUrl" }
]
}
]
}When Conditions
| Field | Type | Description |
|---|---|---|
op | string | Operator: gt (greater than), eq (equals), exists (truthy check) |
variable | string | Variable name to check (e.g., malId) |
value | number/string | Comparison value (required for gt and eq) |
Step 5: Complete Example — Anokoto Pipeline
{
"schemaVersion": 1,
"id": "anokoto",
"name": "Anokoto",
"version": "1.0.1",
"versionCode": 2,
"language": "all",
"nsfw": false,
"description": "MegaPlay HLS + softsubs from AniList or MAL",
"author": "Anisurge",
"capabilities": { "subDub": false, "resolveBy": ["anilistId", "malId"] },
"pipeline": {
"resolve": [
{ "step": "http.get", "url": "https://api.anokoto.example/{{anilistId}}/{{episode}}", "saveAs": "pageHtml" },
{ "step": "extract.regex", "from": "pageHtml", "pattern": "data-sources='([^']+)'", "group": 1, "saveAs": "sourcesJson" },
{ "step": "json.parse", "from": "sourcesJson", "saveAs": "sources" },
{ "step": "map.videos", "file": "{{sources.file}}", "quality": "Auto", "headers": { "Referer": "https://example.com/" }, "subtitles": { "from": "sources.tracks", "urlKey": "file", "labelKey": "label" } }
]
}
}Step 6: Validate Your Pack
Run the official validation script to check for common issues:
node scripts/validate.mjsThe validator checks:
- Valid JSON syntax
schemaVersionequals1idmatches[a-z0-9-]+patternversionCodeis a positive integer- Pipeline contains at least one step ending with
map.videos - Step fields match expected types
- No unknown fields
For index-level validation (after adding to index.json):
node scripts/generate-index.mjs # rebuilds index.json from extensions/
node scripts/validate.mjs # validates both packs and indexStep 7: Test Installation
- Host your
.asxover HTTPS (raw GitHub URLs work well for testing) - Open the install deep link:
anisurgex://extensions/install?type=source&engine=ANISURGE&url=<your_asx_url>&name=<pack_name>&version=<version>- Confirm installation in the dialog
- Play a test title — pick a known anime with a confirmed AniList ID
- Check output — verify the video loads, audio plays, subtitles appear if provided
Step 8: Submit to Official Catalog
-
Create your pack folder under
extensions/<id>/with:<id>.asx— the pack fileicon.png— 48×48 icon (optional but recommended)README.md— usage notes (optional)
-
Add to
index.json— runnode scripts/generate-index.mjs -
Open a pull request on github.com/Anisurge/extensions
PR Checklist
- One extension per PR
-
versionCodebumped on behavior changes -
index.jsonregenerated and committed - No secrets, API keys, or private tokens in the pack
-
nsfw: trueset when appropriate -
.asxpassesvalidate.mjs - Tested with a real episode in the app
Best Practices
- Prefer
trybranches for resilience — try AniList first, fall back to MAL - Keep HTTP requests minimal — each
http.getadds latency to playback startup - Use
onlyIfMissingon regex steps to avoid redundant extractions - Set
optional: trueon non-critical HTTP requests to avoid pipeline failure - Include provider headers (Referer, User-Agent) that match browser behavior
- Version codes are monotonic — never decrease
versionCode - Test both sub and dub if
capabilities.subDubis set - Handle rate limiting — some providers block rapid sequential requests
Add Extension Repository — Install Guide
Step-by-step guide to add the official Anisurge extensions repository, install .asx packs, and configure streaming sources for anime playback in the Anisurge app.
.asx Schema Reference — Complete Field and Step Documentation
Complete technical reference for the Anisurge .asx extension schema version 1 — top-level fields, pipeline steps (http.get, extract.regex, json.parse, json.get, map.videos, try), variable templating, and index.json format.