Skip to main content

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

transform

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)

With Metadata

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
}

Get Metadata

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

Metadata Schema

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.