@useatlas/plugin-sdk
Type definitions and helpers for authoring Atlas plugins. Plugins extend Atlas with new datasources, context providers, interaction surfaces, actions, and sandbox backends.
Quick Start
import { createPlugin } from "@useatlas/plugin-sdk";
import { z } from "zod";
export const myPlugin = createPlugin({
configSchema: z.object({ url: z.string() }),
create: (config) => ({
id: "my-datasource",
types: ["datasource"] as const,
version: "1.0.0",
config,
connection: {
create: () => makeConnection(config.url),
dbType: "postgres",
},
}),
});
Register in atlas.config.ts:
import { defineConfig } from "@atlas/api/lib/config";
import { myPlugin } from "./my-plugin";
export default defineConfig({
plugins: [myPlugin({ url: process.env.MY_DB_URL! })],
});
Plugin Types
Atlas has five plugin types. Each extends AtlasPluginBase with variant-specific fields.
Datasource (AtlasDatasourcePlugin)
Connect to a database. Provides a connection factory, SQL dialect hints, and optional entity definitions.
A datasource plugin must provide at least one connection factory: create() for a static config-defined connection, createFromConfig() for a DB-stored (admin-registered) per-workspace connection, or both. A plugin with only createFromConfig() is an adapter-only plugin — registered for use but with no static datasource (the multi-tenant SaaS model, where every connection is added per workspace).
connection.create() | () => PluginDBConnection | Promise<PluginDBConnection> | One of create/createFromConfig | Factory that returns a connection with query() and close() from the plugin's config-time config (static / self-host) |
connection.createFromConfig() | (config) => PluginDBConnection | Promise<PluginDBConnection> | One of create/createFromConfig | Factory that builds a connection from a DB-stored per-(workspace, install) config (adapter-only / SaaS) |
connection.dbType | PluginDBType | Yes | Database type identifier (postgres, mysql, clickhouse, snowflake, duckdb, or custom) |
entities | EntityProvider | No | Semantic layer fragments merged into the table whitelist at boot |
dialect | string | No | SQL dialect guidance injected into the agent system prompt |
Reference: clickhouse
Context (AtlasContextPlugin)
Inject knowledge into the agent. Context plugins load additional system prompt fragments, semantic layer extensions, or external metadata.
contextProvider.load() | () => Promise<string> | Yes | Returns context string appended to the agent system prompt |
contextProvider.refresh() | () => Promise<void> | No | Cache invalidation hook |
Reference: yaml-context
Interaction (AtlasInteractionPlugin)
Add communication surfaces. Interaction plugins mount HTTP routes or manage non-HTTP transports (stdio, SSE).
routes | (app: Hono) => void | No | Mount Hono routes for webhooks, OAuth, etc. Optional for non-HTTP transports |
Reference: chat, mcp
Action (AtlasActionPlugin)
Enable agent side-effects. Action plugins provide AI SDK tools with approval controls.
actions | PluginAction[] | Yes | Array of action definitions |
actions[].name | string | Yes | Tool name |
actions[].tool | ToolSet[string] | Yes | AI SDK tool definition |
actions[].actionType | string | Yes | Category identifier (e.g. jira:create) |
actions[].reversible | boolean | Yes | Whether the action can be undone |
actions[].defaultApproval | ActionApprovalMode | Yes | auto, manual, or admin-only |
actions[].requiredCredentials | string[] | Yes | Config fields needed at runtime |
Reference: jira, email
Sandbox (AtlasSandboxPlugin)
Provide code isolation for the explore tool. Sandbox plugins create backends that execute shell commands in an isolated environment.
sandbox.create(semanticRoot) | (string) => PluginExploreBackend | Yes | Factory that creates an explore backend |
sandbox.priority | number | No | Higher = tried first. Built-in: Vercel=100, E2B=90, Daytona=85, nsjail=75, sidecar=50, just-bash=0. Plugin default: 60 |
security | object | No | Informational metadata about isolation guarantees |
Reference: nsjail, vercel-sandbox
Base Fields
All plugins share these fields from AtlasPluginBase:
id | string | Yes | Unique identifier (e.g. clickhouse-datasource) |
type | PluginType | Yes | One of datasource, context, interaction, action, sandbox |
version | string | Yes | SemVer version |
name | string | No | Human-readable display name |
config | TConfig | No | Plugin-specific configuration |
initialize(ctx) | (AtlasPluginContext) => Promise<void> | No | Called once at server boot. Throw to block startup |
healthCheck() | () => Promise<PluginHealthResult> | No | Periodic health probe |
teardown() | () => Promise<void> | No | Graceful shutdown (LIFO order) |
onUninstall(workspaceId) | (string) => Promise<void> | void | No | Per-workspace uninstall hook — revoke external webhook subscriptions / OAuth grants the plugin registered for that workspace |
hooks | PluginHooks | No | Agent lifecycle and HTTP hooks |
schema | Record<string, PluginTableDefinition> | No | Declarative table definitions for the internal DB |
Lifecycle
register → initialize(ctx) → healthCheck() → ... → teardown()
└── onUninstall(workspaceId) (per-workspace, at uninstall time)
- Register — Plugins are listed in the
plugins array in atlas.config.ts. Config is validated at factory call time (before server starts).
- Initialize — Called once at server boot with
AtlasPluginContext. Throw to signal fatal initialization failure. Context provides:
ctx.db — Internal Postgres (auth/audit DB). Null when DATABASE_URL is not set.
ctx.connections — Connection registry for analytics datasources.
ctx.tools — Tool registry for adding agent tools.
ctx.logger — Pino-compatible child logger scoped to the plugin.
ctx.config — Resolved Atlas configuration.
- Health check — Periodic probe. Return
{ healthy: false, message } to signal degradation. Never throw. Results surface in the plugins component of GET /api/health — a failing probe shifts the top-level status to degraded (HTTP 200, never 503).
- Teardown — Graceful shutdown in reverse registration order (LIFO). Use
teardown() to release process-level state — third-party connections, drained queues, timers. Note: teardown() runs on server shutdown, not on a per-workspace uninstall (uninstall is a DB-row removal, not a process event) — per-workspace cleanup belongs in onUninstall.
- Per-workspace uninstall —
onUninstall(workspaceId) fires when a workspace uninstalls the plugin (both the marketplace DELETE route and WorkspaceInstaller.uninstall), before Atlas removes the install row and credential stores — so the plugin can still authenticate against the external platform. Use it to revoke webhook subscriptions, OAuth grants, or any other external state registered for that workspace. Attribution rule: never revoke a subscription you can't positively attribute to the uninstalling workspace (recorded id, metadata tag, or workspace marker in the callback URL) — the credential may be shared with other workspaces or out-of-band tooling. Best-effort: a thrown error is logged with the plugin id + workspaceId and the uninstall proceeds (each invocation also runs against a 15s host-side deadline); never rely on it for load-bearing cleanup. It does not fire on datasource disconnects or a workspace purge — only the two plugin-uninstall paths above. See packages/api/src/lib/integrations/jira/lazy-builder.ts for a reference implementation (revokes only workspace-attributed Jira dynamic webhook subscriptions).
v1.1 note: AtlasPluginContext will gain executeQuery, conversations, and actions fields for full host-level decoupling. Currently, interaction plugins that need these inject them via config callbacks.
Uninstall Contract
DELETE /api/v1/admin/marketplace/:id removes a plugin from a workspace. The cleanup contract:
workspace_plugins row | No (deleted) | Canonical "is this plugin installed?" record. |
scheduled_tasks rows tagged with the plugin's catalog_id | No (deleted) | Scoped by (plugin_id, org_id) so cleanup never crosses workspaces. scheduled_task_runs cascade via FK. Cleanup runs in a separate statement after the install row is removed; partial failure leaves the uninstall committed and is recorded as a cleanupFailed: true audit event. |
plugin_<pluginId>_* tables (declared via schema) | Yes (retained) | Reinstall picks up where it left off — cached digest history, sync cursors, etc. Hard-reset only via workspace purge. |
| In-process hook registrations | Not detached on uninstall — teardown() runs only at server shutdown | Hooks become inert for the uninstalled workspace because dispatch checks workspace_plugins for the installation. |
| Webhook subscriptions registered with external platforms | Yes — unless your onUninstall(workspaceId) revokes them | Atlas has no visibility into external state. Implement onUninstall to revoke the subscription — it fires before the install row and credentials are removed, so your plugin can still authenticate. Revoke only subscriptions attributable to the uninstalling workspace. An un-revoked webhook keeps delivering events to a workspace that no longer has the plugin installed. Note the hook fires only on plugin uninstalls — datasource disconnects and workspace purges skip it. |
If your plugin creates scheduled_tasks rows, set plugin_id = $catalogId and org_id = $orgId on insert so the uninstall cleanup picks them up. Untagged tasks (plugin_id IS NULL) are treated as user-created and survive uninstall. See the authoring guide for the full lifecycle.
Config Validation
Use createPlugin() with a Zod schema for typed, validated configuration:
import { createPlugin } from "@useatlas/plugin-sdk";
import { z } from "zod";
const configSchema = z.object({
url: z.string().url(),
poolSize: z.number().int().positive().optional(),
});
export const myPlugin = createPlugin({
configSchema,
create: (config) => ({
id: "my-datasource",
types: ["datasource"] as const,
version: "1.0.0",
config,
connection: { create: () => connect(config.url), dbType: "postgres" },
}),
});
The factory validates config at call time — invalid config fails fast during startup, not at first use. The configSchema accepts any object with a parse(input) method (Zod schemas satisfy this).
For simple plugins without config validation, use definePlugin():
import { definePlugin } from "@useatlas/plugin-sdk";
export default definePlugin({
id: "my-context",
types: ["context"],
version: "1.0.0",
contextProvider: { async load() { return "Extra context..."; } },
});
Hooks
Hooks use a matcher + handler pattern inspired by Better Auth. Matchers are optional — omit to always fire.
hooks: {
beforeQuery: [{
matcher: (ctx) => ctx.connectionId === "warehouse",
handler: (ctx) => {
return { sql: ctx.sql.replace(/SELECT \*/, "SELECT id, name") };
},
}],
afterQuery: [{
handler: (ctx) => {
console.log(`Query took ${ctx.durationMs}ms, returned ${ctx.result.rows.length} rows`);
},
}],
}
beforeQuery | { sql, connectionId? } | { sql } | Fires before SQL execution. Return to rewrite, throw to reject |
afterQuery | { sql, connectionId?, result, durationMs } | — | Fires after SQL execution |
beforeExplore | { command } | { command } | Fires before explore command. Return to rewrite, throw to reject |
afterExplore | { command, output } | — | Fires after explore command |
onRequest | { path, method, headers } | — | HTTP-level: fires before routing |
onResponse | { path, method, status } | — | HTTP-level: fires after response |
Schema Migrations
Plugins declare internal database tables via the schema property:
schema: {
slack_installations: {
fields: {
team_id: { type: "string", required: true, unique: true },
bot_token: { type: "string", required: true },
installed_at: { type: "date" },
},
},
},
Run migrations with the CLI:
atlas migrate
atlas migrate --apply
Tables are prefixed with the plugin ID to avoid collisions.
$InferServerPlugin
Extract plugin types on the client side without importing server code:
import type { $InferServerPlugin } from "@useatlas/plugin-sdk";
import type { clickhousePlugin } from "@useatlas/clickhouse";
type CH = $InferServerPlugin<typeof clickhousePlugin>;
Works with both createPlugin() factory functions and definePlugin() direct objects.
Available inferred fields: Config, Type, Id, Name, Version, DbType (datasource only), Actions (action only), Security (sandbox only).
CLI
atlas plugin list
atlas plugin create <name> --type <type>
atlas plugin add <package-name>
atlas plugin create generates a plugins/<name>/ directory with src/index.ts, src/index.test.ts, package.json, and tsconfig.json.
Type Guards
import {
isDatasourcePlugin,
isContextPlugin,
isInteractionPlugin,
isActionPlugin,
isSandboxPlugin,
} from "@useatlas/plugin-sdk";
if (isDatasourcePlugin(plugin)) {
}
Reference Implementations
Note: These are workspace packages (@useatlas/*) within the monorepo. Use them as reference when authoring your own plugins.
| clickhouse | Datasource | @useatlas/clickhouse | ClickHouse HTTP transport adapter |
| mysql | Datasource | @useatlas/mysql | MySQL pool-based adapter |
| snowflake | Datasource | @useatlas/snowflake | Snowflake callback-based adapter |
| duckdb | Datasource | @useatlas/duckdb | DuckDB in-process adapter |
| yaml-context | Context | @useatlas/yaml-context | YAML semantic layer context provider |
| mcp | Interaction | @useatlas/mcp | MCP server lifecycle (stdio + SSE) |
| chat | Interaction | @useatlas/chat | Chat SDK bridge — Slack, Teams, Discord, etc. via unified adapter |
| jira | Action | @useatlas/jira | Create JIRA tickets from analysis |
| email | Action | @useatlas/email | Send email reports via Resend |
| nsjail | Sandbox | @useatlas/nsjail | Linux namespace isolation via nsjail |
| sidecar | Sandbox | @useatlas/sidecar | HTTP-isolated container sidecar |
| vercel-sandbox | Sandbox | @useatlas/vercel-sandbox | Firecracker microVM via @vercel/sandbox |
| daytona | Sandbox | @useatlas/daytona | Daytona managed cloud sandbox |
| e2b | Sandbox | @useatlas/e2b | E2B Firecracker microVM (managed) |
Source
The definitive type definitions are in src/types.ts. Factory functions and type guards are in src/helpers.ts.