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 — 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) 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 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 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 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)

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:

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

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

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →