Build And Publish A Plugin

Scaffold a plugin in your own repository, run it against a local daemon, install it on your instance under a sandboxed grant, and submit it to the Marketplace only when you choose to.

A plugin adds a capability the platform did not ship with: a new property type, a new view type, a provider, an MCP tool, or a behavior that fires on your data.

The workflow is private by default. You author in a repository you own, run and install it on your own instance, and it reaches the Marketplace only when you run the command that submits it. Nothing before that step contacts a hosted backend.

Before You Start

  • The CLI installed and signed in.
  • A local Dots instance you can install into — the daemon your CLI resolves to.
  • A directory outside your checkout for the plugin itself.

1. Scaffold

Pick the slot your plugin contributes and scaffold into a new directory. The slot is the name of the authoring helper — defineBehavior for code that runs on your data, defineTemplate, defineProperty, and defineView among the declarative ones:

SH
dots plugin init defineBehavior ./my-plugin --name @acme/my-plugin

That writes two files: dots-plugin.json, the capability manifest, and a source stub under src/ whose default export is your definition. Fill in the stub with real logic. Pass --force only when you mean to overwrite an existing manifest or stub.

The distinction between the two kinds of plugin matters later. A declarative plugin contributes data — a template, a schema — and carries no code. A code-bearing plugin contributes defineBehavior, which means it ships source that executes. The two take different paths at both install and packaging.

2. Make The SDK Resolve

The build imports your entry point to extract its definition, so @dots/sdk and zod must resolve from the plugin directory. Two steps get them there, because the scaffold ships no package.json and @dots/sdk resolves from a local Dots checkout:

  1. Add a package.json containing "type": "module". Without it the entry resolves as CommonJS, which the SDK's import-only subpath exports do not satisfy, and the build fails saying ./extend is not defined by exports.
  2. Link @dots/sdk and its zod into the plugin's node_modules from your local Dots checkout — directory junctions on Windows, symlinks elsewhere.

3. Run It Before You Build It

SH
dots plugin dev ./my-plugin --watch

This boots a local daemon in the foreground. Six of the SDK's 18 extension slots register in that daemon: defineProperty, defineView, defineEmbeddingProvider, defineLLMProvider, defineBehavior, and defineTemplate. Every other slot still loads and validates, but contributes no runtime on dots plugin dev; the command warns and names those slots. Run dots mcp --plugin <dir> to exercise a defineMcpTool contribution. --watch reboots the daemon when a source file changes. Ctrl-C stops it. This loop needs no install, no built artifact, and no listing.

4. Build The Artifact

SH
dots plugin build ./my-plugin

The build reads the manifest, assembles the payload, snapshots each declared entry point's source, and computes a SHA-256 over the canonical JSON. It writes into the plugin's own dist/ — never into your Dots checkout — producing the manifest, the payload, the entry sources, and the checksum. --out puts them elsewhere.

That checksum is the same digest install re-verifies later, so a payload modified after the build fails verification instead of installing.

5. Entitle Your Company To Run Code

Installing a code-bearing plugin is gated on the execution.run capability, which is a per-company entitlement rather than a role permission. On your own instance you grant it to yourself, and that grant is the capability-approval step:

SH
dots access entitlements grant execution.run

Without it, install fails closed with a message naming the capability. Revoke it at any time with dots access entitlements revoke execution.run; dots [access](/reference/glossary/access) entitlements list shows what the company currently holds.

6. Install And Exercise It

SH
dots plugin install ./my-plugin
dots plugin list
dots plugin disable @acme/my-plugin
dots plugin enable @acme/my-plugin
dots plugin uninstall @acme/my-plugin

Install parses the manifest, snapshots the entry sources, computes the checksum, and posts the whole envelope to your daemon. Your plugin's code runs in the Code Execution Sandbox, never inside the daemon process, under a grant derived deterministically from the manifest's declared capability atoms — there is no consent prompt, because what you declared is what you get. Network, secrets, and filesystem stay at the deny seed regardless of what a hook asks for.

Install is all-or-nothing: a lifecycle hook that fails aborts the install and persists nothing. Disable unregisters the plugin's slots but keeps the install snapshot, so re-enabling needs no reinstall. Uninstall runs the teardown hook through the sandbox and removes the record.

What executes. Lifecycle hooks run in the sandbox, and a behavior registers for sandbox dispatch. A granted plugin's data access is denied: a real row change triggers no behavior execution, and sandboxed code has no transport for reading or writing your Dotabases.

There is no upgrade command. Re-installing a changed version is how you move an installed plugin forward.

7. Package, Then Publish When You Decide To

There is no private or unlisted flag on a listing. A listing's visibility is its status — draft, in review, published, suspended, archived — so "private" means simply not submitted. A plugin you build and self-host never touches the Marketplace.

When you do want to submit one, it is two commands, and only the second contacts a hosted service:

SH
dots plugin package ./my-plugin

This writes a local listing record to dist/listing.json and stops. Nothing is uploaded. Open the file and read it. A declarative plugin is projected onto a template listing; a code-bearing one onto an SDK plugin listing.

SH
dots marketplace login
dots marketplace publish ./my-plugin/dist --type sdk_plugin

The submission re-serializes the payload with the same canonical JSON the loader checksums, so the bytes the Marketplace stores hash to the digest install re-verifies, then drives draft, create, version, and submit in one pass.

A code-bearing plugin is always routed to human review. Submitting leaves it in review; it reaches the public catalog only after a person approves it, so a submission alone never publishes anything. Publish a Marketplace Listing covers what the review looks for and how a rejection comes back.

What Packaging Refuses

Some slots are operator-scoped — property types, types, providers, and MCP tools change how an entire instance behaves, not one tenant's data. They install fine on your own instance and are refused during packaging, in the manifest's capabilities, in its entry points, and in the built payload alike. Only tenant-scoped declarative slots and defineBehavior are distributable.

A payload carrying private workspace identifiers is refused during packaging for the same reason: packaging distributes the plugin, not the workspace it was built in.

Build And Publish A Plugin · This page in the graph

Connected Records17

Documented by
Mentions
DOT
References
docs(content): Author the eight section overviews and Build a Plugin at full depth
docs(content): Author the eight section overviews and Build a Plugin at full depth
17 connections.

Where To Go Next

Reading this as an agent?For Agents
Something wrong on this page?Dots On GitHub