Extensions
Package reusable eve capabilities and mount them from npm or a monorepo workspace.
Extensions package eve tools, channels, connections, skills, schedules, subagents, instruction fragments, and hooks. An author builds an extension package; each agent that uses it declares the package as a dependency and mounts it. The package can be published to a package registry or kept private inside a monorepo workspace.
Ready-made extensions can also be distributed through an eve integration registry. See Add Integrations to discover and add one with eve add; this page explains how extension packages are authored, mounted, configured, and overridden.
This enables sharing many different capability sets. A browser extension might include several tools for navigating a site. A self-improving extension could pair hooks with dynamic instructions.
Author: create an extension
Create the package
Start with the extension scaffold:
npx eve@latest extension init my-crmThe command creates the package, installs dependencies, and initializes Git. It includes extension/extension.ts, TypeScript configuration, and the package metadata required to build and publish.
An extension uses the same file conventions as an agent for its contributions:
@acme/crm/
package.json
extension/
extension.ts
tools/search.ts
channels/webhook.ts
connections/api.ts
skills/triage/SKILL.md
schedules/sync.ts
subagents/reviewer/agent.ts
instructions.md
hooks/audit.ts
lib/http.tsEach listed slot accepts the same authored forms as its agent counterpart. Static and dynamic tools, connections, skills, and instructions all work in an extension: extension/instructions.ts is as valid as extension/instructions.md, and extension/connections/ can contain defineDynamic(...).
Names come from paths, so call the tool search, not crm_search; the consumer's mount adds the crm__ prefix. The same prefix applies to channel, schedule, and parent-visible subagent IDs, while channel route paths and schedule cron expressions stay unchanged. Keep shared code in extension/lib/.
The extension root cannot declare agent configuration, instrumentation,
memory, a sandbox, or nested extensions. Those agent-level concerns
belong to the consuming application. A subagent contributed under
extension/subagents/ owns its own agent configuration, memory, and sandbox
like any other declared subagent.
Add configuration and contributions
The author's extension/extension.ts default-exports a defineExtension handle. Give it a Standard Schema when consumers need to provide settings:
import { defineExtension } from "eve/extension";
import { z } from "zod";
export default defineExtension({
config: z.object({
apiKey: z.string(),
baseUrl: z.string().url().default("https://api.acme.example"),
}),
});Contributions, including schedule handlers, can import that handle to read the validated configuration. Defaults have already been applied:
import { defineTool } from "eve/tools";
import { z } from "zod";
import extension from "../extension";
export default defineTool({
description: "Search the CRM.",
inputSchema: z.object({ query: z.string() }),
async execute({ query }) {
const { apiKey, baseUrl } = extension.config;
return { query, baseUrl, authenticated: apiKey.length > 0 };
},
});If no configuration is needed, export defineExtension() and let consumers re-export it directly. Config schemas must validate synchronously.
defineState uses a durable key scoped to the logical mount path and the authored state name. Two mounts of the same package can use the same state name without sharing a slot in one context. Contributed subagents use their parent extension's mount identity, but retain their own runtime contexts.
Add a subagent
Author a subagent under extension/subagents/<id>/ using the same files as a subagent declared by an agent. Mounting the extension as crm exposes extension/subagents/reviewer/ to the consuming agent node as crm__reviewer. The subagent's own tools, connections, skills, hooks, instructions, sandbox, and nested subagents remain isolated inside its node and keep their path-derived names.
Modules inside the contributed subagent can import the extension handle. For example, a tool under extension/subagents/reviewer/tools/ can read the configuration bound by the consumer's agent/extensions/crm.ts mount.
A contributed subagent can mount another extension under extension/subagents/<id>/extensions/ and derive that mount's configuration from its own extension's configuration:
// extension/subagents/reviewer/extensions/search.ts
import search from "@acme/search";
import crm from "../../../extension.js";
export default search({ apiKey: crm.config.searchApiKey });Build and optionally publish
The scaffold's package.json declares separate source and distribution roots:
{
"name": "my-crm",
"version": "0.0.0",
"type": "module",
"eve": {
"extension": {
"source": "./extension",
"dist": "./dist/extension",
"externalDependencies": ["@acme/runtime-sdk"],
},
},
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs",
},
"./tools": {
"types": "./dist/tools/index.d.ts",
"default": "./dist/tools/index.mjs",
},
},
"scripts": {
"build": "eve extension build",
"prepare": "eve extension build",
"typecheck": "tsc",
},
"dependencies": {
"@acme/runtime-sdk": "^x",
"zod": "^x",
},
"devDependencies": {
"@types/node": "^x",
"eve": "x.y.z",
"typescript": "^x",
},
"peerDependencies": {
"eve": "*",
},
"engines": {
"node": ">=24",
},
}The scaffold omits engines when it creates a workspace package.
Build the package with eve extension build:
eve extension buildeve extension build writes an agent-shaped dist/extension tree, copies skill assets, emits declarations, and records compatibility metadata. It also manages the package exports for the mount factory (@acme/crm) and tool definitions (@acme/crm/tools). Publish dist/; consumers do not need the author's TypeScript source.
The exact eve development pin controls the extension authoring API and build tooling. The wildcard peer lets the consumer provide the runtime copy of eve. At consumption time, eve checks generated metadata, not the npm peer range. Do not add eve to regular dependencies.
Put runtime packages such as zod or an SDK in dependencies. Most dependencies are bundled into the consuming agent automatically.
When a package must keep normal Node.js package layout at runtime, add it to eve.extension.externalDependencies. Common cases include native addons and SDKs that load package-relative assets. eve extension build requires each listed package to also appear in dependencies, optionalDependencies, or peerDependencies, and records the requirement in the generated compatibility manifest. The consuming eve keeps the package external and preserves its complete package tree; consumers do not need to edit agent.ts or install the transitive package directly.
Consumers can now add the built package to an agent. A workspace-only extension uses the same package contract but does not need to be published; see Use an extension in a workspace.
Consumer: install and mount an extension
A mount gives the extension's contributions a namespace. Updating the package updates the mounted extension; nothing is copied into the consumer's agent.
Install the package
Install the extension with the package manager already used by the consumer's agent project. Fresh eve projects use pnpm:
pnpm add @acme/crmMount it
Create a file under agent/extensions/. Its filename becomes the mount namespace. Call the extension's default export when it needs configuration:
import crm from "@acme/crm";
export default crm({ apiKey: process.env.CRM_API_KEY! });Set CRM_API_KEY in the consumer's environment, such as .env.local for local development.
The mount adds crm__ to named contributions: tools/search.ts becomes crm__search, channels/webhook.ts becomes crm__webhook, schedules/sync.ts becomes crm__sync, connections/api.ts becomes crm__api, and subagents/reviewer/ becomes crm__reviewer. Channels keep their declared route paths, and schedules keep their cron expressions.
For an extension with no configuration, mount its default export directly:
export { default } from "@acme/gizmo";The same mount shape works with an npm package, a workspace dependency, or a linked local package. Each mount binds its own configuration, even when two mounts use the same package. Moving or renaming a mount creates a new instance.
Extension state belongs to the logical mount path (for example, extensions/crm or subagents/research/extensions/crm). A flat crm.ts mount and a directory crm/extension.ts mount have the same identity; moving or renaming the mount changes its state keys. Application-defined state keys are unchanged.
Upgrade from package-scoped extension state
Deployments before eve 0.69 stored extension state under package-prefixed keys, such as acme-crm.requests. When a session from one of those deployments hands off to a newer deployment, eve moves each package-prefixed value to the mount that uses that package.
- The value moves when exactly one mount of that package defines that state name.
- When no mount of that package defines that state name, for example because the extension removed it or the package was renamed, eve drops the value. The target deployment's runtime logs show a
dropping unknown context key during deserializationwarning with the key. - When two or more mounts use the package and define that state name, eve cannot tell which mount owns the value, so the handoff is rejected and the session stays on its current deployment. Keep that deployment available until the session finishes, or start a new session on the deployment you want to use.
- Older deployments cannot read sessions saved by a newer release, so a session that already moved does not hand back after a rollback.
- Local context snapshots follow the same rules.
Use an extension in a workspace
A workspace extension is a regular extension package kept in the same monorepo as its consumers. It is useful when several agents need the same capabilities, or when a private capability should evolve alongside the agents that use it.
For example, a pnpm workspace can keep one extension next to two independently deployable agents:
acme-agents/
├── pnpm-workspace.yaml
├── packages/
│ └── shared-capabilities/
│ ├── package.json
│ └── extension/
│ ├── extension.ts
│ ├── tools/
│ ├── skills/
│ └── hooks/
└── agents/
├── support/
│ ├── package.json
│ └── agent/extensions/shared.ts
└── operations/
├── package.json
└── agent/extensions/shared.tsMake both the extension and agent directories workspace members:
packages:
- "agents/*"
- "packages/*"You can scaffold the extension from a directory already covered by the workspace configuration:
cd packages
npx eve@latest extension init shared-capabilitiesGive the generated package the name consumers will import. Add "private": true if it should never be published:
{
"name": "@acme/shared-capabilities",
"private": true,
"eve": {
"extension": {
"source": "./extension",
"dist": "./dist/extension",
},
},
}Each consuming agent declares its own workspace dependency:
{
"dependencies": {
"@acme/shared-capabilities": "workspace:*",
},
}Then each agent mounts the package:
export { default } from "@acme/shared-capabilities";The mount is intentionally per agent. Each consumer chooses its own mount namespace and, for a configured extension, passes its own configuration. For example, shared.ts contributes shared__search, while mounting the same package as company.ts in another agent contributes company__search.
Develop from source
When eve dev starts a consuming agent, it builds mounted, source-backed extensions found inside the same workspace before compiling the agent. It watches the extension source and relevant package and TypeScript configuration, then rebuilds only the affected extension. If an extension edit fails to build, the previous successful development generation keeps running.
Production builds build the same extensions from source. eve build builds each mounted, source-backed workspace extension before it compiles the agent, and withEve does the same for its agents during next build. Production builds therefore do not depend on the extension package's prepare script, which package managers skip for no-op installs and with --ignore-scripts. eve skips an extension whose distribution was built by the same eve version and is newer than the extension's source, package.json, and TypeScript configuration.
If an extension fails to build, the agent build stops with an error that names the extension package and its directory. Fix the reported error, or run eve extension build in that package directory to build it on its own.
Override a contribution
Use a directory mount to keep overrides beside the mount declaration. Put the declaration in extension.ts and add overrides beside it:
agent/extensions/crm/
extension.ts
tools/search.tsimport crm from "@acme/crm";
export default crm({ apiKey: process.env.CRM_API_KEY! });A same-named consumer channel, tool, connection, skill, schedule, or subagent wins. To adjust an extension tool, import it from the package's ./tools export and define it again:
import { search } from "@acme/crm/tools";
import { defineTool } from "eve/tools";
import { always } from "eve/tools/approval";
export default defineTool({ ...search, approval: always() });To remove an extension tool, use disableTool() in its matching slot:
import { disableTool } from "eve/tools";
export default disableTool();Hooks and instruction fragments are additive, so they cannot be replaced. To replace a dynamic tool, use a dynamic definition in the same slot; dynamic tools win over same-named static tools at runtime. disableTool() removes either kind.
You can also place an override in the corresponding agent-root slot by using the final qualified name. For example, agent/tools/crm__search.ts replaces tools/search.ts from the extension package or its directory override. Application sources have the highest precedence, so an agent-root override wins when both forms exist.
Use an extension tool result in a hook
To retain an extension tool's result type in a consumer hook, import its definition from ./tools and pass it to toolResultFrom:
import { defineHook } from "eve/hooks";
import { toolResultFrom } from "eve/tools";
import { search } from "@acme/crm/tools";
export default defineHook({
events: {
"action.result"(event) {
const match = toolResultFrom(event.data.result, search);
if (match) console.log(match.output);
},
},
});toolResultFrom recognizes the mounted crm__search result from the original definition, not the namespaced string. Publishers should keep tool descriptions distinct so eve can assign each definition an unambiguous identity.
Bundled development extensions
Local eve dev also mounts bundled development extensions without creating a project mount. The self-modification extension is included by default when eve dev starts a local server. Bundled development extensions are not included in production builds. See Self-Modification for the local workflow.
Compatibility
At build time, eve checks the extension's generated capability metadata. If the extension needs an unsupported capability contract, upgrade eve or install a compatible extension release.
What to read next
- Integrations: browse ready-to-install extensions using the Extensions filter
- Code extension: mount eve-code, the coding extension that ships in
eve, and see its benchmark results - Tools: static tools, approval, and tool output
- Dynamic capabilities: dynamic connections, tools, skills, and instructions
- Instructions: static and TypeScript instructions
- Skills: package procedures and supporting files
- Connections: integrate external services
- Channels: receive messages and expose routes
- Schedules: run the agent on a cron cadence
- Subagents: delegate to declared specialists
- Hooks: observe agent events