Tailored AI

Plugins

A plugin is an npm package that extends TAI without forking the monorepo. Eight registries cover the extension surface today:

RegistryAddsRegister with
ToolA new tool the agent can callregisterToolFactory(id, factory)
ChannelA new transport (Slack, Telegram, …)registerChannelFactory(id, factory)
ProviderA new LLM backend (Bedrock, Cohere, …)registerProviderFactory(id, factory)
EmbeddingA new embedding backend (Qdrant, Voyage, …)registerEmbeddingFactory(id, factory)
Task backendA new task store (Linear, Jira, …)registerTaskBackendFactory(id, factory)
Step executorA new workflow step typeruntime.getWorkflowEngine()?.registerExecutor(executor)
TriggerA new workflow trigger kindtrigger-registry (see Workflows)
SkillA SKILL.md the agent can load on demandskill-registry (see Skills)

Each registry is exported from @tailored-ai/core. Built-ins register on module load.

Shape of a plugin

ts
// my-tai-plugin/src/index.ts
import {
  registerChannelFactory,
  registerTaskBackendFactory,
  registerToolFactory,
  type Channel,
  type TaskBackend,
  type Tool,
  type ToolFactoryContext,
} from "@tailored-ai/core";

class SlackChannel implements Channel { /* ... */ }
class LinearTaskBackend implements TaskBackend { /* ... */ }
class WeatherTool implements Tool { /* ... */ }

registerChannelFactory("slack", async (runtime, cfg) => {
  const slack = new SlackChannel(runtime, cfg);
  await slack.connect();
  return { channel: slack, disconnect: () => slack.disconnect() };
});

registerTaskBackendFactory("linear", (config) => {
  return new LinearTaskBackend({
    apiKey: config.tasks?.linear?.apiKey as string,
    teamId: config.tasks?.linear?.teamId as string,
  });
});

registerToolFactory("weather", (config, _ctx: ToolFactoryContext) => {
  const cfg = config.tools?.weather;
  if (!cfg?.enabled) return [];
  return [new WeatherTool({ apiKey: cfg.apiKey as string })];
});

A plugin doesn't need a wrapper class or lifecycle hook. Registering on import is the contract. The user imports your package once at startup and the registrations land.

Installing

bash
tai plugin install @some-author/tai-plugin-slack

The plugin lands in <TAI_HOME>/plugins/ and the CLI appends it to config.yaml's plugins: list for you (comments preserved; pass --no-save to opt out). The list accepts bare names or objects:

yaml
plugins:
  - "@some-author/tai-plugin-slack"
  - module: "@me/tai-plugin-linear"
    config:
      api_token: ${LINEAR_TOKEN}

The CLI dynamic-imports each module at startup. The module's import side-effects (registerToolFactory, registerChannelFactory, etc.) register everything before the runtime asks. Failures are logged and the next plugin is attempted — one broken plugin doesn't take down the others.

The optional per-entry config field is reserved for future routing. Today, plugins read their configuration from the normal tools.*, channels.*, providers.* blocks in the same config file.

For embedders (anyone constructing AgentRuntime directly rather than running tai), call loadPlugins(config, importer) before runtime construction. The importer callback is required so dynamic import() resolves against your package's node_modules, not core's:

ts
import {
  AgentRuntime,
  createProvider,
  loadConfig,
  loadPlugins,
  startRegisteredChannels,
} from "@tailored-ai/core";

const config = await loadConfig("./config.yaml");
await loadPlugins(config, (name) => import(name));

const db = new Database("./agent.db");
const { provider, model } = createProvider(config);
const runtime = new AgentRuntime({ config, db, provider, model });
const channels = await startRegisteredChannels(runtime);

Metadata and config validation

Two optional named exports sit next to the default register function.

meta describes the plugin for UIs and error hints — GET /api/plugins returns it, and registers documents the link between the plugins: entry (which loads code) and the config blocks (which turn features on):

ts
import type { PluginMeta } from "@tailored-ai/core";

export const meta: PluginMeta = {
  name: "AWS Bedrock provider",
  description: "Bedrock-hosted models via the Converse API.",
  registers: [{ kind: "provider", id: "bedrock", configKey: "providers.bedrock" }],
};

validateConfig checks the plugin's own config blocks at load time. Core's validateConfig knows nothing about plugin config shapes on purpose — the plugin owns them:

ts
import type { AgentConfig } from "@tailored-ai/core";

export function validateConfig(config: AgentConfig): string[] {
  const cfg = config.providers.bedrock as BedrockConfig | undefined;
  if (cfg && !cfg.defaultModel) return ["providers.bedrock.defaultModel is empty"];
  return [];
}

Warnings print at startup alongside core's config warnings and appear on GET /api/plugins. A validator can't veto startup; factories still fail fast for hard errors. Both exports are type-only contracts — like the Plugin type itself, they add zero runtime dependency on core.

First-party plugins

These ship from the monorepo as separate npm packages:

PackageWhat it addsHow to use today
@tailored-ai/browser-mediatorThe browser_mediator tool with egress allow-list, vault refs, always-HITL gates.Already a dependency of @tailored-ai/core. Enable via tools.browser_mediator: { enabled: true }.
@tailored-ai/trusted-actionsHITL approval gateway plus the request_action tool.Install separately. Configure under tools.request_action.
@tailored-ai/provider-bedrockThe bedrock model provider (AWS Bedrock Converse API).Install, configure providers.bedrock, select with agent.defaultProvider: bedrock.
@tailored-ai/provider-openrouterThe openrouter model provider (OpenAI-compatible).Install, configure providers.openrouter, select with agent.defaultProvider: openrouter.
@tailored-ai/provider-anthropicThe anthropic model provider (Messages API; supersedes the built-in).Install, configure providers.anthropic, select with agent.defaultProvider: anthropic.
@tailored-ai/provider-openaiThe openai model provider (chat completions; supersedes the built-in).Install, configure providers.openai, select with agent.defaultProvider: openai.
@tailored-ai/channel-slackThe slack channel (Bolt, Socket Mode).Install, configure channels.slack.
@tailored-ai/google-toolsgmail, google_calendar, google_drive tools (via the gog CLI).Install, enable under tools.gmail etc.

Future first-party plugins under consideration: Telegram channel, Linear task backend, Notion knowledge base.

Patterns for plugin-friendly code

  • Take all configuration via the factory's config argument. No reading from process.env directly. Let the user route env vars through YAML.
  • Fail fast in the factory. If required config is missing, throw with a useful message. The startup loop catches and logs.
  • Dynamic-import heavy peer dependencies. If your plugin uses playwright, await import("playwright") inside the connect handler, not at the top of the file. npm install your-plugin shouldn't drag Chromium along unless the user opts in.
  • Don't reach into TAI internals. Use the public exports of @tailored-ai/core. The runtime shape may change between minor versions; public exports won't.

Why register instead of fork?

Forking the monorepo to add a channel or task backend is heavy. Wrapping shell commands in YAML custom tools is limited. The registries fill the gap: write idiomatic TypeScript, ship to npm, your users import your package once and the registrations land. The TAI monorepo doesn't grow. You don't carry a fork.

Where to read next