Extending / Plugin Development
Getting Started
Tickets & Conversations
Automation & Workflows
Email & Notifications
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.