Manifest
The timon field in package.json declares the plugin contract. The npm package uses type: "module"; main points to host code, when present, and exports["./client"] points to the browser entry.
| Field | Contract |
|---|---|
id | Required; 3–40 lowercase letters, digits or hyphens in the current third-party validator; hooks is reserved |
apiVersion | Required positive integer; minimum supported API contract |
kind | One kind or a nonempty list; inferred from slots when absent |
label, description, author | Optional nonempty display strings |
cover | Optional package-relative image path, not a remote URL |
icon | Optional package-relative SVG: the picto of the gallery card, painted as a mask in the theme's text colour (only its shape counts; a 24×24 line drawing reads best). Not a remote URL |
screenshots | Additional package-relative images; default [] |
permissions | Requested capabilities; default [] |
client.slots | Admitted UI surfaces; default [] |
client.external | External module declarations; default [] |
client.mode | esm by default; schema also accepts iframe |
declares | Slots opened for other components, mapped to single, list or keyed |
route | Page route; required for a page, forbidden otherwise. Agents find it with their ui_routes tool, titled with your label, for the users who can open your plugin |
modifies | Informational list of affected surfaces; default [] |
contains | Child manifest IDs for bundles only |
settings | Declarative fields: key, type, label, optional default |
webhooks | Incoming webhooks: name (lowercase slug) and auth, the sender's signature format — standard (default), github, gitea, forgejo, gitlab, stripe, slack, shopify, linear, discord or meta. Any value other than standard needs apiVersion: 8. Needs webhooks:receive (Receiving webhooks) |
license.required | Whether the licence must explicitly permit the plugin; default false |
Slot names have lowercase alphanumeric dot-separated segments beginning with a letter. Routes start with /, use lowercase alphanumeric or hyphenated segments and contain no query or traversal. Covers and screenshots are confined to the package and accept PNG, WebP, JPG, JPEG or SVG paths.
Unknown manifest keys are ignored. Known fields with invalid values, incompatible kinds/slots or an API version newer than the daemon are refused. Use the real templates' ESM mode for browser plugins; an accepted schema option alone does not demonstrate an alternate runtime integration.
Shared browser modules
Keep these imports external to a browser bundle:
react
react-dom
@timon/ui
@timon/api
@timon/theme
@timon/navigate
For esbuild, use format: 'esm', bundle: true and an external array containing those exact specifiers. Do not embed another React instance. Include the built browser file in the published package: installation does not run lifecycle scripts.
Host entry
Export apply(ctx) and register routes or effects through the context. ctx.router() mounts routes beneath /api/plugins/<encoded-id>/ and removes them when the plugin unloads. The /-/ path prefix is reserved for platform endpoints. The router is an Express router. A handler may be async: if it throws or its promise rejects, Timon logs the error under your plugin's id and answers 500 {"error":"internal_error"} without the error's message, and the daemon keeps running. An error carrying a 4xx status keeps that status. Catch the errors you can explain and answer them yourself: the generic 500 tells your user nothing.
Outside a route, the same holds for a promise nobody awaits (void doWork()), an async timer callback or an event listener that throws: the daemon logs the error under your plugin's id, keeps running, and posts one notification naming your plugin (at most once an hour, without the error's text). A listener of ctx.events.on that throws does not stop the other listeners of the event. Repeats of the same error within a minute are counted rather than logged one by one. This is a safety net, not error handling: the work that failed did not happen, so catch what you can recover from.
If you export inject, it may name only kanban, missions, notify, storage, settings, events, http and host. It may not name db, bus or another internal service. Requested permissions are separate from dependency injection.