Building Extensions
This guide walks through creating an Pllan extension from scratch. Extensions can add channels, model providers, tools, or other capabilities.Prerequisites
- Pllan repository cloned and dependencies installed (
pnpm install) - Familiarity with TypeScript (ESM)
Extension structure
Every extension lives underextensions/<name>/ and follows this layout:
Step 1: Create the package
Createextensions/my-channel/package.json:
pllan field tells the plugin system what your extension provides.
For provider plugins, use providers instead of channel.
Step 2: Define the entry point
Createextensions/my-channel/index.ts:
definePluginEntry instead.
Step 3: Import from focused subpaths
The plugin SDK exposes many focused subpaths. Always import from specific subpaths rather than the monolithic root:| Subpath | Purpose |
|---|---|
plugin-sdk/core | Plugin entry definitions, base types |
plugin-sdk/channel-setup | Optional setup adapters/wizards |
plugin-sdk/channel-pairing | DM pairing primitives |
plugin-sdk/channel-reply-pipeline | Prefix + typing reply wiring |
plugin-sdk/channel-config-schema | Config schema builders |
plugin-sdk/channel-policy | Group/DM policy helpers |
plugin-sdk/secret-input | Secret input parsing/helpers |
plugin-sdk/webhook-ingress | Webhook request/target helpers |
plugin-sdk/runtime-store | Persistent plugin storage |
plugin-sdk/allow-from | Allowlist resolution |
plugin-sdk/reply-payload | Message reply types |
plugin-sdk/provider-onboard | Provider onboarding config patches |
plugin-sdk/testing | Test utilities |
channel-runtime
or other larger helper barrels only when a dedicated subpath does not exist yet.
Step 4: Use local barrels for internal imports
Within your extension, create barrel files for internal code sharing instead of importing through the plugin SDK:./api.ts or ./runtime-api.ts instead. The SDK contract is for
external consumers only.
Step 5: Add a plugin manifest
Createpllan.plugin.json in your extension root:
Step 6: Test with contract tests
Pllan runs contract tests against all registered plugins. After adding your extension, run:Lint enforcement
Three scripts enforce SDK boundaries:- No monolithic root imports —
pllan/plugin-sdkroot is rejected - No direct src/ imports — extensions cannot import
../../src/directly - No self-imports — extensions cannot import their own
plugin-sdk/<name>subpath
pnpm check to verify all boundaries before committing.
Checklist
Before submitting your extension:-
package.jsonhas correctpllanmetadata - Entry point uses
defineChannelPluginEntryordefinePluginEntry - All imports use focused
plugin-sdk/<subpath>paths - Internal imports use local barrels, not SDK self-imports
-
pllan.plugin.jsonmanifest is present and valid - Contract tests pass (
pnpm test:contracts) - Unit tests colocated as
*.test.ts -
pnpm checkpasses (lint + format) - Doc page created under
docs/channels/ordocs/plugins/