# Engine Extensions (/docs/sdks/extensions)



The SDKs let you author *workflows* in your language. Extensions let you plug new
*behavior* into the routing and workflow engine itself. There are eight
extension-point types, and they differ sharply in how far outside the Go process
they can live: some are Go-only, some speak gRPC in any language, and some can run
as sandboxed WebAssembly.

Every extension declares its identity and compatibility in a `spec.yaml`
(described [below](#the-specyaml-manifest)). The `type` field is one of eight
values, validated by `modules/validation/validation.go`:

| Type         | `spec.yaml` `type` | Delivery             | Multi-language?    |
| ------------ | ------------------ | -------------------- | ------------------ |
| Strategy     | `strategy`         | Standalone Go module | Go, or gRPC/remote |
| Enricher     | `enricher`         | Plugin               | Go, gRPC, or WASM  |
| Filter       | `filter`           | Plugin               | Go, gRPC, or WASM  |
| Notifier     | `notifier`         | Plugin               | Go, gRPC, or WASM  |
| Transformer  | `transformer`      | Plugin               | Go, gRPC, or WASM  |
| CEL function | `cel_function`     | In-tree              | **Go only**        |
| Middleware   | `middleware`       | In-tree              | **Go only**        |
| Provider     | `provider`         | In-tree              | **Go only**        |

## 1. Strategies [#1-strategies]

A strategy is a routing algorithm — given a set of candidates, pick one (or rank
or allocate them). You ship one as a **standalone Go module**: implement
`strategy.Strategy`, expose a factory, and register it with a name.

```go
reg.Register("my_algorithm", myalgo.NewFactory())
```

The factory's `Create` builds a configured instance; `Info()` reports the
strategy's identity, including an `InterfaceVersion` — currently `1.0.0`
(`pkg/strategy/version.go`) — that Ductor checks before loading so it never runs
a strategy whose interface it can't speak. Users install your module the ordinary
Go way:

```bash
go get github.com/yourusername/ductor-strategy-myalgorithm@v1.0.0
```

The full path — interface, capabilities, tunable params, contracts, and
certification — is covered in
[Writing a custom strategy](/docs/strategies/writing-a-custom-strategy).

## 2–5. Plugins: enricher, filter, notifier, transformer [#25-plugins-enricher-filter-notifier-transformer]

Four plugin types intercept the routing pipeline around the decision:

* **Enricher** — add data to a routable *before* the decision.
* **Filter** — remove ineligible candidates.
* **Notifier** — react to routing events.
* **Transformer** — convert payloads between formats.

Each implements the base `plugin.Plugin` interface (`Info`, `Start`, `Stop`,
`HealthCheck`) plus its type-specific method — a Filter, for example, adds
`Filter(ctx, candidates, routable)` (`examples/plugins/filter/filter.go`).

Plugins have three delivery mechanisms:

<Tabs items="[&#x22;Native Go&#x22;, &#x22;gRPC&#x22;, &#x22;WebAssembly&#x22;]">
  <Tab value="Native Go">
    Compiled into the host, the fastest path.
  </Tab>

  <Tab value="gRPC">
    The plugin runs as a separate process in any language, declared via the `grpc`
    block in `spec.yaml`.
  </Tab>

  <Tab value="WebAssembly">
    Compiled with TinyGo and run in a sandboxed
    [wazero](https://github.com/tetratelabs/wazero) runtime for memory isolation
    and deterministic execution.

    A WASM plugin exports a fixed protocol (`examples/plugins/wasm/README.md`):
    memory management (`alloc`, `dealloc`), the base lifecycle
    (`plugin_info`, `plugin_start`, `plugin_stop`, `plugin_health_check`), and the
    per-type entry point (`filter_candidates` for filters, `enrich` for enrichers).
    The host provides a `ductor` module with `host_log` for logging back into
    Ductor's stream. Build it with TinyGo:

    ```bash title="build a WASM plugin"
    tinygo build -o plugin.wasm -target=wasi -no-debug ./main.go
    ```
  </Tab>
</Tabs>

## 6–8. Go-only extension points [#68-go-only-extension-points]

Three types cannot cross a process boundary and must be written in Go, compiled
in-tree:

* **CEL functions** — custom functions callable from the CEL expressions that
  guard edges, scatter keys, and dataflow steps.
* **Middleware** — intercepts the routing pipeline at defined hook points
  (`pkg/middleware`, `AllHookPoints`).
* **Providers** — connector integrations (see
  [building a provider](/docs/connectors/building-a-provider)).

<Callout type="warn" title="These three cannot be gRPC or WASM">
  CEL functions, middleware, and providers run inside the engine's evaluation and
  request paths where a cross-process hop would be incorrect or prohibitively
  expensive. They are Go-only by design — there is no remote delivery for them.
</Callout>

## Action templates [#action-templates]

There is also a lower-ceremony extension seam that needs no Go at all: **action
templates**. These are YAML-authored composite actions that stitch existing
connector actions into a new one. A template declares `expects` (a typed input
schema), a linear list of `steps`, and a `returns` expression, with
`${{ inputs.x }}` and `${{ steps.<ref>.result.* }}` interpolation between them
(`domain/actiontemplate/template.go`). At load time each template is compiled into
a real connector `ActionSpec` and registered under a namespaced key, so callers
invoke it exactly like any built-in action.

Use an action template when your extension is "call these three connector actions
in sequence and shape the result" — no plugin, no gRPC, no build step.

## The spec.yaml manifest [#the-specyaml-manifest]

Every extension (except in-tree providers wired directly) declares a `spec.yaml`
that carries its identity and compatibility. It is validated structurally, and
its `config` block is validated as a JSON Schema
(`modules/validation/validation.go`):

```yaml title="spec.yaml"
apiVersion: ductor.io/v1
kind: ExtensionSpec
metadata:
  name: language-filter
  version: 1.0.0
  type: filter          # one of the eight types
  interfaceVersion: 1.0.0
  author: Acme
  license: Apache-2.0
  repository: https://github.com/acme/ductor-language-filter
spec:
  description: Filters candidates by supported language.
  config: { }           # JSON Schema for the plugin's config
  capabilities: [cacheable]
  hooks: [ ]
  grpc:                  # present only for gRPC-delivered plugins
    service: acme.LanguageFilter
    methods: [FilterCandidates]
```

`apiVersion` must be `ductor.io/v1` and `kind` must be `ExtensionSpec`; the
`config` block, when present, is compiled and used to validate an operator's
supplied configuration before the extension is activated.

## Sandbox kinds [#sandbox-kinds]

Non-native extensions use the &#x2A;*`in_process`** sandbox kind. The extension
shares the host's memory and process and must be treated as trusted code.

<Callout type="warn" title="Extensions are not an isolation boundary">
  Vet third-party extensions before deployment and grant them only the
  configuration and credentials they require.
</Callout>

## Related [#related]

<Cards>
  <Card title="Extension points" href="/docs/architecture/extension-points">
    The architectural view of where each extension type plugs into the engine.
  </Card>

  <Card title="Writing a custom strategy" href="/docs/strategies/writing-a-custom-strategy">
    The full strategy path — interface, capabilities, contracts, certification.
  </Card>

  <Card title="Building a provider" href="/docs/connectors/building-a-provider">
    Authoring a connector provider, one of the three Go-only extension types.
  </Card>
</Cards>
