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 stores your compiled entry code in 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. Every entry-point module except defineAction must default-export the
definition its define* helper authored. A defineAction module instead exports each
action as a named, branded defineAction(...) value; the build discovers those named
exports and records their names in the built manifest.
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 |
|---|---|---|
defineAction | Plugin action route, run per call | tenant |
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 |
defineRerankProvider | Rerank-provider 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 and stores adefineActionentry's declared actions, whichPOST /api/v1/plugins/{name}/actions/{action}runs one call at a time. 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, a private ESM package.json, and a source stub at
src/defineBehavior.ts whose default export is a minimal, contract-valid definition
for that slot. The package manifest declares the @dots/sdk and zod imports used by
the generated source. --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 scaffold file, and a
slot name outside the catalog is refused with the known slot list printed.
init has no scaffold for defineAction, because an action module exports each
action as a named defineAction(...) value rather than one default definition. It
refuses that slot as a usage error, writes no file, and points you at
@dots/sdk/extend. Write the action module yourself and declare it as a
defineAction entry point and capability; the build records its action names.
For a scaffolded slot, fill in the stub with real logic and keep the default export.
2. Make The SDK Resolve
The build imports each entry point to extract either its default-exported definition or
its named actions, so @dots/sdk and zod must resolve from the plugin directory. The
scaffolded package.json already declares both dependencies and marks the plugin as
ESM. Until @dots/sdk is published, link @dots/sdk and its zod into the plugin's
node_modules from 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 and imports every entry point except defineAction to
collect its default-exported definition. It computes a SHA-256 over the canonical JSON
of that declarative payload. Because the JSON is canonical (object keys sorted, array
order kept), the same payload always produces the same digest.
It then compiles the code the sandbox runs. Each defineBehavior and defineAction
entry is bundled with everything it imports into one JavaScript file, keyed under its
source path with the extension replaced by .js, so ./src/defineBehavior.ts becomes
./src/defineBehavior.js in the built manifest. For defineAction, the build also
imports the module to collect the named exports carrying branded actions and writes
those names into the entry's actions list. An entry of any other slot keeps its source
text as written. A plugin that declares a ui block also gets its UI entry bundled into
one browser module that takes React and @dots/sdk/ui from the host page, plus at most
one stylesheet from ui.css.
The build writes dots-plugin.json, payload.json, entry-sources.json, and
checksum.txt into the plugin's own dist/, plus ui/index.js and ui/index.css for a
plugin declaring them; --out puts them elsewhere. checksum.txt is the digest of
payload.json alone. The command's output record also reports installChecksumSha256,
the checksum install sends, and installBodyBytes, the size of the body install posts.
The build sizes that install body against the server's limits before it writes a file.
An entry source over ENTRY_SOURCE_BYTES_MAX characters, a UI file over
PLUGIN_UI_ASSET_MAX_CHARS characters, or a whole body over JSON_BODY_LIMIT_BYTES
bytes is refused with a message naming the limit and the measured size.
6. Entitle Your Company To Run Plugin Code
Install, upgrade, enable, disable, uninstall, and list are each 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 builds the plugin directory in memory exactly as step 5 does and posts that built body to your daemon: the built manifest, the compiled entry code, the UI files when the plugin has them, and a checksum computed over all of them together. What you install is exactly what you built. Every build refusal, the three size limits included, stops the command before it sends anything. A manifest declaring an entry point whose code is missing from the posted sources is refused before anything is written.
The daemon recomputes that checksum over the manifest, the entry code, and the UI files
exactly as they arrived, before any hook runs. A body whose digest differs from the
posted checksumSha256 is refused with 422 PLUGIN_CHECKSUM_MISMATCH, and nothing is
written. dots plugin upgrade ./my-plugin builds and posts the same way to replace an
installed plugin in place, under the same check.
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