Appearance
Plugins
Plugins extend the Lota SDK runtime with services, tools, database schemas, and environment variable declarations. They are typed via the LotaPlugin interface and registered through the pluginRuntime config field.
Plugin Interface
Every plugin implements the LotaPlugin interface, exported from @lota-sdk/core:
ts
import type { LotaPlugin, LotaPluginContributions } from '@lota-sdk/core'
interface LotaPluginContributions {
envKeys: readonly string[]
schemaFiles: readonly (string | URL)[]
}
interface LotaPlugin<TServices = unknown, TTools = unknown> {
services: TServices
tools?: TTools
contributions: LotaPluginContributions
}| Field | Type | Description |
|---|---|---|
services | TServices | Plugin-owned service instances. Can include any methods the plugin needs to expose, including an optional connectDatabase() method for DB initialization. |
tools | TTools | Plugin-owned tool factories that agents can use during execution. Optional. |
contributions.envKeys | readonly string[] | Environment variable names the plugin requires. Surfaced via runtime.contributions.envKeys so the host can validate its environment at startup. |
contributions.schemaFiles | readonly (string | URL)[] | SurrealDB .surql schema files applied at startup alongside built-in SDK schemas. |
Registering Plugins
Plugins are registered via the pluginRuntime field in createLotaRuntime(). Each key becomes the plugin's name on runtime.plugins:
ts
import { createLotaRuntime } from '@lota-sdk/core'
const runtime = await createLotaRuntime({
// ... other config
pluginRuntime: {
github: githubPlugin,
linear: linearPlugin,
slack: slackPlugin,
},
})Plugin Lifecycle
Plugins participate in the runtime lifecycle at several points:
1. Schema Application
Plugin schema files are collected from all registered plugins and merged into the runtime's contributed schema list. When a host applies schemas (via runtime.connect() or a separate schema step), plugin schemas are included:
ts
// Plugin schema files are available at:
runtime.contributions.schemaFiles
// Contains all schema files from all registered plugins2. Environment Key Surfacing
All plugin envKeys are merged with the SDK's own required keys and deduplicated:
ts
// All required env keys (SDK + plugins) are available at:
runtime.contributions.envKeys
// e.g. ['AI_GATEWAY_URL', 'REDIS_URL', ..., 'GITHUB_TOKEN', 'LINEAR_API_KEY']This allows the host to validate that every required environment variable is present before starting.
3. Database Connection
If a plugin exposes a connectDatabase() method on its services, call runtime.connectPluginDatabases() during host startup. This iterates over all registered plugins and invokes services.connectDatabase() on each one that provides it:
ts
await runtime.connect()
await runtime.connectPluginDatabases()4. Tool Availability
Plugin tools are available on the plugin instance and can be included in agent tool building via the host's buildAgentTools configuration.
Building a Plugin
Here is a step-by-step example of building a Slack notification plugin.
Step 1: Define the Plugin Types
ts
// slack-plugin.ts
import type { LotaPlugin } from '@lota-sdk/core'
interface SlackServices {
sendMessage(channel: string, text: string): Promise<void>
connectDatabase(): Promise<void>
}
interface SlackTools {
sendSlackMessage: ReturnType<typeof createSendSlackMessageTool>
}Step 2: Create the Plugin Factory
ts
export function createSlackPlugin(config: {
webhookUrl: string
}): LotaPlugin<SlackServices, SlackTools> {
return {
services: {
async sendMessage(channel, text) {
await fetch(config.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel, text }),
})
},
async connectDatabase() {
// Initialize any plugin-specific DB state if needed
},
},
tools: {
sendSlackMessage: createSendSlackMessageTool(config),
},
contributions: {
envKeys: ['SLACK_WEBHOOK_URL'],
schemaFiles: [new URL('./schema/slack.surql', import.meta.url)],
},
}
}Step 3: Register the Plugin
ts
import { createSlackPlugin } from './slack-plugin'
const slackPlugin = createSlackPlugin({
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
})
const runtime = await createLotaRuntime({
// ... other config
pluginRuntime: {
slack: slackPlugin,
},
})
await runtime.connect()
await runtime.connectPluginDatabases()Accessing Plugin Services
Registered plugins are available on runtime.plugins keyed by their registration name:
ts
const slack = runtime.plugins.slack as LotaPlugin<SlackServices>
await slack.services.sendMessage('#general', 'Deployment complete.')You can also access plugin tools for inclusion in agent tool sets:
ts
const slackTools = runtime.plugins.slack as LotaPlugin<SlackServices, SlackTools>
const tool = slackTools.tools?.sendSlackMessagePlugin Schema Files
Plugin schema files follow the same rules as all SDK schemas:
- Use
IF NOT EXISTSfor allDEFINEstatements -- neverOVERWRITEorREMOVE. - Assume a fresh database. No migration DML.
- Place
.surqlfiles alongside the plugin source and reference them withimport.meta.url:
sql
-- schema/slack.surql
DEFINE TABLE IF NOT EXISTS slackNotification SCHEMAFULL;
DEFINE FIELD IF NOT EXISTS channel ON TABLE slackNotification TYPE string;
DEFINE FIELD IF NOT EXISTS text ON TABLE slackNotification TYPE string;
DEFINE FIELD IF NOT EXISTS sentAt ON TABLE slackNotification TYPE datetime
DEFAULT time::now();
DEFINE FIELD IF NOT EXISTS workstreamId ON TABLE slackNotification TYPE record<workstream>;
DEFINE INDEX IF NOT EXISTS idx_slack_workstream
ON TABLE slackNotification COLUMNS workstreamId;ts
contributions: {
envKeys: ['SLACK_WEBHOOK_URL'],
schemaFiles: [new URL('./schema/slack.surql', import.meta.url)],
}Summary
| Concern | Where It Lives |
|---|---|
| Plugin type | LotaPlugin from @lota-sdk/core |
| Registration | pluginRuntime field in createLotaRuntime() |
| Access at runtime | runtime.plugins.<name> |
| Schema files | runtime.contributions.schemaFiles |
| Env keys | runtime.contributions.envKeys |
| DB initialization | runtime.connectPluginDatabases() |