Skip to main content

Building a plugin

For developers. A plugin adds a work area to the desk through its extension points; this page describes them, the manifest, the rules a plugin must follow, and the APIs of the plugins that come with the desk.

PartWhat a plugin can add
The appPages, its item in the sidebar and its page in Settings, a tab on Reports.
The caseSections and actions on the case, actions on the inbox's selected cases, timeline entries.
The serverAPI routes, handlers on case events, events for webhooks, audit log entries.
IntegrationsPermissions for tokens, and tools for AI assistants.

Admins turn plugins on and off under Settings → Plugins (admin guide). The Software Development plugin in plugins/dev/ and the Incident Management plugin in plugins/incidents/ are complete examples.

The page in order: Layout and the manifest; the server side (routes, access, extension points, events, jobs, permissions, MCP tools, tables, tests); the web side (extension points, the side panel, slots); then the plugin API and the API of each plugin that comes with the desk.

[!IMPORTANT] Plugins are compiled with the desk, not loaded at runtime: a desk runs exactly the plugins its image was built with.

To add your own:

  1. Put it in plugins/<id>/.
  2. List it in plugins/plugins.config.ts.
  3. Build the image.

Layout​

plugins/
plugins.config.ts the plugins this desk is built with: ['dev', …]
dev/
package.json a workspace package (@desk/plugin-dev) with its dependencies
manifest.ts id, name, permissions …
shared.ts types and constants both sides use (optional)
server/index.ts default export: definePluginServer({ manifest, module })
server/… a NestJS module: controllers, services, MCP tools, tests (*.test.ts)
web/index.tsx default export: defineWebPlugin({ … })
web/… React pages and components, CSS
apps/server/prisma/schema/
plugin-dev.prisma the plugin's tables (plugin_dev_…)

The desk never imports a plugin's files by name. It reads the list in plugins.config.ts and loads <id>/server/index and <id>/web/index.tsx by convention. Removing a plugin is deleting its line (and, if you like, its folder): the desk still builds, and the plugin's tables and data stay in the database.

How it is built​

The server build (nest build, swc) compiles apps/server/src and plugins/ (without the web/ folders) into apps/server/dist, so a plugin's server code ends up in dist/plugins/<id>/server. Vite bundles the web/ folders into the app (import.meta.glob), with the board-style pages split into their own chunks when you load them lazily. The Dockerfile copies plugins/ into the build; nothing there names a plugin.

Imports​

[!IMPORTANT] Plugins use the desk through two names, and nothing else of the desk's code.

ImportFileWhat it has
@desk/server/plugin-sdkapps/server/src/plugin-sdk.tsThe extension points, access scope, Prisma, errors, validation, decorators, Markdown, the case service.
@desk/web/plugin-sdkapps/web/src/plugins/sdk.tsThe extension point types, the API client and hooks, the app's components (lib/ui.tsx), auth and toasts, and the list parts below.

@desk/shared has the manifest types (packages/shared/src/plugins.ts).

For long lists the web SDK has the inbox's parts (lib/list.tsx), so a plugin's list works like the inbox's:

  • FilterPill, GlanceChips, ColHead;
  • useDensity with DensityToggle;
  • useListKeys (the j/k keyboard);
  • Pager and usePager.

A list of tasks uses the shared frame:

  • ListToolbar: search, then the standard filters, your extra ones, and the end (sort, clear, columns, density).
  • The standard pills PersonPill, AccountPill and PeriodPill (Created, Updated) next to FilterPill.
  • TaskTable with useColumns(list, columns) and ColumnsMenu: describe every column (ListColumn: id, label, width as a grid track, render, optional sort, main for the title, default) and each person picks theirs, saved as columns.<list> in their preferences.

Package dependencies​

List them in the plugin's package.json with exact versions. Packages the server side needs at runtime must also be dependencies of apps/server (compiled plugin code runs from apps/server/dist and resolves packages from there); web-only packages (like @dnd-kit/core for the board) need nothing more.

The plugin API​

EndpointWhoWhat
GET /api/v1/pluginsSigned in (any token)Every plugin: id, name, description, version, enabled, scopes, events, requires and requiredBy (plugin ids).
GET /api/v1/admin/pluginsAdmins, no tokensAs above, with turnedOn and turnedOnAt.
POST /api/v1/admin/plugins/<id>/on, …/offAdmins, no tokensTurn it on or off (stored as the installation setting plugin.<id>.enabled, audited), and the plugin as it is now. On also turns on what it needs; off leaves the plugins built on it turned on but off with it (turnedOn: true, enabled: false), until it is on again.

In this section​