Skip to content

Getting Started for Creators ​

azphalt packages portable host-facing functionality and data into one signed .azp format. That includes sandboxed code extensions, asset packs, mixed packages (code plus the assets it uses), companion apps, MCP servers, packs, skills, scripts, composables, and declarative workflow and role packages. A further kind, llm (off-device language models), is proposed in the llm spec and not yet accepted by the tooling.

The important distinction is not "plugin versus not-plugin"; it is the package kind, because each kind has its own trust/runtime rules. Workflow and role packages, for example, are signed declarative data and never receive the code-extension sandbox's capabilities.

The fastest start for scaffolded package kinds is:

bash
npm create azphalt@latest

It asks for a project name, a namespace (used for the package id), an author and a licence, then what you're building, and drops a ready-to-run project in place. Every project also gets a publisher signing key and a signing workflow; see the create-azphalt readme for what they are and why the key must stay out of git.

Creating a Code Extension ​

Pick Code Extension in the scaffolder. You get a working starter — a brightness filter and a crossfade transition — that you edit, test locally, and build:

bash
npm create azphalt@latest      # name it my-extension, choose "Code Extension"
cd my-extension
npm install
npm test                       # runs your contributions against the reference runtime
npm run build                  # → my-extension-1.0.0.azp
  • code/main.js holds your code — one export per contribution, each branded with defineFilter / defineTransition / defineTool / defineCommand from @azphalt/azdk.
  • manifest.json declares your capabilities — the only surface your code can reach (the host grants exactly what you list and denies the rest). Ask for the least you need; you can never reach the camera, sensors, the filesystem, the network, or the host's engine. See the Capability model.
  • npm test dispatches your extension against @azphalt/runtime-reference in memory, so you iterate without a host app.
  • To scope an extension to one app (e.g. your own), add "targetApps": ["com.the.app"] to the manifest.

When it's ready, submit it as source via a pull request under submissions/<your-id>/ (see the repo's submissions/README.md and the code submission template) — CI re-packages and validates it.

Creating an Asset Pack ​

Pick Asset Pack in the scaffolder for a workspace that bundles your files into a .azp. Or, to package a single asset quickly, use one of the dedicated CLI importers.

Packaging a Video Overlay ​

  1. Ensure you have Node.js installed.
  2. Run the video importer on your asset:
    bash
    npx @azphalt/importer-video my-explosion.webm explosion-pack.azp
  3. Your explosion-pack.azp is now ready to be uploaded to any azphalt repository or dragged-and-dropped directly into compatible host apps!

Available CLI Importers ​

  • @azphalt/importer-abr: Convert Adobe Photoshop Brushes
  • @azphalt/importer-cube: Convert 3D LUTs for color grading
  • @azphalt/importer-isf: Convert Interactive Shader Format shaders
  • @azphalt/importer-gltf: Package 3D meshes
  • @azphalt/importer-hdri: Package Environment lighting maps
  • @azphalt/importer-material: Package PBR textures (Albedo, Normal, Roughness)
  • @azphalt/importer-motion: Package easing curves
  • @azphalt/importer-palette: Package Adobe Swatch Exchange files
  • @azphalt/importer-image: Package standard decals and textures
  • @azphalt/importer-video: Package footage and VFX loops
  • @azphalt/importer-font: Package typography
  • @azphalt/importer-audio: Package SFX and music stems
  • @azphalt/importer-transition: Convert GL Transitions (gl-transitions.com) into transition assets
  • @azphalt/importer-vector: Package vector graphics
  • @azphalt/importer-onnx, @azphalt/importer-tflite, @azphalt/importer-litert, @azphalt/importer-sherpa: Package on-device ML models

The CLIs take different arguments; the importers readme lists each one's usage.

Publishing to azphalt.store ​

