Build A Plugin

Author a plugin in a directory of your own, declare the seam it contributes and the authority it asks for, run it against a local daemon, and install it on your own instance — without editing a line of Dots source.

A plugin adds something the platform did not ship with — a behavior that fires on your rows, a Dotabase blueprint, a property type, an embedding or language-model provider, a tool your coding agent can call. You write it in a directory of your own, and nothing you write is compiled into Dots.

That isolation is the point. The build writes into your plugin's own dist/, never into a checkout. Install snapshots your entry-point source into your instance's own install record and runs it in the Code Execution Sandbox, never inside the daemon process. An agent iterating on a plugin all afternoon cannot take the platform down, because it never had the platform's source open.

This page takes one plugin from an empty directory to running inside your workspace. SDK is the wider surface it sits in.

What A Plugin Is

A plugin is a directory holding two kinds of file: a dots-plugin.json manifest and one source module per contributed artifact. The manifest is the whole declarative contract:

JSON
{
  "name": "@acme/my-plugin",
  "version": "1.0.0",
  "dotsSdkVersion": "^1.0.0",
  "capabilities": [{ "slot": "defineBehavior", "atoms": ["dotabase.rows.update"] }],
  "dependencies": [],
  "entryPoints": [
    { "slot": "defineBehavior", "source": "./index.ts", "lifecycle": ["install", "uninstall"] }
  ]
}

name is an npm-style package name and version an exact MAJOR.MINOR.PATCH. Each capabilities[] entry names one contributed slot and the access atoms the plugin asks for in that slot — both validated against the live catalogs, so an invented slot or an invented atom fails the parse rather than installing with authority nobody recognizes. Each entryPoints[] entry names a slot, a plugin-directory-relative source, and the lifecycle phases that artifact participates in. A source starting anywhere but ./, or containing a parent-directory segment, is refused. The schema is strict at the top level: an unknown key fails.

Two rules bind the arrays together. Every entryPoints[].slot must also appear in capabilities[], so a contributed artifact can never carry authority the manifest never declared. And every entry-point module must default-export the definition its define* helper authored — that default export is what the dev harness loads and what the build serializes.

An optional runtime block declares the secret environment-variable names and network hosts your sandboxed code needs, as { "envNames": [...], "hosts": [...] }.

The Seams A Plugin May Use

Each contributed slot is one typed authoring helper exported from @dots/sdk/extend. The install scope says where a registration lands: an operator-scoped extension changes one deploy-level registry for the whole instance, while a tenant-scoped one binds against a company's own Dotabases.

HelperRegistersInstall scope
defineBehavior engine, bound per tenant
defineConnectorConnector registryoperator
defineEdgeTypeCross-module edge-type overlaytenant
defineEmbeddingProviderEmbedding-provider registryoperator
defineExtractorDerivation extractor registrytenant
defineJobJob registryoperator
defineLLMProviderLanguage-model provider catalogoperator
defineMcpToolMCP runtime tool registryoperator
defineModuleModule registry identity surfaceoperator
defineNormalizerMarkdown-normalizer registryoperator
definePromptPrompt artifact settenant
definePropertyProperty-type registryoperator
defineScrubRuleScrub registryoperator
defineSearchEntityTypeSearch entity registryoperator
defineSuperpowerTypeSuperpower template registrationtenant
defineTagCategoryTag-category registrytenant
defineTemplateDotabase Templates Registrytenant
defineViewView-type overlayoperator

Where Each Seam Takes Effect

The three ways to run a plugin admit different slots, and each one reports what it did rather than silently dropping the rest.

  • dots plugin dev boots a daemon carrying an overlay built from your entry points. Six slots map onto that overlay: defineProperty, defineView, defineEmbeddingProvider, defineLLMProvider, defineBehavior, and defineTemplate. Every other slot still loads and validates its definition, and the command warns on stderr naming each one it could give no effect.
  • dots mcp --plugin <dir> loads a plugin's defineMcpTool entry points and threads them into the MCP server as runtime tools, advertised to the host when the connecting credential holds the tool's required atom.
  • dots plugin install registers runtime behavior for defineBehavior entry points. Every other slot comes back in the install response's skipped list carrying the reason it was skipped, so the response stays honest about what is live.

Before You Start

  • The CLI installed and signed in — see Install the CLI.
  • An instance you can install into: the daemon your CLI resolves to.
  • A directory for the plugin, outside any Dots checkout.
  • A local Dots checkout to resolve @dots/sdk from, per step 2.

1. Scaffold The Plugin

Pick the slot the plugin contributes and scaffold into a new directory:

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

That writes dots-plugin.json and a source stub at src/defineBehavior.ts whose default export is a minimal, contract-valid definition for that slot. --name takes an npm-style package name; omitted, the name is derived from the directory. The scaffolded manifest is re-parsed through the real schema before it is written, so a defect surfaces here rather than three commands later. Pass --force only when you mean to overwrite an existing manifest or stub, and a slot name outside the catalog is refused with the known slot list printed.

Fill in the stub with real logic. Keep the default export.

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:

  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:
CODE
node_modules/@dots/sdk -> <dots>/packages/sdk
node_modules/zod       -> <dots>/packages/sdk/node_modules/zod

The Extend axis imports only zod, the -atom catalog, and SDK-local mirrors, so linking those two pulls in no heavier package.

3. Run It Against A Local Daemon

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

This boots a daemon in the foreground with your plugin's overlay applied. Ctrl-C stops it. With --watch, a debounced source change closes the daemon, resets the process-wide registrations so re-registering the edited definition does not collide, reloads the entry-point modules, and reboots. This loop needs no install and no built artifact.

On a freshly provisioned instance, a plugin's templates register after the first owner completes onboarding rather than at boot, because template registration arms on the post-provision surface.

4. Declare What It May Touch

Your manifest's declared atoms are the grant. There is no consent prompt at install, because there is nothing left to consent to — the grant is derived deterministically from what you wrote.

Derivation starts from a deny-all seed and narrows upward. dotabase.read yields a targetless search scope; the graph atoms yield read and write scopes; an atom with no mapping yields no scope rather than a fabricated one. Filesystem access stays at the deny seed. runtime.hosts turns network egress from deny into an allowlist over exactly those hosts, and runtime.envNames names the secrets the run may see. A row-scoped atom binds to the triggering row's own Dotabase at the moment the behavior fires, since the manifest atom itself names no Dotabase.

Ask for the narrowest atom set that lets the plugin finish its job. Access and Tenancy explains how atoms resolve into an answer, and MCP Tools lists the atom each tool requires.

5. Build The Artifact

SH
dots plugin build ./my-plugin

The build reads the manifest, imports each entry point to collect its definition, snapshots each entry-point source file verbatim, and computes a SHA-256 over the canonical JSON of the payload. It writes dots-plugin.json, payload.json, entry-sources.json, and checksum.txt into the plugin's own dist/; --out puts them elsewhere. Because the JSON is canonical — object keys sorted, array order kept — the same payload always produces the same digest.

6. Entitle Your Company To Run Plugin Code

Every plugin route is gated on two things at once: the per-company execution.run entitlement, and the workspace.settings.manage permission atom. Both fail closed with a denial rather than a partial install. On your own instance you grant the entitlement to yourself, and that grant is the approval step:

SH
dots access entitlements grant execution.run

dots access entitlements list shows what the company currently holds, and dots access entitlements revoke execution.run takes it back at any time.

7. Install It On Your Own Instance

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 every declared entry-point source, computes the checksum over the manifest and those sources together, and posts the whole envelope to your daemon. A manifest declaring an entry point whose source is missing from the snapshot is refused before anything is written.

Install is all-or-nothing. Lifecycle hooks run in the sandbox, and a hook that returns a failure aborts the transition and persists nothing. Installing under a name the company already has is a conflict, not an overwrite — uninstall first. 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.

Which sandbox actually runs the code is your instance's choice, selected by the executor provider it was configured with: a container on a machine where Docker is detected, or a microVM through a hosted executor. Where the configured provider is unavailable, code execution is disabled with a visible notice rather than quietly falling back.

Confirm It Works Inside Your Dots

The install response is the confirmation, and it is specific. It carries the derived grant the plugin now holds, the behavior ids it registered, and every slot it skipped with the reason. Read it back at any time:

SH
dots plugin list

A behavior id shaped plugin:<name>:<slot> in that means the platform is holding your code as a live registration, namespaced under your plugin so two plugins never collide. Your plugin works inside your Dots.

When the next thing you want is an agent calling it under an identity of its own rather than yours, Issue Scoped Agent Credentials is the procedure.

Build A Plugin · This page in the graph

Connected Records17

Documented by
Mentions
DOT
References
17 connections.

Where To Go Next

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