casen Plugin Authoring
casen plugins are ordinary npm packages that export a CasenPlugin object. Once installed, their
commands appear in the main TUI and in shell tab-completion alongside the built-in ones.
Scaffold a new plugin
Section titled “Scaffold a new plugin”The fastest way to start is with the official scaffolding tool:
pnpm create @bpmnkit/casen-plugin# or: npx @bpmnkit/create-casen-plugin# or: bunx @bpmnkit/create-casen-pluginThe tool runs interactively:
@bpmnkit/create-casen-plugin — casen plugin scaffolding
Plugin name (npm package name): casen-deploy Display name (Deploy): Description (): Git-tag-aware deploys for casen Author (): acme
Initialize git repo? (Y/n): Y
✓ package.json ✓ tsconfig.json ✓ src/index.ts ✓ .gitignore ✓ git init
Done! Created casen-deploy at ./casen-deployNon-interactive mode
Section titled “Non-interactive mode”Pass flags to skip all prompts — useful in CI or cookiecutter scripts:
pnpm create @bpmnkit/casen-plugin \ --name casen-deploy \ --description "Git-tag-aware deploys for casen" \ --author acme \ --no-git| Flag | Description |
|---|---|
--name / -n | npm package name (first positional arg also works) |
--display-name | Human-readable name shown in casen plugin list |
--description / -d | One-line description |
--author / -a | Author name or npm username |
--no-git | Skip git init |
What gets generated
Section titled “What gets generated”casen-deploy/├── package.json # "casen-plugin" keyword pre-set; no manual step needed├── tsconfig.json # standalone, no monorepo dependency├── .gitignore└── src/ └── index.ts # CasenPlugin default export with an example commandGenerated package.json
Section titled “Generated package.json”{ "name": "casen-deploy", "version": "0.1.0", "description": "Git-tag-aware deploys for casen", "type": "module", "main": "dist/index.js", "keywords": ["casen-plugin"], "scripts": { "build": "tsc", "build:watch": "tsc --watch", "prepublishOnly": "tsc" }, "devDependencies": { "@bpmnkit/cli-sdk": "latest", "typescript": "latest" }}The "casen-plugin" keyword is injected automatically — it is how casen plugin search discovers
your package on npm once published.
Generated src/index.ts
Section titled “Generated src/index.ts”import type { CasenPlugin } from "@bpmnkit/cli-sdk"
const plugin: CasenPlugin = { id: "com.acme.casen-deploy", name: "Deploy", version: "0.1.0", groups: [ { name: "deploy", description: "Deploy commands", commands: [ { name: "hello", description: "Example command — replace with your own", async run(ctx) { ctx.output.ok("Hello from Deploy!") }, }, ], }, ],}
export default pluginReplace the example command with your own logic. Add as many command groups and commands as needed.
The @bpmnkit/cli-sdk SDK
Section titled “The @bpmnkit/cli-sdk SDK”Install the SDK as a devDependency in your plugin:
pnpm add -D @bpmnkit/cli-sdkIt exports all types you need — no runtime dependency on casen internals.
CasenPlugin
Section titled “CasenPlugin”The root export. Default-export one instance of this from dist/index.js:
interface CasenPlugin { id: string // reverse-domain ID, e.g. "com.acme.casen-deploy" name: string // shown in "casen plugin list" version: string groups: CommandGroup[]}CommandGroup
Section titled “CommandGroup”Maps to one top-level token in the CLI (casen <group>). The name must be unique across all
installed plugins and the casen core commands:
interface CommandGroup { name: string // kebab-case, e.g. "deploy" aliases?: string[] description: string commands: Command[]}Command
Section titled “Command”A single executable action within a group (casen <group> <command>):
interface Command { name: string aliases?: string[] description: string args?: ArgSpec[] flags?: FlagSpec[] examples?: Example[] run(ctx: RunContext): Promise<void>}RunContext
Section titled “RunContext”Passed to every run() function. Use it to read arguments, write output, and access
authenticated Camunda clients:
interface RunContext { positional: string[] // positional args after <group> <command> flags: ParsedFlags // { flagName: value } output: OutputWriter // table / json / yaml renderer getClient(): Promise<unknown> // Camunda C8 REST client getAdminClient(): Promise<unknown> // Camunda Admin API client}OutputWriter
Section titled “OutputWriter”Use ctx.output for all output so the --output flag (table / json / yaml) is respected:
ctx.output.ok("Done.") // ✓ Done.ctx.output.info("Deploying…") // → Deploying…ctx.output.printList({ items }, columns) // table of rowsctx.output.printItem(singleObject) // key-value pairsctx.output.print(anything) // raw, format-awareWriting commands
Section titled “Writing commands”Positional arguments
Section titled “Positional arguments”{ name: "release", description: "Tag and deploy the current process version", args: [ { name: "process-id", description: "BPMN process ID", required: true }, { name: "tag", description: "Release tag, e.g. v1.2.0", required: true }, ], async run(ctx) { const processId = ctx.positional[0] const tag = ctx.positional[1] if (!processId || !tag) throw new Error("Missing required arguments") // ... },}{ name: "release", flags: [ { name: "dry-run", short: "n", description: "Show what would happen without making changes", type: "boolean", default: false, }, { name: "env", description: "Target environment", type: "string", default: "production", enum: ["staging", "production"], }, ], async run(ctx) { const dryRun = ctx.flags["dry-run"] === true const env = ctx.flags.env as string // ... },}Calling the Camunda API
Section titled “Calling the Camunda API”Cast getClient() to CamundaClient from @bpmnkit/api if you need full type coverage,
or access it generically via the unknown type:
import type { CamundaClient } from "@bpmnkit/api"
async run(ctx) { const client = await ctx.getClient() as CamundaClient const { items } = await client.processDefinition.searchProcessDefinitions({}) ctx.output.printList({ items }, [ { key: "bpmnProcessId", header: "PROCESS ID" }, { key: "name", header: "NAME" }, { key: "version", header: "VER" }, ])},Throwing errors
Section titled “Throwing errors”Throw a plain Error for user-facing errors. casen catches it, prints error: <message> to
stderr, and exits with code 1:
async run(ctx) { const name = ctx.positional[0] if (!name) throw new Error("Missing required argument: <name>") // ...}Test locally
Section titled “Test locally”Build and install your plugin from the local directory:
cd casen-deploypnpm installpnpm build
casen plugin install ./casen-deployRestart casen. Your commands appear under casen deploy (or whatever group name you chose).
To uninstall during development:
casen plugin remove casen-deployPublish to npm
Section titled “Publish to npm”When you’re ready to share:
cd casen-deploynpm publishThe prepublishOnly script in the generated package.json runs tsc automatically before
publishing, so dist/ is always up to date.
Because "casen-plugin" is in keywords, your package is immediately discoverable via:
casen plugin search deployPlugin naming conventions
Section titled “Plugin naming conventions”| Convention | Reason |
|---|---|
Name packages casen-<feature> | Predictable, easy to search |
Set id to a reverse-domain string | Avoids conflicts across organisations |
| Keep each group name unique | casen merges all groups into one flat namespace |
| Prefix group name with your org for internal plugins | e.g. acme-deploy avoids clashing with a published casen-deploy |