@useatlas/plugin-sdk
v0.0.17
Published
Type definitions and helpers for authoring Atlas plugins
Downloads
3,385
Maintainers
Readme
@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).
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| 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.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| 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).
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| routes | (app: Hono) => void | No | Mount Hono routes for webhooks, OAuth, etc. Optional for non-HTTP transports |
Action (AtlasActionPlugin)
Enable agent side-effects. Action plugins provide AI SDK tools with approval controls.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| 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 |
Sandbox (AtlasSandboxPlugin)
Provide code isolation for the explore tool. Sandbox plugins create backends that execute shell commands in an isolated environment.
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| 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:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| 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
pluginsarray inatlas.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 whenDATABASE_URLis 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 thepluginscomponent ofGET /api/health— a failing probe shifts the top-level status todegraded(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 inonUninstall. - Per-workspace uninstall —
onUninstall(workspaceId)fires when a workspace uninstalls the plugin (both the marketplaceDELETEroute andWorkspaceInstaller.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. Seepackages/api/src/lib/integrations/jira/lazy-builder.tsfor a reference implementation (revokes only workspace-attributed Jira dynamic webhook subscriptions).
v1.1 note:
AtlasPluginContextwill gainexecuteQuery,conversations, andactionsfields 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:
| State | Survives uninstall? | Notes |
|-------|---------------------|-------|
| 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 } to rewrite, throw to reject, return void to pass through
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`);
},
}],
}| Hook | Context | Mutation | Description |
|------|---------|----------|-------------|
| 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 # Preview migration SQL
atlas migrate --apply # Apply to internal DBTables 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>;
// CH["Config"] → { url: string; database?: string }
// CH["Type"] → "datasource"
// CH["DbType"] → "clickhouse"
// CH["Actions"] → never (not an action plugin)
// CH["Security"] → never (not a sandbox plugin)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 # List installed plugins from atlas.config.ts
atlas plugin create <name> --type <type> # Scaffold a new plugin (datasource|context|interaction|action|sandbox)
atlas plugin add <package-name> # Install a plugin package via bunatlas 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)) {
// plugin is AtlasDatasourcePlugin — connection, entities, dialect available
}Reference Implementations
Note: These are workspace packages (
@useatlas/*) within the monorepo. Use them as reference when authoring your own plugins.
| Plugin | Type | Package | Description |
|--------|------|---------|-------------|
| 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.
