Plugin Registry
The Plugin Registry is the core of Conductor’s plugin system. It’s a global singleton that stores all operations (built-in and custom) and makes them available across all contexts.
Key Concepts
Universal Operations
Once an operation is registered, it works everywhere:
- ✅ Ensembles
- ✅ Pages
- ✅ Forms
- ✅ APIs
- ✅ Webhooks
No context-specific code needed!
Singleton Pattern
The registry is a singleton - there’s only one instance per Worker:
import { getPluginRegistry } from '@ensemble-edge/conductor'
const registry = getPluginRegistry()
// Always returns the same instance
Built-in Operations
Conductor includes these built-in operations:
fetch-data
Fetch data from various sources:
operations:
- operation: fetch-data
config:
source: payload # payload, d1, kv, r2, api
collection: users
query:
active: true
Declarative data transformations - return literal values, apply modifiers (pick/omit/defaults), or merge data:
agents:
- name: mock-data
operation: transform
config:
value:
- { id: 1, name: "Alice" }
- { id: 2, name: "Bob" }
- name: clean-user
operation: transform
config:
input: ${fetch-user.output}
omit: [password, secret]
defaults:
status: "active"
- name: combine
operation: transform
config:
merge:
- ${agent1.output}
- ${agent2.output}
The transform operation is a first-class agent operation. Expression interpolation (${...}) is resolved by the runtime before the agent executes. See the Transform Agent documentation for full details.
custom-code
Execute custom JavaScript:
operations:
- operation: custom-code
config:
code: "return { result: input.value * 2 }"
input: ${previous.output}
Registering Operations
Basic Registration
import { getPluginRegistry, type OperationHandler } from '@ensemble-edge/conductor'
const registry = getPluginRegistry()
const handler: OperationHandler = {
async execute(operation, context) {
const { config } = operation
// Perform operation logic
return { result: 'success' }
}
}
registry.register('custom:op', handler)
registry.register('custom:op', handler, {
name: 'custom:op',
description: 'Custom operation description',
version: '1.0.0',
author: '@conductor/custom',
contexts: ['form', 'api'], // or ['all']
tags: ['custom', 'data'],
inputs: {
foo: 'string',
bar: 'number'
},
outputs: {
result: 'any'
}
})
Operation Context
Operations receive a context object:
interface OperationContext {
request?: Request // HTTP request (if available)
env: ConductorEnv // Environment bindings
ctx: ExecutionContext // Cloudflare Workers context
params?: Record<string, string> // URL/route parameters
query?: Record<string, string> // Query string parameters
headers?: Record<string, string> // Request headers
data?: Record<string, any> // Input data
contextType: 'ensemble' | 'form' | 'api' | 'webhook'
}
Example:
const handler: OperationHandler = {
async execute(operation, context) {
// Access environment bindings
const db = context.env.DB
// Access request data
const userId = context.params?.userId
// Check context type
if (context.contextType === 'ensemble') {
// Ensemble-specific logic
}
return { success: true }
}
}
Executing Operations
Direct Execution
const result = await registry.execute({
operation: 'custom:op',
config: {
foo: 'bar',
baz: 123
}
}, context)
With Custom Handler
Override the registered handler for a specific invocation:
const result = await registry.execute({
operation: 'any:op',
config: {},
handler: async (context) => {
return { custom: true }
}
}, context)
Discovery API
List All Operations
const all = registry.list()
// ['fetch-data', 'transform', 'custom-code', 'plasmic:render', ...]
List by Context
const formOps = registry.listByContext('form')
// Only operations that work in form context
List by Tag
const uiOps = registry.listByTag('ui')
// Only operations tagged with 'ui'
Check if Operation Exists
if (registry.has('plasmic:render')) {
// Operation is registered
}
const meta = registry.getMetadata('plasmic:render')
console.log(meta.description) // "Render Plasmic component"
console.log(meta.version) // "1.0.0"
console.log(meta.contexts) // ['form', 'api']
console.log(meta.tags) // ['ui', 'visual', 'render']
Context-Aware Operations
Operations can check their execution context:
const handler: OperationHandler = {
async execute(operation, context) {
switch (context.contextType) {
case 'ensemble':
return await handleEnsembleContext(operation, context)
case 'api':
return await handleApiContext(operation, context)
case 'form':
return await handleFormContext(operation, context)
default:
throw new Error(`Unsupported context: ${context.contextType}`)
}
}
}
registry.register('context:aware', handler, {
name: 'context:aware',
description: 'Context-aware operation',
contexts: ['ensemble', 'api', 'form'] // Specify supported contexts
})
Complete metadata interface:
interface OperationMetadata {
name: string // Required: operation identifier
description: string // Required: human-readable description
version?: string // Semver version
author?: string // Plugin/package name
contexts?: Array< // Supported contexts
'ensemble' | 'form' | 'api' | 'webhook' | 'all'
>
inputs?: Record<string, any> // Input schema (for docs/validation)
outputs?: Record<string, any> // Output schema (for docs/validation)
tags?: string[] // Tags for categorization
}
Testing Operations
import { describe, it, expect, beforeEach } from 'vitest'
import { getPluginRegistry } from '@ensemble-edge/conductor'
describe('Custom Operation', () => {
let registry
beforeEach(() => {
registry = getPluginRegistry()
registry.reset() // Reset to clean state
})
it('should register and execute', async () => {
const handler = {
async execute(operation, context) {
return { result: 'test' }
}
}
registry.register('test:op', handler)
const result = await registry.execute({
operation: 'test:op',
config: {}
}, mockContext)
expect(result.result).toBe('test')
})
})
Best Practices
Naming Convention: Use namespace:operation format
// Good
'plasmic:render'
'unkey:validate'
'stripe:charge'
// Avoid
'render'
'validate'
'charge'
Metadata is Documentation: Include comprehensive metadata
registry.register('op', handler, {
name: 'op',
description: 'Clear, concise description',
version: '1.0.0',
author: '@conductor/plugin',
contexts: ['ensemble'], // Be explicit
tags: ['ui', 'render'], // Aid discovery
})
Error Handling: Throw meaningful errors
const handler: OperationHandler = {
async execute(operation, context) {
if (!operation.config.required) {
throw new Error('[custom:op] Missing required config: required')
}
// ... operation logic
}
}
Type Safety: Use TypeScript
interface CustomOpConfig {
required: string
optional?: number
}
const handler: OperationHandler = {
async execute(operation, context) {
const config = operation.config as CustomOpConfig
// Typed access to config
}
}
Management API
Unregister Operation
registry.unregister('custom:op')
Clear All Operations
registry.clear()
// Warning: Removes built-in operations too!
Reset to Initial State
registry.reset()
// Clears all and re-registers built-ins
Next Steps
The Plugin Registry was previously known as the “Operation Registry”. The API remains the same, but the class and function names now use “Plugin” prefix for consistency with the plugin system terminology.