Manifest
Every field in manifest.json — the entries, the full contributes surface, and why most permissions are derived rather than written.
manifest.json is the only file Studio reads without executing your code. It declares what the plugin provides, and everything the install prompt shows is computed from it.
{
"manifestVersion": 2,
"id": "yourname.hello",
"name": "Hello",
"version": "1.0.0",
"publisher": "Your Name",
"description": "A starter plugin.",
"entries": { "studio": "main.js", "runtime": "runtime.js" },
"contributes": {
"blueprintNodes": ["yourname.hello.log"],
"runtimeCapabilities": ["store"]
},
"permissions": []
}Top-level fields
| Field | Required | Notes |
|---|---|---|
manifestVersion | yes | Always 2. Version 1 is rejected. |
id | yes | Namespaced: publisher.plugin-name, lowercase, at least one dot. |
name | yes | Display name. |
version | yes | Semver (1.0.0), optionally with a prerelease or build suffix. |
publisher | no | Shown in the plugin list. |
description | no | One line, shown at install. |
entries | yes | { studio?, runtime? } — package-relative ESM paths. At least one; both files must exist. |
contributes | no | Everything the plugin provides. Omitting a key is the same as an empty list. |
permissions | no | Author-declared privileged capabilities only — see below. |
Entry paths cannot be absolute or contain .., ., null bytes, ?, or #.
contributes
contributes is the single source of truth for what a plugin can do. Studio validates a project against it without running plugin code, and the install permission set is derived from it.
| Key | Type | What it declares |
|---|---|---|
blueprintNodes | string[] | Blueprint node types this plugin provides. |
widgets | string[] | Widget element types this plugin provides. |
widgetText | object | The props of each contributed widget that hold words a player reads. |
locales | object[] | Studio language packs — locales the plugin adds or fills. |
runtimeData | string[] | Plugin storage namespaces to publish with the game. |
runtimeCapabilities | string[] | Capability domains the runtime entry may use. |
sidecars | object[] | Native child processes shipped inside the author's game. |
buildDependencies | object[] | External binaries fetched and cached at build time. |
tests | string[] | Test types this plugin provides, for Run > Test. |
buildConfig | object[] | Build-configuration fields this plugin adds to the build dialog. |
externalLinks | string[] | Address patterns the runtime entry may open in the player's browser. |
network | string[] | Host patterns the runtime entry may fetch bytes from. |
reservedSaveIds | string[] | Save ids this plugin keeps for itself, left out of the player's save list. |
structs | object[] | The fields of the rows this plugin's nodes hand out. |
agentTools | object[] | Tools the studio entry offers AI agents, { name, write } each. See Agent tools. |
agentGuide | string | A Markdown file in the package, served to agents as this plugin's guide chapter. |
Every node type, widget type, storage namespace, sidecar id, build-dependency id, reserved save id and struct id must be prefixed with your plugin id. An unknown key under contributes is rejected rather than ignored.
blueprintNodes and widgets
Registering a type you did not declare throws at load time, on both entries. The declaration is what lets Studio tell an author before a build that a node their project uses has no runtime provider — instead of shipping a game with a node that does nothing.
widgetText
A contributed widget that shows words a player reads, such as a title or a caption, names the props that hold them, by widget type:
{
"contributes": {
"widgets": ["yourname.hello.badge"],
"widgetText": {
"yourname.hello.badge": [
{ "prop": "title", "label": "Title", "localized": { "zh": "标题", "ja": "タイトル" } },
{ "prop": "caption", "multiline": true }
]
}
}
}| Field | Notes |
|---|---|
prop | Required. The prop that holds the words. |
keyProp | The prop the translation key is kept in, when the author reads the words from one. <prop>LocalizationKey when absent. Studio writes it; the widget never reads it to draw. |
label | What Properties calls the words. The prop name when absent. |
localized | label per editor language, keyed by the editor's locale code, such as zh or ja. |
multiline | Whether the words may run over several lines. One line when absent. |
Studio handles each declared prop as it handles the words of its own Text and Button widgets. Properties shows them in a Content section with the choice of Direct or Translation key, the canvas draws a key's words, the words have rows in the translation table and in exports, the project checks report a missing translation or key, and the glyph check reads them. In the game, render receives the words already in the player's language, in the prop that was declared; the widget never resolves a key itself.
Both entries read this declaration: the studio entry's widget is edited by it, and the runtime entry's widget is drawn by it. Each type named must also be in contributes.widgets and have at least one entry. prop and keyProp are identifiers, cannot be runtimeTextOrigin or runtimeTextUnit, and cannot repeat a name within one widget. widgetText derives no install permission.
runtimeData
Plugin storage lives under the project's editor directory, which is never packaged. If your runtime entry needs data an author authored in Studio — a catalog, a lookup table — list those namespaces here and read them with app.game.data.readJson(namespace).
{ "contributes": { "runtimeData": ["yourname.hello.catalog"] } }The list is an explicit allowlist so editor-only plugin state cannot leak into a shipped game by accident. readJson is synchronous — the data travels with the pack — and returns null when the namespace was not declared, the project never wrote it, or the game predates the data being published. Degrade gracefully; do not assume authored data exists.
runtimeCapabilities
The thirteen capability domains, each mapping onto one namespace of app.game. An undeclared domain is absent from the object, not a method that throws. Declaring one without an entries.runtime is a manifest error.
{ "contributes": { "runtimeCapabilities": ["store", "events", "state.read"] } }store · events · state.read · state.write · saves.read · saves.write · ui.overlay · assets · locale · menu · story.compile · diagnostics · process.memory
The full model, what each one grants, and how environments narrow it further: Runtime API.
sidecars and buildDependencies
The two heavy declarations. A sidecar is a native child process that ships inside the games an author builds; a build dependency is an external binary Studio downloads, verifies, and caches at build time. Both are keyed by <platform>-<arch> and both require a sha256.
locales
A Studio language pack — a locale the plugin adds, or gaps it fills in a built-in one. See Make a plugin. A manifest with only contributes.locales and no entry code is a valid plugin.
reservedSaveIds
A save the plugin writes under its own id, such as a quick save slot or a checkpoint, sits in the project's save store beside the player's saves. List those ids here to keep them off the player's save list: List Saves, every save screen built on it and the Dev Mode saves panel leave them out, as they leave out autosaves. Everything else still reaches them: the plugin's own app.game.saves, whose listIds returns the store as it is, and the save nodes that take an id, such as Load Save, Delete Save and the save readers.
{ "contributes": { "reservedSaveIds": ["yourname.quicksave.slot"] } }Each id must start with your plugin id, so a plugin cannot hide a slot the author's own Save Game wrote. The ids are reserved while the plugin runs in the game; in a game that does not run it, those saves are listed like any other.
structs
A node that hands out a set of records, such as gallery entries, achievements or shop items, declares the fields of those records here and gives its output pin the struct's type:
{
"contributes": {
"blueprintNodes": ["yourname.shop.getGoods"],
"structs": [
{
"id": "yourname.shop.good",
"name": "Shop item",
"localized": { "zh": "商品", "ja": "商品" },
"fields": [
{ "key": "id", "type": "string" },
{ "key": "name", "type": "string" },
{ "key": "price", "type": "number" },
{ "key": "icon", "type": "image" },
{ "key": "owned", "type": "boolean" }
]
}
]
}
}{ id: "goods", kind: "output", semantic: "data", valueType: "array<struct:yourname.shop.good>", label: "Goods" }| Field | Notes |
|---|---|
id | Required. Prefixed with your plugin id. A pin carrying one record is typed struct:<id>, an array of them array<struct:<id>>. |
name | Required. What the editor calls the struct, on pins and in a List's Struct list. |
localized | name per editor language, keyed by the editor's locale code, such as zh or ja. |
fields | Required, at least one. { key, type } for each field, in the order a List shows them. type is string, number, boolean, image, color or json. A key cannot repeat. |
A declared struct is handled as the engine's own, such as an ending or a save entry:
- A List can choose it as its Struct under Edit content. Its fields come from the plugin and cannot be edited, so an author does not copy field names from your documentation.
- A wire dragged off the pin offers each field in the add-node menu, and
Get Fieldreads a field by name with the field's type on its output. - Rows wired into
Set List Contentthat lack a field of the List's struct are reported on the canvas and by the project checks.
A field's key is also its id, so a field of your struct cannot be renamed by an author. The editor, the running game and the command-line checks read the declaration from the manifest before any plugin code runs. While the plugin is not loaded (disabled or not installed), its structs are unknown, as its nodes are: a List that names one shows no fields, and the project's dependency check names the plugin. structs derives no install permission.
Permissions are derived
Install permissions come in two families, and the split is the point.
Author-declared — written by you in permissions[]. These are privileged Studio controls that have nothing to do with contributes:
{
"permissions": [
{ "kind": "filesystem", "path": "/absolute/path", "mode": "readwrite", "recursive": true },
{ "kind": "api", "capability": "bash.execute" }
]
}| Kind | Shape |
|---|---|
filesystem | { path, mode: "read" | "write" | "readwrite", recursive } — a real path string. |
api | { capability } — currently only bash.execute is usable by a plugin. |
These affect the studio entry only. Grants are recorded per pluginId@version, so bumping your version requires the author to approve again.
Derived — computed by Studio from contributes, never written by you:
| Kind | Derived from |
|---|---|
runtime | Each entry in contributes.runtimeCapabilities. |
sidecar | Each contributes.sidecars entry, with the platform keys it ships. |
buildDependency | Each contributes.buildDependencies entry, with the hostnames it downloads from. |
externalLink | contributes.externalLinks, as the author's own patterns — unrewritten, so the prompt and the manifest are one document. |
network | contributes.network. A separate question from externalLink and never folded into it: opening a page hands an address to the browser and nothing comes back, while this fetches, and what comes back runs inside the game. |
agentTools | contributes.agentTools: every tool, and the ones that change the project. Adding a tool, or turning a reading tool into a writing one, asks again. |
Writing a runtime, sidecar, buildDependency, externalLink, network or agentTools permission by hand is a manifest error — the plugin fails to install with "permission kind … is derived from contributes and must not be declared by hand." Declare the capability once, in contributes, and the permission follows.
This is what keeps the install prompt honest: a capability is declared in exactly one place, so what the prompt lists and what the plugin can actually reach cannot drift apart. It also makes updates behave — adding a capability widens the derived permission set, which re-prompts the author automatically, while a version that widens nothing inherits the existing grant.
Validation failures
| Symptom | Check |
|---|---|
| Install rejected | manifest.json is not valid JSON, or id / version / entries failed validation. |
| "requires a runtime entry" | runtimeCapabilities or sidecars declared without entries.runtime. |
| "must not be declared by hand" | A derived permission kind written into permissions[]. |
| "Unknown plugin runtime capability" | A typo in runtimeCapabilities. The list is closed. |
| Registration throws at load | A node or widget type that is not in contributes, or not prefixed with the plugin id. |
| Preview fails with "Plugin validation failed" | A node or widget the project uses has no runtime provider — plugin disabled, missing, no entries.runtime, or the type is not declared. |
| Digest mismatch on install | A sidecar file's bytes do not match its sha256. |
Contributions that derive no permission
tests, buildConfig, reservedSaveIds and structs are the contribution kinds that produce no install permission, and in each case the absence is a decision rather than an omission.
Every other code-backed contribution gains a power the moment the plugin loads. A test only ever runs because the author opened Run > Test, picked it out of the dialog and pressed Start; there is no ambient capability to consent to at install time, and what a test may reach while running is gated separately by its own requires. A build-configuration field is a blank the author fills in, and filling it in gives the plugin no reach it did not already have. A reserved save id only takes ids in the plugin's own namespace out of the player's save list. A struct only describes the fields of rows the plugin's nodes already hand out.