Skip to main content
Extending / Plugin Development

Plugin Development

Escalated's Plugin SDK (@escalated-dev/plugin-sdk) lets you extend the platform with custom integrations, UI components, and automation — written once in TypeScript and running across every Escalated backend framework.

What plugins can do

  • Actions — fire-and-forget event handlers that respond to ticket lifecycle events (ticket.created, ticket.assigned, reply.created, etc.)
  • Filters — intercept and transform data as it flows through the system (e.g., adding notification channels, modifying ticket action menus)
  • Pages — register custom admin or agent pages that appear in the sidebar, backed by Vue components served via Inertia.js
  • Components — inject Vue components into existing page slots (ticket sidebar, dashboard panels, etc.)
  • Widgets — add dashboard widgets with optional dynamic badges polled from the plugin backend
  • Endpoints — expose data API routes that your Vue pages consume
  • Webhooks — receive inbound HTTP requests from external services (Slack, GitHub, Stripe, etc.)
  • Cron — schedule recurring background tasks (hourly, daily, or any interval)

Architecture

The SDK is built on the Grafana/HashiCorp subprocess model. Plugins run as a single shared Node.js process (@escalated-dev/plugin-runtime), communicating with the host framework over JSON-RPC 2.0 via stdio. This means your TypeScript plugin code runs on every backend — Laravel, Django, Rails, and AdonisJS — without modification.

Host Framework (any)              Plugin Process (Node.js)
┌──────────────┐   JSON-RPC/stdio  ┌─────────────────────┐
│ Laravel      │◄─────────────────►│ Plugin Runtime       │
│ Django       │                    │                      │
│ AdonisJS (*) │                    │ ┌─────────────────┐ │
│ Rails        │   spawn/manage     │ │ Slack Plugin     │ │
│              │──────────────────→│ │ Jira Plugin      │ │
│ Bridge pkg   │                    │ │ ... all plugins  │ │
└──────────────┘                    │ └─────────────────┘ │
                                    └─────────────────────┘
(*) AdonisJS: in-process, no subprocess needed

Each framework has a thin native bridge package that spawns the runtime, reads the plugin manifest, registers routes and menu items, and proxies ctx.* calls (database queries, config reads, broadcasts) back from the plugin process to the host ORM.

Package Registry Purpose
@escalated-dev/plugin-sdk npm Plugin authoring SDK — definePlugin(), types, runtime
@escalated-dev/plugin-runtime npm Process host — loads plugins, handles JSON-RPC
escalated/plugin-bridge Composer Laravel bridge
escalated-plugin-bridge PyPI Django bridge
escalated-plugin-bridge RubyGems Rails bridge
@escalated-dev/plugin-bridge npm AdonisJS bridge (in-process)

AdonisJS in-process mode

On AdonisJS, plugins are loaded directly via require() — no subprocess, no JSON-RPC. The same PluginContext interface is used, but the implementation calls Lucid ORM directly. Plugin behavior is identical across modes; the PluginContext contract guarantees this.

Quick start

import { definePlugin } from '@escalated-dev/plugin-sdk'

export default definePlugin({
  name: 'hello-world',
  version: '0.1.0',
  description: 'A minimal Escalated plugin',

  config: [
    { name: 'greeting', label: 'Greeting Message', type: 'text' },
  ],

  actions: {
    'ticket.created': async (event, ctx) => {
      const { greeting } = await ctx.config.all()
      ctx.log.info(`${greeting ?? 'Hello'}: new ticket #${event.id} — ${event.title}`)
    },
  },
})

See Getting Started for installation and local testing instructions.

Getting Started

Install the SDK

npm install @escalated-dev/plugin-sdk

For TypeScript projects (recommended), also install the TypeScript compiler:

npm install -D typescript

Create your first plugin

All plugins are defined with definePlugin(). Create src/index.ts:

import { definePlugin } from '@escalated-dev/plugin-sdk'

export default definePlugin({
  name: 'my-plugin',
  version: '0.1.0',
  description: 'My first Escalated plugin',

  // Configuration fields — rendered in Admin UI, stored as JSON
  config: [
    { name: 'api_key', label: 'API Key', type: 'password', required: true },
    { name: 'notify_channel', label: 'Notify Channel', type: 'text' },
  ],

  onActivate: async (ctx) => {
    // Runs when the plugin is activated in the Admin UI
    // Set up defaults, seed initial config, etc.
    const config = await ctx.config.all()
    if (!config.notify_channel) {
      await ctx.config.set({ notify_channel: '#support' })
    }
  },

  onDeactivate: async (ctx) => {
    // Runs when the plugin is deactivated — clean up resources
  },

  actions: {
    'ticket.created': async (event, ctx) => {
      const settings = await ctx.config.all()
      if (!settings.api_key) return
      ctx.log.info('New ticket', { id: event.id, title: event.title })
      // Call your external service here...
    },
  },
})

Config fields

The config array defines the settings form shown in the Admin UI. Each field is stored as JSON and accessible via ctx.config.

Type Renders as Example use
text Text input Domain, channel name
password Masked input API tokens, secrets
number Number input Timeout values
boolean Toggle switch Enable/disable features
select Dropdown { type: 'select', options: [{value, label}] }
multiselect Multi-select Event type selection
textarea Textarea Templates, message formats
json Code editor Complex configuration objects
url URL input with validation Webhook URLs, API base URLs

Example with multiple field types:

config: [
  { name: 'bot_token', label: 'Bot Token', type: 'password', required: true },
  { name: 'channel', label: 'Default Channel', type: 'text' },
  { name: 'notify_on', label: 'Notify On', type: 'multiselect', options: [
    { value: 'ticket.created', label: 'New Ticket' },
    { value: 'ticket.assigned', label: 'Assignment' },
    { value: 'reply.created', label: 'New Reply' },
  ]},
  { name: 'enabled', label: 'Enable Notifications', type: 'boolean' },
],

Testing your plugin locally

Install the runtime alongside the SDK:

npm install @escalated-dev/plugin-runtime

Run your plugin against a local Escalated instance by pointing the bridge at your development directory. For Laravel:

# In your Laravel .env
ESCALATED_PLUGIN_PATH=/path/to/your/plugin-project/node_modules

The runtime loads all packages matching @escalated-dev/plugin-* from the configured path. During development you can symlink your plugin into a local node_modules directory and the runtime will pick it up on next restart.

To run the plugin in isolation and inspect JSON-RPC messages:

npx @escalated-dev/plugin-runtime --dev --verbose

The --dev flag enables source maps and detailed error output. The --verbose flag logs every JSON-RPC message to stderr.