Paperclip Plugin Architecture: How to Build Custom Plugins for Paperclip
Paperclip's extensibility is powered by a plugin SDK that uses a three-part architecture: a manifest declaring capabilities and UI slots, a worker process communicating over JSON-RPC, and an optional React UI bundle that renders inside the host application.
Paperclip is an open-source AI-powered workspace platform. Its modular design allows developers to extend core functionality without modifying the main codebase. This guide explains how the Paperclip plugin architecture works and how you can build your own custom plugins using the official SDK.
The Three Core Components of a Paperclip Plugin
Every Paperclip plugin consists of three mandatory or optional pieces located under packages/plugins/sdk:
- Manifest (
manifest.ts) — Declares identity, version, capabilities, UI slots, and entrypoints - Worker (
worker.ts) — Runs in an isolated process and implements lifecycle hooks via thePluginContextAPI - UI bundle (
ui/…) — Optional React components that render in Paperclip's interface
The Paperclip server spawns a worker process for each installed plugin. Communication happens over JSON-RPC 2.0 on stdio, with the contract defined in packages/plugins/sdk/src/protocol.ts.
Defining the Plugin Manifest
The manifest shape PaperclipPluginManifestV1 in src/manifest.ts drives discovery and security gating.
import type { PaperclipPluginManifestV1 } from "@paperclipai/plugin-sdk";
const manifest: PaperclipPluginManifestV1 = {
id: "my-awesome-plugin",
apiVersion: 1,
version: "0.1.0",
displayName: "My Awesome Plugin",
description: "Demo plugin that adds a sidebar link and a detail tab.",
author: "Your Name",
categories: ["ui"],
capabilities: [
"ui.sidebar.register",
"ui.detailTab.register",
"projects.read",
"plugin.state.read",
],
instanceConfigSchema: {
type: "object",
properties: {
showFeature: {
type: "boolean",
title: "Show Feature",
default: true,
},
},
},
entrypoints: {
worker: "./dist/worker.js",
ui: "./dist/ui",
},
ui: {
slots: [
{
type: "projectSidebarItem",
id: "my-sidebar-link",
displayName: "My Feature",
exportName: "MySidebarLink",
entityTypes: ["project"],
order: 5,
},
{
type: "detailTab",
id: "my-detail-tab",
displayName: "My Tab",
exportName: "MyDetailTab",
entityTypes: ["project"],
order: 5,
},
],
},
};
export default manifest;
Source: [packages/plugins/examples/plugin-file-browser-example/src/manifest.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/examples/plugin-file-browser-example/src/manifest.ts)
Key Manifest Fields
capabilities— Strings that gate what the worker may call on the host, such asui.sidebar.register,projects.read, orplugin.state.readinstanceConfigSchema— JSON Schema describing per-instance settings surfaced in the admin UIentrypoints— Paths to compiled worker (dist/worker.js) and UI bundle (dist/ui)ui.slots— Declares extension points:projectSidebarItem,detailTab,commentAnnotation, and more
Building the Worker with definePlugin
The worker entrypoint calls definePlugin from @paperclipai/plugin-sdk. The only required hook is setup(ctx), where you register handlers.
import { definePlugin } from "@paperclipai/plugin-sdk";
export default definePlugin({
async setup(ctx) {
// Register UI data source
ctx.data.register("my-feature-enabled", async ({ companyId }) => {
const cfg = await ctx.config.get(companyId);
return { enabled: cfg.showFeature ?? false };
});
// Subscribe to domain events
ctx.events.on("issue.created", async (event) => {
const { companyId, payload } = event;
ctx.logger.info("New issue created", { issueId: payload.id, companyId });
await ctx.http.fetch(`https://api.example.com/notify`, {
method: "POST",
body: JSON.stringify({ issueId: payload.id }),
});
});
// Register background job
ctx.jobs.register("full-sync", async (job) => {
// Long-running work here
});
},
async onHealth() {
return { status: "ok", message: "All systems nominal" };
},
});
Source: [packages/plugins/sdk/src/define-plugin.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sdk/src/define-plugin.ts)
The PluginContext API
The PluginContext interface in packages/plugins/sdk/src/types.ts exposes these capabilities:
| API | Purpose |
|---|---|
ctx.events.on(event, handler) |
Subscribe to host-emitted domain events |
ctx.jobs.register(id, handler) |
Register background jobs |
ctx.data.register(key, fetcher) |
Provide data to UI components |
ctx.config.get(companyId) |
Resolve company-scoped configuration |
ctx.secrets.resolve(ref) |
Resolve secret references |
ctx.http.fetch(url, options) |
Perform outbound HTTP calls |
ctx.state.get/set(params) |
Read/write scoped key-value state |
ctx.logger.info/warn/error |
Structured logging |
ctx.activity.log(entry) |
Emit audit activity logs |
ctx.metrics.write(record) |
Emit numeric metrics |
ctx.telemetry.track(event) |
Track custom telemetry events |
ctx.span.record(span) |
Record provider spans for tracing |
Source: [packages/plugins/sdk/src/types.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sdk/src/types.ts)
Host-Worker RPC Protocol
All method calls are typed in HostToWorkerMethods (host calls worker) and WorkerToHostMethods (worker calls host). The host invokes initialize, configChanged, getData, performAction, and environment* methods. The worker calls back for services like config.get, state.get, and http.fetch.
Errors are namespaced under PLUGIN_RPC_ERROR_CODES in packages/plugins/sdk/src/protocol.ts. For example, CROSS_TENANT_CONFIG fires when a single-tenant plugin receives configuration for a second company.
Source: [packages/plugins/sdk/src/protocol.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sdk/src/protocol.ts)
Creating React UI Components
When your manifest declares UI slots, the compiled UI bundle must export components matching each exportName. The host injects these into the appropriate location and supplies data from ctx.data.register.
import * as React from "react";
import { usePluginData } from "@paperclipai/plugin-sdk";
export const MySidebarLink = () => {
const { data, loading } = usePluginData("my-feature-enabled");
if (loading) return null;
if (!data?.enabled) return null;
return (
<a href="#" onClick={() => alert("Clicked My Feature!")}>
My Feature
</a>
);
};
The component name MySidebarLink must match the exportName in your manifest. The host loads the UI bundle under a sandbox that respects your declared capabilities.
Optional: Sandbox Providers for Isolated Execution
For plugins requiring isolated runtimes—such as Docker containers or remote VMs—the SDK defines a sandbox driver interface via environment* methods. Reference implementations live in packages/plugins/sandbox-providers/:
novita— Serverless GPU sandbox providere2b— E2B code sandboxdaytona— Daytona workspace environment
Opt in by including environment.* in your capabilities array and implementing the corresponding RPC handlers.
Source: [packages/plugins/sdk/src/protocol.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sdk/src/protocol.ts)
Installing Your Custom Plugin
Build and install from the host CLI:
# Build the plugin (monorepo checkout)
pnpm --filter @paperclipai/plugin-file-browser-example build
# Install via local path (development)
npx paperclipai plugin install ./packages/plugins/examples/plugin-file-browser-example
The host persists your manifest in the database, resolves the worker entrypoint at dist/worker.js, and spawns the process. Uninstalling stops the worker and removes the manifest.
Source: [packages/plugins/examples/plugin-file-browser-example/README.md](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/examples/plugin-file-browser-example/README.md)
Summary
- Paperclip plugin architecture consists of a manifest, worker process, and optional UI bundle
- The manifest (
manifest.ts) declares capabilities that gate host API access and UI slots that determine where components render - The worker uses
definePluginfrom@paperclipai/plugin-sdkto register event handlers, jobs, data providers, and lifecycle hooks - JSON-RPC 2.0 over stdio enables typed, bidirectional communication between host and worker
- The PluginContext API in
packages/plugins/sdk/src/types.tsprovides 12+ subsystems for configuration, state, HTTP, logging, metrics, and more - UI components receive data via
ctx.data.registerand render inside capability-restricted sandboxes - Sandbox providers enable isolated execution environments for security-sensitive operations
Frequently Asked Questions
How does Paperclip isolate plugins from the main application?
Paperclip spawns each plugin in a separate worker process and communicates over JSON-RPC on stdio. The host enforces capability-based access control—plugins can only call host methods explicitly listed in their manifest's capabilities array. Sandboxed execution environments are available for additional isolation.
What programming language are Paperclip plugins written in?
Plugins are written in TypeScript and compiled to JavaScript. The SDK (@paperclipai/plugin-sdk) provides full type safety for the manifest, context APIs, and RPC protocol. UI components use React with hooks provided by the SDK.
Can a plugin access data across multiple companies or tenants?
No. The plugin system enforces tenant isolation. If a single-tenant plugin receives configuration for a second company, the host returns PLUGIN_RPC_ERROR_CODES.CROSS_TENANT_CONFIG. The PluginContext automatically scopes state, configuration, and secrets to the requesting company's ID.
Where can I find complete working examples of Paperclip plugins?
The repository includes packages/plugins/examples/plugin-file-browser-example/ demonstrating sidebar items and detail tabs. For workspace manipulation examples, see packages/plugins/plugin-workspace-diff/src/. Sandbox provider implementations are in packages/plugins/sandbox-providers/.
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 →