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:
{
"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.
| Helper | Registers | Install scope |
|---|---|---|
defineBehavior | engine, bound per | tenant |
defineConnector | Connector registry | operator |
defineEdgeType | Cross-module edge-type overlay | tenant |
defineEmbeddingProvider | Embedding-provider registry | operator |
defineExtractor | Derivation extractor registry | tenant |
defineJob | Job registry | operator |
defineLLMProvider | Language-model provider catalog | operator |
defineMcpTool | MCP runtime tool registry | operator |
defineModule | Module registry identity surface | operator |
defineNormalizer | Markdown-normalizer registry | operator |
definePrompt | Prompt artifact set | tenant |
defineProperty | Property-type registry | operator |
defineScrubRule | Scrub registry | operator |
defineSearchEntityType | Search entity registry | operator |
defineSuperpowerType | Superpower template registration | tenant |
defineTagCategory | Tag-category registry | tenant |
defineTemplate | Dotabase Templates Registry | tenant |
defineView | View-type overlay | operator |
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 devboots a daemon carrying an overlay built from your entry points. Six slots map onto that overlay:defineProperty,defineView,defineEmbeddingProvider,defineLLMProvider,defineBehavior, anddefineTemplate. 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'sdefineMcpToolentry 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 installregisters runtime behavior fordefineBehaviorentry 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/sdkfrom, per step 2.
1. Scaffold The Plugin
Pick the slot the plugin contributes and scaffold into a new directory:
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:
- Add a
package.jsoncontaining"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./extendis not defined by exports. - Link
@dots/sdkand itszodinto the plugin'snode_modulesfrom your local Dots checkout — directory junctions on Windows, symlinks elsewhere:
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
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
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:
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
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:
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
- mentionstodots marketplace and dots plugin
- mentionstoDotabase
- mentionstoBehavior
- mentionstoAccess
- mentionstoDots
- mentionstoSuperpower
- mentionstoDOT
- mentionstodots mcp, dots repo, and dots graph
- mentionstoView
- mentionstoEdge
- referencestoMCP Tools
- referencestoAccess And Tenancy
- referencestoIssue Scoped Agent Credentials
- referencestoInstall The CLI
- referencestoSDK Overview
- documented byfromCLI Plugin Lifecycle
- referencesfromSDK Overview