Skip to content

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.

The fastest way to start is with the official scaffolding tool:

Terminal window
pnpm create @bpmnkit/casen-plugin
# or: npx @bpmnkit/create-casen-plugin
# or: bunx @bpmnkit/create-casen-plugin

The 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-deploy

Pass flags to skip all prompts — useful in CI or cookiecutter scripts:

Terminal window
pnpm create @bpmnkit/casen-plugin \
--name casen-deploy \
--description "Git-tag-aware deploys for casen" \
--author acme \
--no-git
FlagDescription
--name / -nnpm package name (first positional arg also works)
--display-nameHuman-readable name shown in casen plugin list
--description / -dOne-line description
--author / -aAuthor name or npm username
--no-gitSkip git init
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 command
{
"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.

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 plugin

Replace the example command with your own logic. Add as many command groups and commands as needed.

Install the SDK as a devDependency in your plugin:

Terminal window
pnpm add -D @bpmnkit/cli-sdk

It exports all types you need — no runtime dependency on casen internals.

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[]
}

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[]
}

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

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
}

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 rows
ctx.output.printItem(singleObject) // key-value pairs
ctx.output.print(anything) // raw, format-aware
{
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
// ...
},
}

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" },
])
},

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>")
// ...
}

Build and install your plugin from the local directory:

Terminal window
cd casen-deploy
pnpm install
pnpm build
casen plugin install ./casen-deploy

Restart casen. Your commands appear under casen deploy (or whatever group name you chose).

To uninstall during development:

Terminal window
casen plugin remove casen-deploy

When you’re ready to share:

Terminal window
cd casen-deploy
npm publish

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

Terminal window
casen plugin search deploy
ConventionReason
Name packages casen-<feature>Predictable, easy to search
Set id to a reverse-domain stringAvoids conflicts across organisations
Keep each group name uniquecasen merges all groups into one flat namespace
Prefix group name with your org for internal pluginse.g. acme-deploy avoids clashing with a published casen-deploy