How to Extend Paperclip Using the Plugin System: A Complete Developer Guide
Extend Paperclip using the plugin system by creating a manifest file that declares capabilities and entrypoints, then implementing a worker with definePlugin() that receives a PluginContext for logging, secrets, HTTP, jobs, events, and UI integration.
Paperclip's architecture centers on a typed plugin SDK and manifest-driven registration model. This article walks through the core concepts, runtime flow, and working code examples from the paperclipai/paperclip repository to help you build production-ready extensions.
Understanding the Plugin Architecture
Every Paperclip plugin consists of two required parts that work together to extend the platform securely and predictably.
The Manifest: Declaring What Your Plugin Does
The manifest is a TypeScript or JSON file that Paperclip's host reads at load time to understand how to wire your plugin into the system. It lives at a predictable path like src/manifest.ts and exports a PaperclipPluginManifestV1 object.
Key fields include:
id— Stable namespaced identifier (e.g.,paperclip.novita-sandbox-provider)apiVersion— Currently1capabilities— Strings the host matches against runtime abilities (e.g.,"environment.drivers.register","ui.dashboardWidget.register")entrypoints— Paths to compiled worker (./dist/worker.js) and optional UI bundle (./dist/ui)- Capability-specific sections — Such as
environmentDriversorui.slotsthat declare configuration schemas and registration points
The manifest serves as the single source of truth. The host validates it before launching your worker process.
The Worker: Implementing Runtime Behavior
The worker runs inside a sandboxed Node process. You author it using definePlugin() from @paperclipai/plugin-sdk, which returns a plugin definition object. The host calls lifecycle hooks—setup, onHealth, validateConfig, and capability-specific handlers—via an RPC protocol.
The SDK creates a runtime wrapper that exposes the host-provided PluginContext to your code. This context is your gateway to Paperclip's platform services.
Core SDK: definePlugin and PluginContext
The definePlugin implementation in packages/plugins/sdk/src/define-plugin.ts is the entry point for every worker. It receives a shape matching the PluginDefinition type and handles the RPC translation layer.
Lifecycle Hooks
| Hook | Purpose | When Called |
|---|---|---|
setup(ctx) |
Initialize subscriptions, jobs, data sources | Once at worker startup |
onHealth() |
Return health status | Periodic probes by host |
validateConfig(params) |
Validate plugin configuration | Before activation, on config changes |
| Capability RPCs | Implement specific features | On-demand via protocol |
PluginContext Capabilities
The PluginContext object (defined in packages/plugins/sdk/src/types.ts) provides:
ctx.logger— Structured logging with levelsctx.secrets.resolve(ref, options)— Retrieve secrets with company scopingctx.http.fetch(url, init)— Make authenticated HTTP requestsctx.events.on(event, handler)— Subscribe to platform eventsctx.jobs.register(name, handler)— Register background jobsctx.data.register(name, fetcher)— Expose data to UI componentsctx.state.get/set— Read and write persistent state
The protocol implementation in packages/plugins/sdk/src/protocol.ts serializes all requests and responses between host and worker.
Runtime Flow: From Discovery to Shutdown
Understanding how Paperclip loads and runs plugins helps you design for reliability and performance.
- Discovery — The server scans
packages/plugins/**/manifest.ts(or user-supplied folders) and loads each manifest - Capability negotiation — The host checks declared
capabilitiesagainst its runtime configuration - Worker launch —
runWorker(plugin, import.meta.url)spawns a child process and establishes bidirectional RPC - Setup phase — Host calls
setup(ctx); plugin initializes resources - Health and config validation — Ongoing probes and validation cycles
- Graceful shutdown — RPC
shutdownsignal allows cleanup
Example 1: Minimal UI-Only Plugin
This "Hello World" example from packages/plugins/examples/plugin-hello-world-example/ demonstrates the simplest valid plugin structure.
Manifest (src/manifest.ts)
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const PLUGIN_ID = "paperclip.hello-world-example";
const PLUGIN_VERSION = "0.1.0";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: PLUGIN_VERSION,
displayName: "Hello World Widget (Example)",
description: "Reference UI plugin that adds a simple Hello World widget to the Paperclip dashboard.",
author: "Paperclip",
categories: ["ui"],
capabilities: ["ui.dashboardWidget.register"],
entrypoints: {
worker: "./dist/worker.js",
ui: "./dist/ui",
},
ui: {
slots: [
{
type: "dashboardWidget",
id: "hello-world-dashboard-widget",
displayName: "Hello World",
exportName: "HelloWorldDashboardWidget",
},
],
},
};
export default manifest;
Worker (src/worker.ts)
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
const PLUGIN_NAME = "hello-world-example";
const plugin = definePlugin({
async setup(ctx) {
ctx.logger.info(`${PLUGIN_NAME} plugin setup complete`);
},
async onHealth() {
return { status: "ok", message: "Hello World example plugin ready" };
},
});
export default plugin;
runWorker(plugin, import.meta.url);
Source files: manifest.ts, worker.ts
Example 2: Capability-Rich Sandbox Provider
The Novita sandbox provider plugin at packages/plugins/sandbox-providers/novita/ shows how to extend Paperclip with complex configuration and RPC handlers.
Manifest with Config Schema (src/manifest.ts)
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const PLUGIN_ID = "paperclip.novita-sandbox-provider";
const PLUGIN_VERSION = "0.1.0";
const manifest: PaperclipPluginManifestV1 = {
id: PLUGIN_ID,
apiVersion: 1,
version: PLUGIN_VERSION,
displayName: "Novita Sandbox Provider",
description:
"Sandbox provider plugin that provisions Novita Agent Sandbox environments for Paperclip agent runs.",
author: "Novita AI",
categories: ["automation"],
capabilities: ["environment.drivers.register"],
entrypoints: {
worker: "./dist/worker.js",
},
environmentDrivers: [
{
driverKey: "novita",
kind: "sandbox_provider",
displayName: "Novita Agent Sandbox",
description:
"Provisions Novita Agent Sandbox instances with configurable templates, idle timeout, workspace path, and lease reuse.",
configSchema: {
type: "object",
properties: {
apiKey: { type: "string", format: "secret-ref", description: "Novita API key or secret reference." },
domain: { type: "string", description: "Optional API domain." },
template: { type: "string", description: "Sandbox template ID or name." },
requestedCwd: { type: "string", default: "/home/user/paperclip-workspace", description: "Workspace directory." },
timeoutMs: { type: "number", default: 300_000, description: "Sandbox lifetime (ms)." },
requestTimeoutMs: { type: "number", default: 30_000, description: "SDK request timeout (ms)." },
secure: { type: "boolean", default: true, description: "Use secure connections." },
autoPause: { type: "boolean", default: false, description: "Enable auto-pause." },
reuseLease: { type: "boolean", default: false, description: "Reuse leases across runs." }
},
},
},
],
};
export default manifest;
Worker with RPC Handlers (src/plugin.ts)
import { definePlugin } from "@paperclipai/plugin-sdk";
import type {
PluginEnvironmentAcquireLeaseParams,
PluginEnvironmentLease,
PluginEnvironmentValidateConfigParams,
PluginEnvironmentValidationResult,
} from "@paperclipai/plugin-sdk";
import { Sandbox } from "novita-sandbox";
export const plugin = definePlugin({
async validateConfig({ config }: PluginEnvironmentValidateConfigParams): Promise<PluginEnvironmentValidationResult> {
const errors = validateNovitaDriverConfig(parseNovitaDriverConfig(config));
return { ok: errors.length === 0, errors };
},
async acquireLease(params: PluginEnvironmentAcquireLeaseParams): Promise<PluginEnvironmentLease> {
const driverConfig = parseNovitaDriverConfig(params.config);
const sandbox = await createSandbox(params, driverConfig);
return { leaseId: sandbox.id, metadata: { provider: "novita" } };
},
// Additional RPCs: execute, realizeWorkspace, releaseLease, etc.
});
export default plugin;
Source files: manifest.ts, plugin.ts
Example 3: Full PluginContext Usage
This pattern demonstrates integrating all major PluginContext services for event-driven, stateful plugins with UI components.
import { definePlugin } from "@paperclipai/plugin-sdk";
export default definePlugin({
async setup(ctx) {
// Structured logging
ctx.logger.info("Custom plugin is initializing");
// Event subscription with secret resolution
ctx.events.on("issue.created", async (event) => {
const companyId = event.companyId;
const cfg = await ctx.config.get(companyId);
const apiKey = await ctx.secrets.resolve(cfg.apiKeyRef, {
companyId,
configPath: "apiKeyRef",
});
await ctx.http.fetch("https://api.example.com/notify", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ title: event.payload.title }),
});
});
// Background job registration
ctx.jobs.register("daily-sync", async (job) => {
ctx.logger.info("Running daily sync", { runId: job.runId });
// Sync implementation
});
// UI data source registration
ctx.data.register("sync-status", async ({ companyId }) => {
const state = await ctx.state.get({
scopeKind: "company",
scopeId: String(companyId),
stateKey: "last-sync",
});
return { lastSync: state };
});
},
async onHealth() {
return { status: "ok", message: "Custom plugin healthy" };
},
});
Adding New Capabilities to Paperclip
To extend the platform itself with new plugin capabilities:
- Add capability string to the host's capability registry (e.g.,
"myFeature.doSomething") - Update SDK with typed method on
PluginContextimplementing the capability - Document protocol in
doc/PLUGIN_SPEC.md - Publish plugin listing the new capability and implementing corresponding hooks
This pattern keeps the core modular while enabling ecosystem growth.
Scaffolding New Plugins
Use the official CLI generator to bootstrap a plugin package:
pnpm create-paperclip-plugin my-plugin
This creates the standard directory structure, TypeScript configuration, and build scripts. The generator source lives in packages/plugins/create-paperclip-plugin/src/index.ts.
Summary
- Two-part architecture: Manifest declares capabilities; worker implements behavior via
definePlugin() - Type-safe SDK:
PluginContextprovides logging, secrets, HTTP, events, jobs, data, and state - Sandboxed runtime: Workers run in isolated Node processes with RPC-based host communication
- Capability-driven: Host matches plugin declarations against runtime abilities for secure loading
- Full extensibility: Add UI widgets, environment drivers, background jobs, or custom platform features
Frequently Asked Questions
What is the minimum code needed for a valid Paperclip plugin?
A valid plugin requires a manifest with id, apiVersion, version, capabilities, and entrypoints.worker, plus a worker file that calls definePlugin() and runWorker(). The "Hello World" example above shows this in under 50 lines.
How does Paperclip handle plugin security and isolation?
Plugins run in sandboxed Node child processes spawned by runWorker(). The RPC protocol in packages/plugins/sdk/src/protocol.ts mediates all communication, and capabilities are explicitly declared in the manifest before the host loads any code.
Can plugins store persistent state?
Yes. Use ctx.state.get() and ctx.state.set() with scoped keys (scopeKind: "company" or "user"). State is managed by the host and persists across plugin restarts.
How do I expose UI components from my plugin?
Include a UI bundle path in entrypoints.ui and register slots in the manifest's ui.slots array. The UI bridge in ui/src/plugins/bridge.ts loads your bundle and wires components into Paperclip's React component tree.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →