Plugin Builder
Build on top of Vellum with plugins. A plugin bundles multiple surfaces into a single installable package that extends what an assistant can do.
Plugins are in beta. The peer-dep range you declare is what gets you load. Treat everything you write as something that can break between Vellum releases until 1.0 ships, and pin a real range.
What is a plugin?
A plugin is a directory in the assistant's workspace (<workspaceDir>/plugins/<name>/) that groups different surfaces into one cohesive capability. The assistant can build plugins directly in this folder or install one from the community via the CLI:
assistant plugins install <name>
Plugins can also be discovered and managed from the Plugins tab in the app, or searched from the CLI with assistant plugins search. The catalog is a curated allowlist that the Vellum team approves and curates.
The surfaces a plugin can bundle
A single plugin can contribute several different kinds of behavior. Each surface is discovered by convention from a named subdirectory. Missing directories are simply skipped, so a plugin contributes only what it ships.
| Surface | Lives in | What it does |
|---|---|---|
| Lifecycle hooks | hooks/<name>.ts |
Run code at fixed points in the Assistant's lifecycle to read or transform what flows through, and broadcast progress to the UI. |
| Skills | skills/<name>/ |
Directories of instructions and associated assets, scripts, and resources that the Assistant loads dynamically when relevant. |
| Model-visible tools | tools/<name>.ts |
Add new tools the model can call. Plugin tools land in the same catalog as built-in tools. |
| MCP servers | mcp.json |
Declare MCP servers the assistant connects on install. Their tools land as mcp__<id>__<tool> alongside workspace-configured MCP tools. |
| HTTP routes | routes/<path>.ts |
Serve HTTP endpoints in the plugin's own /x/plugins/<name>/ namespace (apps, local callers, and the handler behind public ingress). |
| Channels | channels/ingress.json |
Declare public webhook and WebSocket routes that make the plugin a channel. The gateway verifies them and forwards to matching routes. |
| Apps | apps/<name>/ |
Ship persistent interactive apps (dashboards, trackers, visualizations) compiled from a Preact + TSX bundle and rendered in the workspace panel. |
The two extensibility patterns serve different goals. Plugins are for distribution: you intend to share the capability, publish to the marketplace, or install it across multiple assistants. The plugin manifest (package.json), the @vellumai/plugin-api peer dependency, and the install flow exist to make a capability portable, versioned, and discoverable by others.
Direct workspace contributions are for personal extension: you simply want to extend your assistant and have no intention of distributing the work. Skip the plugin packaging entirely. Drop the file directly into the matching top-level workspace directory (/workspace/tools/<name>/ for a tool, /workspace/skills/<name>/ for a skill, /workspace/mcp.json for MCP servers) and the assistant picks it up automatically. No manifest, no install step, no peer dependency. A plugin is the way to ship those same servers with a versioned, installable unit.
Several surfaces that plugins contribute run in the same process as the main Assistant process. They can import all internal methods from the Assistant from the single public package, @vellumai/plugin-api, which is the only supported contract. Anything not exported from there is internal and can change without notice. See references/plugins.md for the full export surface.
Before you write a single file
Ask before building. Six questions, in this order. Stop if the user is unclear on any of them.
- What job does the plugin do? One sentence, plain language. If you cannot write this, the plugin should not be built yet.
- Which surfaces does it ship? Pick from the surfaces table above. Most plugins ship one or two, not all of them. See
references/plugins.mdfor the directory layout and manifest, and the surface-specific references for each surface's contract. - Does it need credentials? An API key, OAuth token, or webhook secret is not a value that belongs in a
.tsfile. For LLM inference credentials, usegetConfiguredProvider()from@vellumai/plugin-apito route through the workspace's stored credentials without handling plaintext. For other credential types (OAuth tokens, webhook secrets), store them via the credential vault and resolve at runtime withresolveCredential()from@vellumai/plugin-api, which returns the plaintext value scoped to the service named after your plugin. CatchCredentialResolutionErrorto degrade gracefully. - Does it keep state? A plugin is fully self-contained: durable state lives in its
data/directory (InitContext.pluginStorageDir), with schema created idempotently by theinithook, handles closed inshutdown, and per-conversation rows purged inconversation-deleted. A plugin never persists state in the assistant's database or elsewhere in the workspace. See "State is plugin-owned" inreferences/plugins.md. - Where will the source live? A GitHub repo, ideally under the user's own namespace. The marketplace entry pins to a full commit SHA.
- Is the user writing TypeScript or compiling ahead? In-repo Bun/Node compile on assistant start is the default. If they want a different build, ask now.
You have an alignment problem if the user cannot answer questions 1 and 2. Push back and clarify before scaffolding. The most expensive waste of plugin-authoring time is building a plugin whose job is fuzzy.
Scaffold the directory
Choose a kebab-case directory name. It becomes the install name. @scope/<name> is allowed; the loader strips the scope for the runtime plugin name. Duplicate names fail registration. See references/plugins.md for the full directory layout, manifest fields, and loader rules.
To exercise the plugin locally before pushing to the catalog, you have two options:
Option A: direct copy. Copy the directory into the workspace's plugins/ folder:
cp -R my-plugin $VELLUM_WORKSPACE_DIR/plugins/my-plugin
Option B: install from a GitHub URL (untrusted). If the plugin is already pushed to a public GitHub repo, install it directly without waiting for marketplace review:
assistant plugins install https://github.com/owner/my-plugin
assistant plugins install https://github.com/owner/repo/tree/my-branch/packages/my-plugin
assistant plugins install owner/repo --name my-plugin
A URL install bypasses the marketplace entirely: the tree is cloned verbatim (no adapter stub is overlaid) and the source is untrusted. The CLI prints a yellow warning naming the source. See references/distribution.md for the full details.
Verify before shipping
- Plugin directory copied into
plugins/<name>/,assistant plugins listshows statusok(noterror, notskipped). assistant plugins inspect <name>reportsup-to-dateanddrift: none.- Each surface the plugin ships exercised on a real code path — invoked, fired, loaded, or opened the way a user would reach it.
- Compiled files win: if you ship both
.jsand.tsfor the same basename, the.jsis loaded.
If a surface fails to load or fire, see references/plugins.md for loader rules and references/distribution.md for the CLI diagnostic commands.
Shipping to the catalog
See references/distribution.md for the full publishing walkthrough (push to GitHub, add a marketplace.json entry with a copy-pasteable template, and what the review checks), plus the manifest schema, CLI commands, and commit-pinning rules.
Once merged, users install by name: assistant plugins install my-plugin. The new plugin is picked up automatically.
SKILL COMPLETE WHEN
- Job and surfaces locked in the alignment pass (questions 1 and 2 answered).
- Directory matches the loader convention: one subdirectory (or declared file) per surface it ships (see the surfaces table), plus an optional
src/for internal modules. package.jsondeclaresname,version, and a realpeerDependencies["@vellumai/plugin-api"]range.- Any durable state lives in
data/, created byinitand cleaned up byshutdown/conversation-deleted. - Each surface has been exercised locally with a working example.
- A
marketplace.jsonentry exists with a full SHA insource.ref, and the Vellum team's review is in flight.
Reference files
references/plugins.md: Directory layout, manifest fields, and the full@vellumai/plugin-apiexport surface.references/hooks.md: Every lifecycle hook with its context fields, the agent loop diagram, resolution order, and a hook anatomy example.references/tools.md: Tool definition fields, the execute context, result shape, resolution order, and a tool anatomy example.references/skills.md: Frontmatter reference, resolution order, and a skill anatomy example.references/mcp.md: Rootmcp.jsondeclarations, transports, server ids, credentials, and risk defaults.references/routes.md: The/x/plugins/<name>/namespace, path mapping, handler signature, and a route anatomy example.references/channels.md:channels/ingress.json, public/webhooks/plugins/<name>/routes, guardian approval, verification, and inbound delivery.references/apps.md: The Preact + TSX app structure,src/→dist/compilation, theplugins~<name>~<app>id scheme, serving in the workspace panel, and an app anatomy example.references/distribution.md: Marketplace catalog, CLI commands, drift and upgrades, the manifest schema, commit pinning, and adapters.