Skip to content

Manifest schema

manifest.json is decoded and validated before your JavaScript is evaluated. Unknown fields inside the structured blocks are rejected rather than ignored, so a typo is a load failure and not a silently missing feature.

Top level

FieldRequiredTypeNotes
idyesstringstable reverse-DNS; the principal every check is made against
nameyesstringshort directory-style name
versionyesstringyour own version string
permissionsyesstring[]see Permissions
intentsyesobjectrequired even when both arrays are empty
runtimeno"javascript" | "bundled-swift"defaults to javascript
displayNamenostringwhat a person sees
iconnostringSF Symbol name
settingsnoarraydeclarative settings UI
networknoobject{ "allow": [host…] }
automationnoobject{ "schedules": [...] }
eventsnoobject{ "publishes": [], "observes": [] }

runtime: bundled-swift is refused in a user inventory

It is reserved for implementations compiled into Tenon's own sealed app inventory. It is not a native plugin SDK. Omitting runtime deliberately defaults to javascript.

intents

json
{
  "intents": {
    "uses": ["process.exec.v1"],
    "provides": [ /* provisions */ ]
  }
}
FieldTypeNotes
usesstring[]every intent you send; sending an undeclared one fails
providesarraycontracts you serve

A provision

FieldRequiredType
nameyesstring — prefix with your plugin id, versioned .v1
titlenostring
descriptionnostring
audiencesnostring[] — user makes it palette-invocable
effectsnoobject — see below
inputSchemanoJSON Schema
outputSchemanoJSON Schema
errorsnostring[] — your own domain error codes
palettenoobject — see below

effects

FieldValues
kindread, write, destructive
idempotencywhether repeating it is safe
confirmationnever, policy, always
externalboolean — does it leave the machine

palette

FieldTypeNotes
categorystringgroups the row
iconstringSF Symbol name
keywordsstring[]extra match terms
keystringa keybinding
whenstringa condition for showing it
launcherbooleandefault false
fillsPanebooleandefault false

launcher marks a creation verb — something that opens a terminal, a view, an agent. The tab strip's + offers exactly these; the palette still offers everything. It defaults to false so a destructive or navigational command never volunteers itself under a plus sign.

fillsPane declares that the command can occupy a pane supplied by its invocation scope. Empty-grid launchers project only these, because the click already chose a destination — tab and split structure commands cannot satisfy that action.

settings

json
{
  "settings": [
    { "key": "repoPath", "label": "Repository path", "type": "string", "default": "~" },
    {
      "key": "mode",
      "label": "Mode",
      "type": "select",
      "default": "compact",
      "options": [
        { "value": "compact", "label": "Compact" },
        { "value": "full",    "label": "Full" }
      ],
      "group": "Display"
    }
  ]
}
FieldRequiredNotes
keyyeswhat tenon.settings.get reads
labelyesshown in Settings — not title
typeyesstring, boolean, number, select
defaultnothe initial value
optionsfor select{ value, label }[] — the stored value is the value string
groupnosection heading; omitted groups it into an unnamed leading section

A select that omits options is a plugin bug the UI degrades on, not a manifest decode failure.

network

json
{
  "permissions": ["network"],
  "network": { "allow": ["api.github.com", "*.example.com"] }
}

An exact host, or a wildcard covering subdomains. *.example.com does not match example.com. Matching is case-insensitive, and an empty or missing allowlist grants nothing.

automation

json
{
  "automation": {
    "schedules": [
      { "id": "tick", "every": "1m" },
      { "id": "morning", "daily": "09:00", "grace": "2h" }
    ]
  }
}
FieldRule
idunique per plugin, 1…64 bytes
every | dailyexactly one per schedule
every"<positive integer><s|m|h|d>", min 1m, max 7d
dailyzero-padded 24-hour "HH:mm", machine-local
graceoptional 1m7d; defaults to one interval for every, 6h for daily

At most 8 schedules per plugin. Validation is fail-closed at decode with strict unknown-field rejection. → Automations

events

json
{
  "events": {
    "publishes": ["index.changed"],
    "observes": ["dev.example.other/cache.changed"]
  }
}

publishes uses your local channel name — the host qualifies it as <your-id>/<name> on the way out, so a plugin can only publish under its own id. observes uses the fully qualified name.

Events

Complete example

json
{
  "id": "dev.example.notes",
  "name": "notes",
  "displayName": "Notes",
  "version": "0.2.0",
  "icon": "note.text",
  "permissions": ["filesystem.read", "filesystem.write"],
  "settings": [
    { "key": "root", "label": "Notes folder", "type": "string", "default": "~/notes" }
  ],
  "events": { "publishes": ["index.changed"], "observes": [] },
  "automation": { "schedules": [{ "id": "reindex", "every": "15m" }] },
  "intents": {
    "uses": ["filesystem.directory.list.v2", "filesystem.file.read.v1"],
    "provides": [
      {
        "name": "dev.example.notes.new.v1",
        "title": "New note",
        "description": "Creates a note and opens it.",
        "audiences": ["plugin", "user"],
        "effects": {
          "kind": "write",
          "idempotency": "none",
          "confirmation": "never",
          "external": false
        },
        "inputSchema": {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "type": "object",
          "properties": { "title": { "type": "string" } },
          "required": ["title"],
          "additionalProperties": false
        },
        "outputSchema": {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "type": "object",
          "additionalProperties": false
        },
        "palette": {
          "category": "Notes",
          "icon": "square.and.pencil",
          "keywords": ["note", "capture"],
          "launcher": true
        }
      }
    ]
  }
}