Extensibility

Plugins

A plugin is a directory whose package.json is the manifest and whose subdirectories are the surfaces it contributes. This page covers that layout and the single public package every surface imports from.

The other pages in this section cover each surface on its own. This page is the glue: how those surfaces sit together in one directory, what the manifest declares, and what a plugin is allowed to import from the host.

Directory layout

A plugin lives at <workspaceDir>/plugins/<name>/. The host introspects the directory at load time: the manifest names the plugin, and each named subdirectory is discovered by convention.

my-plugin/
├── package.json               # Manifest (required)
├── README.md                  # Optional plugin docs
├── config.json                # User-editable config (preserved across upgrades)
├── data/                      # Runtime data directory (preserved across upgrades)
├── hooks/                     # Lifecycle hooks, one per file
│   ├── init.ts
│   └── pre-model-call.ts
├── tools/                     # Model-visible tools, one per file
│   └── example.ts
├── skills/                    # On-demand instruction bundles
│   └── my-skill/
│       └── SKILL.md
└── src/                       # Internal modules (NOT walked by the loader)
  └── state.ts

A few rules govern how the loader walks that tree:

  • Compiled files win. When both a .js and a .ts exist for the same basename, the .js is used, matching compiled-binary semantics. Clean stale .js files when iterating on .ts source, or the loader will silently pick up old code.
  • Missing directories are skipped. A plugin contributes only the surfaces it ships. Absent surface directories are silently omitted.
  • A broken surface file fails only itself. A surface file present but missing a usable default export is logged with attribution and skipped. Sibling plugins keep loading.
  • src/ is yours. Only the named surface directories are walked. Put shared helpers in src/ (or any other directory) and import from them normally.
  • Loading is time-boxed. Each plugin has a 10s import budget. Anything slower is treated as a load failure and the plugin is skipped.

Each surface can also be dropped straight into the workspace at /workspace/<surface>/<name>/ without wrapping it in a plugin. A plugin is what lets you ship several surfaces together as one installable unit.

Preserved entries

Three entries at the plugin root are runtime-owned state, not part of the plugin's source tree. They are excluded from fingerprinting, drift detection, and upgrades, so user edits and runtime data never show as drift and survive re-installs:

EntryPurpose
config.jsonUser-editable plugin config. Read by the init hook via InitContext.config. Ship a default in your repo; users edit it in place.
data/Runtime data directory. The init hook receives its path via InitContext.pluginStorageDir. Write whatever you want here.
.disabledSentinel file created by assistant plugins disable. Presence skips the plugin entirely (no hooks, no tools).

Uninstalling a plugin removes the entire plugin directory, so config.json, data/, and .disabled go with it. No orphaned state is left behind.

The manifest

Every plugin has a package.json. The loader reads three fields and passes everything else through untouched, so your editor, linter, and publish tooling keep working as normal.

{
  "name": "@you/my-plugin",
  "version": "0.0.1",
  "peerDependencies": {
    "@vellumai/plugin-api": "^0.8.0"
  },
  "vellum": {}
}
  • name (required). Any npm-style name. The loader strips the scope (@you/) for the in-runtime plugin name, and duplicate names fail registration. The unscoped portion must be kebab-case (e.g. my-plugin, not myPlugin or my_plugin), matching the convention used for catalog entries and directory names. The default- prefix is reserved for the first-party plugins that ship with the Assistant, so installing a plugin whose unscoped name starts with default- is rejected.
  • version. Informational, and defaults to 0.0.0 when absent.
  • peerDependencies["@vellumai/plugin-api"]. A semver range checked against the running assistant. While plugins are in beta a mismatch is logged but does not block load. Once the install path stabilizes the mismatch will harden into a hard reject, so pin a real range.
  • vellum. Reserved for future use.

The marketplace catalog entry can point at a subdirectory of a repo using source.path in the catalog manifest. See the Distribution page for the full source.path field and the catalog manifest schema.

The @vellumai/plugin-api surface

