> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ensemble.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin Registry

> Global registry for custom operations that work everywhere

# 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:

```typescript theme={null}
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:

```yaml theme={null}
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:

```yaml theme={null}
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}
```

<Note>
  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](/conductor/operations/transform) for full details.
</Note>

### `custom-code`

Execute custom JavaScript:

```yaml theme={null}
operations:
  - operation: custom-code
    config:
      code: "return { result: input.value * 2 }"
      input: ${previous.output}
```

## Registering Operations

### Basic Registration

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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:**

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
const result = await registry.execute({
  operation: 'any:op',
  config: {},
  handler: async (context) => {
    return { custom: true }
  }
}, context)
```

## Discovery API

### List All Operations

```typescript theme={null}
const all = registry.list()
// ['fetch-data', 'transform', 'custom-code', 'plasmic:render', ...]
```

### List by Context

```typescript theme={null}
const formOps = registry.listByContext('form')
// Only operations that work in form context
```

### List by Tag

```typescript theme={null}
const uiOps = registry.listByTag('ui')
// Only operations tagged with 'ui'
```

### Check if Operation Exists

```typescript theme={null}
if (registry.has('plasmic:render')) {
  // Operation is registered
}
```

### Get Metadata

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
// Good
'plasmic:render'
'unkey:validate'
'stripe:charge'

// Avoid
'render'
'validate'
'charge'
```

**Metadata is Documentation**: Include comprehensive metadata

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
registry.unregister('custom:op')
```

### Clear All Operations

```typescript theme={null}
registry.clear()
// Warning: Removes built-in operations too!
```

### Reset to Initial State

```typescript theme={null}
registry.reset()
// Clears all and re-registers built-ins
```

## Next Steps

* [Creating a Functional Plugin](/conductor/plugins/functional-plugins)
* [Creating a Lifecycle Plugin](/conductor/plugins/lifecycle-plugins)
* [Plugin Examples](/conductor/plugins/examples)
* [Plugin Best Practices](/conductor/plugins/best-practices)

<Note>
  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.
</Note>
