# Clearing Extension Points (/docs/architecture/extension-points)



Ductor is meant to be extended without editing the core. The mechanism is
[go.uber.org/fx](https://uber-go.github.io/fx/) **option groups**: a module
contributes an implementation tagged with a group name, and the engine collects
everything in that group at startup. Adding a strategy, a connector, or a
pipeline stage is a matter of registering into the right group — no changes to
the composition root, no engine fork.

## Option groups as the seam [#option-groups-as-the-seam]

When many implementations of the same kind should be discovered and wired
together, fx uses a *value group*. A provider tags its output with
`group:"<name>"`, and a consumer receives the whole slice. Ductor uses this
pattern for every pluggable subsystem. The groups the engine collects include:

| Group                            | What it contributes                                      |
| -------------------------------- | -------------------------------------------------------- |
| `routing_options`                | Options that configure the routing service               |
| `pipeline_stages`                | Custom stages inserted into the routing pipeline         |
| `routing_enrichers`              | Enrich-stage contributors (attach data before selection) |
| `routing_middleware`             | Cross-cutting middleware around routing                  |
| `connector_interceptors`         | Interceptors wrapping connector dispatch                 |
| `workflow_lifecycle_listeners`   | Hooks on workflow run lifecycle events                   |
| `effect_intent_handlers`         | Handlers that drain effect-intent outbox entries         |
| `digest_fire_handlers`           | Handlers invoked when a digest window fires              |
| `notification_provider_handlers` | Notification delivery providers                          |
| `api_service_entries`            | Connect/gRPC service registrations                       |

The seam is fan-in: independent modules each tag a contribution with the same
group name, never referencing one another, and the consumer receives the whole
slice at construction time. Adding an extension means adding a provider — not
editing the consumer:

```mermaid
flowchart LR
  m1[module A] -->|group: pipeline_stages| g((fx value group))
  m2[module B] -->|group: pipeline_stages| g
  m3[module C] -->|group: pipeline_stages| g
  g -->|slice| consumer[routing pipeline]
```

Because contribution is by group, the set of active extensions is just "whatever
modules were included in this build" — which is exactly what
`modules/defaults/defaults.go` assembles for the production binary.

## Strategies [#strategies]

A recipient-selection [Strategy](/docs/concepts/routing-pipeline#strategies) is
registered through the strategy registry (`pkg/strategy/registry.go`); built-ins
live in `pkg/strategy/builtin/`. The engine calls `Info()` first to check
compatibility, then `Select()` on the hot path. A strategy can opt into extra
capability interfaces:

* `BatchStrategy` — score many candidates at once.
* `ExplainStrategy` — return *why* a candidate won.
* `HealthCheckStrategy` — report readiness before use.

To ship a custom strategy you implement the interface and register it; the
routing pipeline picks it up like any built-in. See
[Writing a Custom Strategy](/docs/strategies/writing-a-custom-strategy).

### Remote and tenant-governed strategies [#remote-and-tenant-governed-strategies]

A strategy doesn't have to run in-process. A **governed remote strategy
deployment** delegates selection to an external service over a
[Connect](https://connectrpc.com/) RPC — the engine holds a
`PluginService` client and calls `Select()` on the hot path
(`pkg/strategy/remotegrpc/`). This is the seam for tenant-supplied selection
logic that must stay outside the core binary.

Two properties keep this surface safe:

* **Startup validation.** Before a remote deployment is exposed through the live
  registry, the adapter opens the connection, runs a protocol **handshake** to
  negotiate a supported version, and performs an initial **health check**. A
  deployment that fails either never enters the routing path — it fails visibly
  at load rather than at first request. The adapter also carries per-call
  guards: a timeout, a circuit breaker, an in-flight cap, and payload/response
  size limits.
* **Tenant scoping.** Deployments are keyed by tenant and environment
  (`application/routing/remotestrategy/`). A deployment's `tenant_id`,
  `environment`, and `strategy_name` must match its binding, and the derived
  registry key is tenant/environment-scoped by default — so one tenant's custom
  strategy can't shadow another's. A `CertificationGate` blocks uncertified
  deployments from entering authoritative routing; the production composition
  root wires the gate or refuses to boot.

See [Governance](/docs/strategies/governance) for the certification and rollout
controls around custom/remote runtimes.

## Pipeline stages [#pipeline-stages]

The [routing pipeline](/docs/concepts/routing-pipeline) is itself extensible. A
module contributes a stage into the `pipeline_stages` fx group and the engine
folds every contribution into a **stage registry**
(`pkg/routing/pipeline/stage_registry.go`) at startup — no fork of the engine.
Each stage implements one of the role interfaces the pipeline iterates:
`Validator`, `Enricher`, `Filter`, `Selector`, `Assigner`, or a `PostRouteHook`
that runs after a successful assignment (`pkg/routing/pipeline/stage.go`).

Registration is **two-tier**, which is what lets a module override a
standard-bundle stage deterministically:

* `RegisterDefault` adds a standard-bundle stage. It is a no-op if a
  module-supplied stage with the same name already exists — a default may never
  demote an override.
* `Register` adds a module stage, and *replaces* a default of the same name. Two
  non-default registrations of the same name collide.

A collision surfaces at fx wire time, not at first request: contributing two
producers with the same stage `Name()` makes `fx.New(...).Err()` non-nil at boot
(`cmd/ductor/fx_routing_stages.go`), consistent with the "every path either
works or fails visibly" rule.

## Connectors [#connectors]

A [connector](/docs/concepts/connectors) contributes a provider definition to the
`ProviderRegistry` and one or more `ActionSpec`s to the `ActionRegistry`, and can
wrap dispatch via the `connector_interceptors` group. Provider implementations
live under `infrastructure/connector/providers/`. Adding one requires no change
to the coordinator or the dispatch path — the registries are the whole contract.

## Workflow archetypes [#workflow-archetypes]

The step types in [The DAG Workflow Model](/docs/concepts/dag-workflow-model) are
built on an **archetype** abstraction with namespaced kinds. Core step kinds are
`core.*`, first-party AI kinds are `ai.*`, and plugins register `vendor.<name>.*`
kinds. An archetype implements the coordinator's switch points — schedule a ready
node, apply an attempt result, apply a drained signal, apply a fired timer — plus
optional capabilities (cancelable, replicable for cross-region, resettable).
Kinds must be namespaced; an un-namespaced kind is rejected at fx wire-up, which
keeps the extension surface disciplined.

## Modules [#modules]

`modules/` holds optional feature modules (forecast, postback, retention, SLA,
custom strategies, plugins). Each self-registers through the groups above, and
`modules/defaults/defaults.go` bundles the standard set for the production
binary. This is how a deployment tailors the engine: include the modules you
want, and their contributions flow into the right groups automatically.

<Callout title="Extend at the seam, not the core">
  The layering rules still apply to extensions. A strategy or connector lives in
  its own package and contributes through a group; it does not reach into the
  coordinator or mutate run state. The
  [dependency rules](/docs/architecture/layered-architecture#dependency-enforcement)
  apply to extensions just as they do to the core.
</Callout>

## Where to go next [#where-to-go-next]

* [Layered Architecture](/docs/architecture/layered-architecture) — the rules extensions must respect.
* [The Routing Pipeline](/docs/concepts/routing-pipeline) — the stages you can extend.
* [Connectors](/docs/concepts/connectors) — the registry contract for providers.