Once you have a valid .azp, there are two ways to get it onto the azphalt.store marketplace so apps can discover and install it:

  1. Submit as source (open lane). Open a pull request adding your project under submissions/<your-id>/ (see the repo's submissions/README.md and the code / asset / model templates). CI re-packages and validates it, then it lands in the free registry. Best for free, open extensions that live in the open registry.

  2. Publish the signed bytes. Send the .azp, signed with your publisher key, to POST https://azphalt.store/packages (Repository API § 9) — or use the store's Publish page:

    bash
    curl -X POST -H "Content-Type: application/vnd.azphalt.package" \
      --data-binary @my-extension.azp https://azphalt.store/packages

    The store verifies the container and the signature, then opens that same pull request under submissions/<your-id>/ for you and answers 202 with its address. Your first publish of an id pins your key; every later version must be signed by the same key, or it is refused. A package already maintained in the repository by hand is updated by pull request, not here. Up to 4 MB — put heavy assets behind remoteUrl + checksum.

Either path ends in the same reviewed pull request and the same checks; nothing reaches the store without a merge, and a listing on the store is never a lower bar than an open submission.

Building an MCP server? An MCP server is packaged as its own package kind (kind: "mcp") and published exactly like any other extension — there is no separate MCP publish protocol. The .azp is a signed header describing how a host reaches the server; the host runs it under its own trust prompt.

Selling your extension (the consignment lane) ​

The registry lane is free and fee-free by design. If you want to charge for an extension, you consign it onto the paid lane — the only place a fee exists:

  • You onboard once (Stripe Connect Express), and the store routes payouts to your account.
  • You set a one-time price or a subscription; a buyer's purchase mints a signed entitlement token their host app presents to download (see Use the Store § paid packages).
  • The store shows the buyer an honest split — gross → processor fee, platform fee → your net — and the fee never leaks into the open registry. See Architecture § The marketplace and Marketplace Integrity.

Your extension's bytes and manifest are identical whether it's free or paid — consignment is an overlay on a normal package, not a different format. You can list a package for sale without changing a line of its code.

Bundling an extension pack ​

An extension pack (kind: "pack") is a curated set of packages published as one — a recommended bundle, or a base set your app installs for a new user. Scaffold one with npm create azphalt@latest → Extension Pack, and edit the pack.entries in its manifest.json:

jsonc
{
  "kind": "pack",
  "targetApps": ["com.example.myapp"],   // scope it to your app (omit for a global pack)
  "pack": {
    "entries": [
      { "id": "com.foldlab.filmluts", "required": true,  "note": "core LUTs" },
      { "id": "com.other.author.brushes", "required": true },       // someone else's package — fine
      { "id": "com.hereliesaz.halftone", "required": false }        // recommended paid add-on
    ]
  }
}
  • A pack references packages by id — it never re-bundles them, so it can include other creators' extensions, and a paid member still needs its own purchase to install.
  • required: true is the base set (installed with the pack); omit it for recommended members.
  • Omit an entry's version to always track that member's latest; pin it for reproducibility.

Publish it like any package. A host installs it with @azphalt/repository-client's resolvePack(id), then downloads each member.

Ship it pre-installed, too. Since an .azp is just a signed archive, you can bundle the members' bytes in your app and load them at startup with readAzp — no network — using the pack as the "what to include" list. Pack = the which extensions; your app picks bundle-now vs. fetch-on-first-run. See the pack spec.

Building a Skill ​

A skill (kind: "skill") bundles one or more Agent Skills — SKILL.md plus optional scripts/, references/, and assets/ — for an AI-agent host to load, the same way kind: "mcp" packages an MCP server: a signed header the host reads, not sandboxed code an azphalt runtime executes. Scaffold one with npm create azphalt@latest → Skill:

jsonc
{
  "kind": "skill",
  "skill": {
    "skills": [
      { "id": "release-notes", "name": "Release Notes", "description": "Draft release notes from a range of commits." }
    ]
  }
}
  • Each entry's id names a directory in the package: skills/<id>/SKILL.md (plus its scripts/ / references/ / assets/). name / description mirror the SKILL.md frontmatter and power the store card — a host that parses SKILL.md treats it, not this block, as authoritative.
  • A skill package carries no capabilities and no /code entry/runtime — it's instructional text and support files an agent reads, not code an azphalt runtime executes.
  • To bundle more than one skill, add another skills/<id>/SKILL.md directory and a matching entry in skill.skills.

Publish it like any other package. See the skill spec.

Building a Script ​

A script (kind: "script") is a native script — bash, Python, PowerShell, whatever — that a host installs and runs the way a package manager installs its own package: declare what it needs, and the host resolves that before making the script runnable. It's not kind: "code" — a script needs real filesystem/process access to do anything useful, the same reason an MCP server can't be a sandboxed extension either, so it gets its own kind rather than a "native" flavor of code. Scaffold one with npm create azphalt@latest → Script:

jsonc
{
  "kind": "script",
  "script": {
    "interpreter": "bash",
    "entry": "script/git-sync.sh",
    "command": "git-sync",
    "dependencies": { "apt": ["git", "openssh"] }
  }
}
  • interpreter and entry are required; entry is the real, integrity-covered script payload — not a pointer to something launched elsewhere.
  • command is what the script should be callable as once installed; omit it and the host picks a reasonable default.
  • dependencies is optional and namespaced by package manager (apt, brew, …) — omit it entirely for a script needing nothing beyond what the host already guarantees. When the interpreter itself might be missing, list its package here too: dependencies: { "apt": ["python3"] } alongside interpreter: "python3" is expected, not redundant — interpreter says what runs the script, dependencies says what to install first.
  • A script package carries no capabilities and no /code entry/runtime — dependency resolution and execution happen at the host's own OS/package-manager layer, never as an azphalt editor capability.

Publish it like any other package. See the script spec.

Building a Composable ​

A composable (kind: "composable") describes one or more UI elements for a host's own already-compiled renderer to build — never code an azphalt runtime executes. It selects a templateId from a template library your host app links at build time (e.g. a Gradle implementation("group:artifact:x.y.z") dependency), the same way kind: "mcp"/kind: "app" each package a signed header rather than sandboxed code. Scaffold one with npm create azphalt@latest → Composable:

jsonc
{
  "kind": "composable",
  "composable": {
    "library": { "group": "com.hereliesaz.conveyance", "artifact": "conveyance-m3-expressive", "version": "1.4.0" },
    "elements": [
      {
        "id": "confirm-record",
        "templateId": "m3e.tile.record",
        "hue": "azure",
        "surface": "recordTile",
        "scale": "lead",
        "act": "create",
        "jobs": ["confirms a destructive action", "shows the record being created"]
      }
    ]
  }
}
  • library is purely descriptive — azphalt never fetches or resolves it. A templateId a host never linked simply fails to resolve at render time: a missing-dependency error, not a security question, since nothing was ever downloaded or executed on the host's behalf.
  • hue, surface, scale, and act are your host's own design-token vocabulary (open strings) — azphalt validates only that they're non-empty, never what they mean.
  • jobs is a checkable declaration of what the element is actually for — write down its real purpose so a reviewer (or you, later) can check the element against its own claim.
  • A composable package carries no capabilities and no /code entry/runtime — and unlike skill/script it bundles no payload file at all; every field is inline in the manifest.

Publish it like any other package. See the composable spec.

Building a Workflow Package ​

A workflow package (kind: "workflow") carries signed orchestration data for a workflow-aware host. It does not ship executable runtime/UI code. The host decides whether it understands the declared workflow.format, resolves dependencies, asks the user about any symbolic hostPermissions, and renders any declarative screens with its own compiled UI.

A minimal manifest looks like:

jsonc
{
  "kind": "workflow",
  "targetApps": ["com.example.workflowhost"],
  "workflow": {
    "format": "example.workflow.v1",
    "definitions": [
      { "id": "release", "path": "workflows/release.json" }
    ],
    "hostPermissions": ["WorkflowRegister", "WorkflowLaunch"]
  },
  "files": {
    "workflows/release.json": "sha256-…"
  }
}

targetApps is discovery scoping, not authority. A matching host still verifies the package and makes its own permission/execution decision. Dependencies keep their own signatures, entitlements, permissions, and update lifecycle.

See the workflow package spec.

Building a Role Package ​

A role package (kind: "role") carries declarative persona/role definitions for a host that supports that role format. Like workflow packages, it contains no downloaded executable code and receives no Azphalt sandbox capabilities.

jsonc
{
  "kind": "role",
  "targetApps": ["com.example.workflowhost"],
  "role": {
    "format": "example.role.v1",
    "roles": [
      { "id": "researcher", "name": "Researcher", "path": "roles/researcher.json" }
    ]
  },
  "files": {
    "roles/researcher.json": "sha256-…"
  }
}

Installing a role package does not grant that role authority. The host decides how the payload maps into its own role/company model and what permissions a resulting host-owned role may later receive.

See the role package spec.

What's Next? ​