Writing a Niles add-on
What an add-on is, what it can and cannot reach, and the capabilities at a glance.
A third-party add-on is what Niles calls a plugin in the app: something
somebody else wrote, installed from GitHub. The technical names keep the old
word, because plugins already built depend on them: the WIT package is
niles:integration, the manifest niles-integration.toml and the component
integration.wasm. For how a household installs and manages one, see
Add-ons and accounts.
v0.3 of the interface, built. Niles 0.2.0 runs plugins (the sandbox, every host interface, devices, tools, timers, events, pages, MQTT, and from Niles 0.3.0 accounts and webhooks) and installs them: Add-on from GitHub with its approval screen, update checks, updates that show what changes in permissions and roll back by themselves if the new version fails, turning off and removing, and the
niles plugin checkandniles plugin devcommands. The app draws everything a plugin declares: its setup panel generated from the manifest's settings, its log, its pages in the menu and its widgets on the front page and the rooms. Not built yet:niles plugin dev --live, which would run a plugin on a running Niles, and--http, which would serve its webhook locally. The interface (schemas/niles-integration.wit) is v0.3: v0.1 plus MQTT (0.2), the house's time zone and lights kept out of their room's (0.2.1), and connecting accounts and webhooks (0.3). Niles still runs plugins built for 0.2 and 0.1. See versioning for what may change.
Niles is an open-source home assistant, written in Rust, that you talk to. It runs the lights, music, heating and calendars of one house, and it hands questions it cannot answer from fixed phrases to a language model with tools. Its built-in add-ons live in its own repository. This guide is for everything else: the thing in your house, or the service you use, that Niles does not know about.
You write the add-on in Rust, JavaScript, Go or Python, compile it to a WebAssembly component, publish it as a GitHub release, and anyone running Niles installs it by pasting your repo's URL into the app.
The three terms you need
- A WebAssembly component is a compiled
.wasmfile that also says, in its own metadata, which functions it needs from its host and which functions it provides. It runs in a sandbox: it can do nothing the host does not hand it. - WIT (WebAssembly Interface Type) is the language those declarations are
written in. Niles's is one file,
niles-integration.wit: packageniles:integration@0.3.0, worldplugin. A world is the full contract, what you import and what you export. You do not write WIT; you read it, and a bindings generator turns it into types and functions in your language. cargo-componentandwit-bindgenare the Rust tools that do that generation.wit-bindgen'sgenerate!macro reads the WIT and writes the Rust types, the functions you call (Niles's) and a trait you implement (yours). Rust'swasm32-wasip2target then builds a component directly.cargo-componentwraps the same steps in acargosubcommand; the template uses plaincargowith the target.
What you can build
- Devices from a cloud or local API. An inverter, a heat pump, a pool pump, a smart plug with an HTTP API, a weather station on the LAN. They appear in their rooms in the app, can be switched by voice ("turn off the pool pump"), and rules can follow them ("when the garage door opens after sunset…").
- Tools for the assistant. "When is power cheapest tomorrow?", "When's the next bus?", "How full is the car's battery?" You describe a tool with a JSON Schema; the voice model and the Claude and ChatGPT assistants decide when to call it.
- Pages and widgets in the app. A page in the menu, or a card on the front page or a room, drawn from a small set of Niles's own components: readings, lists, charts, buttons, switches, sliders.
- Spoken notifications. "The washing machine is done", through the same chime, quiet hours and recall as Niles's own.
- Reactions to the house. Subscribe to device changes, button presses and whether anyone is home.
- Devices that speak MQTT. A 3D printer, Tasmota and Shelly plugs, the household's own broker: Niles holds the connection and hands your plugin each message.
What you cannot do
A plugin runs in a sandbox. It cannot read Niles's database, other add-ons' passwords, conversations, memory or the people in the house. It has no filesystem, no environment variables and no sockets; the only network access is HTTP to the hosts your manifest declares, and MQTT to the brokers it names. It cannot put HTML or script into the app. See permissions.
Where it shows up
Once installed, a plugin sits on the app's Add-ons page (Settings → Add-ons)
among the built-ins, in the group its manifest names (Music & TV, Presence & climate…),
or under Custom if it names none. Its row carries a Custom badge, and
its panel shows your repo, version, authors and install date, a status line
("Working", "Not set up: needs API key", "Not working: …", "Off", "Update
available: v0.2.0"), the settings form generated from your manifest, where
each of your widgets goes, the last lines of its log (yours, and what Niles
noted about it), and the buttons to turn it off, check for updates, update,
roll back and remove it. Your pages join the menu, where the household shows,
hides and moves them in Settings → Menu like the built-in ones. People find
Add-on from GitHub at the end of the Available tab, and your add-on on the
Browse tab once your repo has the topic niles-addon
(publishing).
Capabilities at a glance
| You want to | Use | Permission | Guide |
|---|---|---|---|
| Read your settings and secrets | config.get, config.secret | none | manifest |
| Know the house's time zone | config.time-zone (0.2.1) | none | host API |
| Keep data between restarts | storage (1 MiB) | none | host API |
| Call an API on the internet | http.send | http = ["api.example.com"] | host API |
| Call a device on the LAN | http.send | local_network = ["setting:address"] | permissions |
| Run something every N seconds | timers.every → on-timer | none | host API |
| Add lights, plugs, sensors, covers | devices.upsert, report-state | devices = true | devices |
| Be switched by voice and rules | export set-device | devices = true | devices |
| Start rules when something happens on a device | devices.report-action | devices = true | devices |
| Keep a light out of its room's lights | devices.set-room-light (0.2.1) | devices = true | devices |
| Answer questions, do things on request | exports tools, call-tool | tools = true | tools |
| Add a page to the menu | [[ui.menu]], exports render, on-action | ui = true | UI |
| Add a widget to the front page or a room card | [[ui.widgets]], same exports | ui = true | UI |
| Speak through the satellites | notify.say | notify = true or "urgent" | host API |
| React to other devices or presence | events.subscribe → on-event | events = [...] | host API |
| Talk to an MQTT device or broker | mqtt.connect, subscribe, publish → on-mqtt-message, on-mqtt-status | mqtt = [{ name, host, tls }] (0.2) | MQTT |
| Connect an account through its sign-in page | exports authorize-url, on-callback | callback = true (0.3) | accounts |
| Keep a token you refreshed, encrypted | secrets.get, set, delete | none (0.3) | host API |
| Be called by a service that pushes | inbound.hook-url → export on-webhook | webhook = true (0.3) | accounts |
| Log | log.log | none | host API |
Not yet: raw TCP or UDP, WebSockets, and a page that runs your own HTML. See host API.
The guide
- Getting started: template, build, run locally, release, install
- The manifest: every field of
niles-integration.toml - Host API: every import and export, with examples
- Devices: classes, state fields and units, controllable devices, naming
- Tools: designing tools the AI uses well
- Pages and widgets: the declarative UI
- MQTT: brokers,
trust-devicecertificates, a printer as the example - Accounts and webhooks: signing in to a service, keeping its tokens, being called by it
- Permissions: what each one allows and how the household sees it
- Publishing: release layout, the add-on library, versioning, updates
- Worked example: spot prices: a complete plugin in Rust, with the same tool in JavaScript and Python
Why plugins are WebAssembly components, and what the sandbox does and does not promise, are in ARCHITECTURE.md's Big decisions and Security notes.