# How to Extend Paperclip Using the Plugin System: A Complete Developer Guide

> Extend Paperclip using its plugin system. Learn to create manifests and implement workers with definePlugin to integrate custom capabilities and enhance your AI workflows.

- Repository: [Paperclip/paperclip](https://github.com/paperclipai/paperclip)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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`](https://github.com/paperclipai/paperclip/blob/main/src/manifest.ts) and exports a `PaperclipPluginManifestV1` object.

Key fields include:

- **`id`** — Stable namespaced identifier (e.g., `paperclip.novita-sandbox-provider`)
- **`apiVersion`** — Currently `1`
- **`capabilities`** — Strings the host matches against runtime abilities (e.g., `"environment.drivers.register"`, `"ui.dashboardWidget.register"`)
- **`entrypoints`** — Paths to compiled worker ([`./dist/worker.js`](https://github.com/paperclipai/paperclip/blob/main/./dist/worker.js)) and optional UI bundle (`./dist/ui`)
- **Capability-specific sections** — Such as `environmentDrivers` or `ui.slots` that 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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/packages/plugins/sdk/src/types.ts)) provides:

- **`ctx.logger`** — Structured logging with levels
- **`ctx.secrets.resolve(ref, options)`** — Retrieve secrets with company scoping
- **`ctx.http.fetch(url, init)`** — Make authenticated HTTP requests
- **`ctx.events.on(event, handler)`** — Subscribe to platform events
- **`ctx.jobs.register(name, handler)`** — Register background jobs
- **`ctx.data.register(name, fetcher)`** — Expose data to UI components
- **`ctx.state.get/set`** — Read and write persistent state

The protocol implementation in [`packages/plugins/sdk/src/protocol.ts`](https://github.com/paperclipai/paperclip/blob/main/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.

1. **Discovery** — The server scans `packages/plugins/**/manifest.ts` (or user-supplied folders) and loads each manifest
2. **Capability negotiation** — The host checks declared `capabilities` against its runtime configuration
3. **Worker launch** — `runWorker(plugin, import.meta.url)` spawns a child process and establishes bidirectional RPC
4. **Setup phase** — Host calls `setup(ctx)`; plugin initializes resources
5. **Health and config validation** — Ongoing probes and validation cycles
6. **Graceful shutdown** — RPC `shutdown` signal 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`](https://github.com/paperclipai/paperclip/blob/main/src/manifest.ts))

```typescript
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`](https://github.com/paperclipai/paperclip/blob/main/src/worker.ts))

```typescript
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](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/examples/plugin-hello-world-example/src/manifest.ts), [worker.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/examples/plugin-hello-world-example/src/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`](https://github.com/paperclipai/paperclip/blob/main/src/manifest.ts))

```typescript
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`](https://github.com/paperclipai/paperclip/blob/main/src/plugin.ts))

```typescript
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](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sandbox-providers/novita/src/manifest.ts), [plugin.ts](https://github.com/paperclipai/paperclip/blob/master/packages/plugins/sandbox-providers/novita/src/plugin.ts)

---

## Example 3: Full PluginContext Usage

This pattern demonstrates integrating all major `PluginContext` services for event-driven, stateful plugins with UI components.

```typescript
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:

1. **Add capability string** to the host's capability registry (e.g., `"myFeature.doSomething"`)
2. **Update SDK** with typed method on `PluginContext` implementing the capability
3. **Document protocol** in [`doc/PLUGIN_SPEC.md`](https://github.com/paperclipai/paperclip/blob/main/doc/PLUGIN_SPEC.md)
4. **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:

```bash
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`](https://github.com/paperclipai/paperclip/blob/main/packages/plugins/create-paperclip-plugin/src/index.ts).

---

## Summary

- **Two-part architecture**: Manifest declares capabilities; worker implements behavior via `definePlugin()`
- **Type-safe SDK**: `PluginContext` provides 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`](https://github.com/paperclipai/paperclip/blob/main/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`](https://github.com/paperclipai/paperclip/blob/main/ui/src/plugins/bridge.ts) loads your bundle and wires components into Paperclip's React component tree.