Plugins import everything they need from a single package, @vellumai/plugin-api. It is the only supported contract: anything not exported from there is assistant-internal and can change without notice. Most of the surface is types (the contexts the host hands your code), with a small set of runtime handles that resolve to the assistant's live singletons. The hook-related exports (context types, the HOOKS constant, the PluginHookFn signature) are documented on the Hooks page, and the tool-related exports (ToolDefinition, ToolContext, ToolExecutionResult, RiskLevel) are on the Tools page. The remaining exports are listed below. Expand a group to see what it exports.

Logging1 exports

The logger the host binds to your plugin name and threads onto the contexts. Log through it rather than rolling your own.

ExportKindPurpose
PluginLoggertypePino-compatible logger shape, bound to { plugin: <name> } and present on the contexts.
Runtime handles10 exports

Values, not just types, that a plugin consumes at module-load or init time. A boot-time shim rebinds each from the assistant's own namespace, so they resolve to the same live singletons the assistant uses.

ExportKindPurpose
assistantEventHubvalueThe assistant's pub/sub hub for runtime events. Subscribe to react to activity outside the hook chain.
getModelProfilesvalueList this workspace's inference profiles in /model picker order, so a routing plugin can learn which profile keys exist before assigning one to PreModelCallContext.modelProfile. Reads live config, so call it at init to build a map once or per call.
doesSupportVisionvalueCheck whether a profile's resolved model can process image input. Takes a ModelProfileInfo entry from getModelProfiles() and resolves the effective (provider, model) by merging the profile over the workspace default and inferring the provider for model-only profiles. Handles mix profiles (true if any arm supports vision). Unknown models default to true (fail-open). Use this to gate image-processing logic on capability rather than model name strings.
getConfiguredProvidervalueResolve a provider instance for a call site (typically 'inference'), optionally overriding the profile. Returns null when no provider is configured. A plugin that needs to run its own model call (e.g. captioning an image with a vision model) uses this to route through the workspace's credentials without supplying its own API key. Pair with getModelProfiles() and doesSupportVision() to pick the right profile.
ModelProfileInfotypeShape of each entry getModelProfiles() returns: key, label, description, isActive, isDisabled, and isMix. Disabled profiles and weighted mix profiles are included and flagged; a mix is a valid target that splits the call across its constituents per conversation.
AssistantEventtypePayload shape of an event published on the hub.
AssistantEventHubtypeInterface of the event hub itself.
AssistantEventCallbacktypeSubscriber callback invoked for each matching event.
AssistantEventFiltertypeFilter narrowing which events a subscription receives.
AssistantEventSubscriptiontypeHandle returned by subscribing, used to unsubscribe.

Surfaces not yet in plugins

The assistant supports these surfaces today, but they are not yet contributed through the plugin system. They may be added in the future.

SurfaceWhat it does
SchedulesCron-style triggers that fire on a recurring schedule.
AppsPersistent interactive apps (dashboards, games, tools) served in the workspace panel.
RoutesHTTP routes the assistant exposes, used for webhooks and integrations.
ArtifactsVersioned outputs the assistant produces and tracks (documents, diagrams, generated files).
WebhooksInbound HTTP endpoints that deliver external events into the assistant.
PromptsReusable system prompt fragments and templates.
UIsCustom UI surfaces rendered in the conversation or workspace.
BinCLI commands the assistant exposes as tools.
IntegrationsOAuth-connected and MCP-connected external services (Google, Linear, Slack, etc.) with credential management.
Slash commandsShortcuts triggered by typing / in the conversation, expanding into prompts or actions.
AgentsDelegated sub-agents with scoped roles, tools, and context windows.
WorkflowsMulti-step automated processes that chain tools, hooks, and model calls into reusable pipelines.

When should my assistant write a Plugin?

Reach for a plugin when you want to package a capability to share, version, or install across assistants, rather than extend only your own. The plugin is the distribution unit: its package.json manifest, the @vellumai/plugin-api peer dependency, and the install flow exist to make hooks, tools, and skills portable and discoverable.

The Personal AI you were promised

GET STARTED