Configuration
The runtime-writable config surface — system config snapshots, feature flags, per-tenant overrides, runtime config refs, and the routing pipeline mode — versus static env configuration.
Not all of Ductor's configuration is baked in at boot. A meaningful slice is runtime-writable — feature flags you toggle, per-tenant overrides you set, a routing pipeline mode you flip — and takes effect across the fleet without a redeploy. This page maps the config surface: what you can read, what you can change at runtime, and what is fixed at process start via environment variables.
Two tiers of configuration
| Tier | How it's set | When it takes effect | Examples |
|---|---|---|---|
| Static (env/file) | configs/ductor.yaml + DUCTOR_* env vars, read once at boot | Process restart | database.url, cache.url, server.http_addr, connector.encryption_key, workflow_runtime.enabled |
| Dynamic (runtime-writable) | API + CLI, backed by a store with pub/sub invalidation | Live, across all pods | Feature flags, per-tenant config overrides, routing pipeline mode |
The static tier is the deployment's shape — connection strings, ports, keys — and is documented in Getting Started and the deployment guides. Everything below is the dynamic tier.
Two different 'dynamic config' systems — don't conflate them
Ductor has two independent runtime-config mechanisms, and they solve different problems:
- The dynamic-config registry (
pkg/dynamicconfig) — a Postgres-backed, Redis-invalidated store of typed operational settings with first-class kill switches and gradual (percentage) rollout. This is the durable, cross-pod control plane the services below write to. - The file-based feature-flag / A-B system (
internal/config—flags.go,feature_config.go,abtest.go,file_source.go) — flags and experiment buckets resolved from config files/env at load time.
The APIs on this page (ConfigService, TenantConfigService,
RuntimeConfigService) drive the registry side. The operational deep-dive on
the registry, its kill switches, and gradual-change semantics lives in
Dynamic config.
Where it lives
Three services cover dynamic config, each with its own scope:
ConfigService (system-wide, /api/config), TenantConfigService
(per-tenant, /api/tenants/{id}/config), and RuntimeConfigService
(per-artifact dependency refs, /api/runtime-config). Routing pipeline mode is
a routing-domain setting changed through the CLI.
System config: ConfigService
System-wide dynamic config lives under /api/config, resource_type: "config".
| Operation | RPC | HTTP | Action |
|---|---|---|---|
| Snapshot the full config | GetConfigSnapshot | GET /api/config | read |
| Get version + hash | GetVersion | GET /api/config/version | read |
| List feature flags | ListFlags | GET /api/config/flags | read |
| Toggle a feature flag | ToggleFlag | PUT /api/config/flags/{key} | write |
| Force a reload | ReloadConfig | POST /api/config/reload | admin |
Snapshot and version
GetConfigSnapshot returns the entire current system config as a key/value map —
this is your export of what the running config actually is:
curl -s https://api.ductor.io/api/config \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"GetVersion returns a version, a content hash, and loaded_at — cheap to
poll, and the right way to detect that config changed without pulling the whole
snapshot:
{ "version": "2026-07-11T09:00:00Z", "hash": "sha256:1f3c…", "loaded_at": "2026-07-11T09:00:02Z" }Feature flags
ListFlags returns every flag with its enabled state and rollout
percentage. ToggleFlag updates one, keyed in the URL:
curl -s -X PUT https://api.ductor.io/api/config/flags/enable_advanced_routing \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "percentage": 25 }'percentage (0–100) drives a gradual rollout and only applies when enabled is
true. The response echoes the updated FeatureFlag.
Reload
ReloadConfig forces an immediate re-read from the backing store and returns the
new version + hash. It requires the admin action (stricter than the write
used for flag toggles) because it re-loads the entire config:
curl -s -X POST https://api.ductor.io/api/config/reload \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"Per-tenant config: TenantConfigService
Per-tenant overrides live under /api/tenants/{tenant_id}/config,
resource_type: "tenant_config".
| Operation | RPC | HTTP | Action |
|---|---|---|---|
| Get tenant overrides (raw) | GetTenantConfig | GET /api/tenants/{tenant_id}/config | read |
| Replace tenant config | SetTenantConfig | PUT /api/tenants/{tenant_id}/config | write |
| Get resolved config | GetResolvedConfig | GET /api/tenants/{tenant_id}/config/resolved | read |
GetTenantConfig returns only the tenant's own overrides — not inherited
defaults. SetTenantConfig replaces the entire config (it's a PUT, not a
merge), so read-modify-write the whole object:
curl -s -X PUT https://api.ductor.io/api/tenants/$DUCTOR_TENANT/config \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "config": { "default_strategy": "weighted_round_robin", "features": { "beta_scoring": true } } }'GetResolvedConfig is the one you'll reach for when debugging "why is this tenant
behaving this way" — it returns the fully merged view (system defaults +
tenant overrides + optional pool-level overrides) plus a sources list showing
precedence order. Pass pool_id to fold in a pool's overrides too:
curl -s "https://api.ductor.io/api/tenants/$DUCTOR_TENANT/config/resolved?pool_id=$POOL_ID" \
-H "Authorization: Bearer $DUCTOR_ADMIN_TOKEN"Runtime config refs: RuntimeConfigService
RuntimeConfigService (/api/runtime-config, resource_type: "runtime_config")
is a read-only, always-redacted surface describing the runtime dependencies
an executable artifact needs — the secrets, variables, connection metadata, and
non-secret config it references. It never returns plaintext secret values; it
answers questions about config bindings:
| Question | RPC | HTTP |
|---|---|---|
| What refs does this artifact use? | ListArtifactConfigRefs | GET /api/runtime-config/refs |
| Are its required refs satisfiable? | ValidateRuntimeConfigBindings | POST /api/runtime-config/bindings:validate |
| What breaks if I change/rotate/delete X? | PreviewRuntimeConfigImpact | POST /api/runtime-config/impact:preview |
| Dry-run resolution (redacted) | ResolveRuntimeConfigDryRun | POST /api/runtime-config/resolve:dry-run |
| Who consumes this ref? | ListRuntimeConfigConsumers | GET /api/runtime-config/consumers |
| Why is a ref unresolved? | ExplainMissingRuntimeConfig | POST /api/runtime-config/missing:explain |
Use impact:preview before rotating a secret or deleting a variable to see every
artifact that would be affected — it's the safe pre-flight for config changes that
ripple across workflows and connections.
Routing pipeline mode
Pipeline mode selects how a routing request is executed. It's resolved
per-request down a chain — pool → tenant → global — with each level able to
inherit from the next.
The resolver (domain/pool/pipeline_mode.go, Resolve) walks the chain and
returns the first concrete (non-inherit) value: pool override wins, else
the tenant override, else the global setting. If all three levels are inherit
(the out-of-the-box state, the safe fall-through for an unknown value), it
resolves to dag — so a fresh install routes through the DAG runtime with no
configuration.
| Mode | Meaning |
|---|---|
dag | Run the request through the DAG runtime. The default when everything inherits. |
linear | Legacy inline pipeline in the routing goroutine — a kill-switch path. |
inherit | Defer to the next level in the resolution chain. |
shadow | Retired. A retained persisted enum value only — PipelineMode.Valid() still accepts it (so it appears in the CLI help and the config reference), but both the write path and the router reject it as retired. |
Shadow is a retained value, not a usable mode
shadow passes the enum's Valid() check because rows persisted before the
DAG cutover may still carry it — but you cannot set it or run on it. The CLI's
writer (PipelineModeWriter.SetPoolPipelineMode / SetTenantPipelineMode) and
the DAG dispatcher both fail with "pipeline_mode=shadow is retired; use
routing.stage_registry_shadow with the isolated graph." For actual
linear-vs-DAG parity/shadow evaluation, use the stage-registry shadow
mechanism (WithStageRegistryShadow), which runs one path authoritatively and
spawns an isolated parity comparison — not pipeline_mode=shadow.
Pipeline mode is a routing-domain setting, not an HTTP resource — there is
no /api/... endpoint to change it. It's set through the Ductor CLI (which
writes the pool's pipeline_mode or tenant_config.routing_pipeline_mode) and
propagated to every pod over a Redis pub/sub channel so the change lands
fleet-wide within the kill-switch budget.
# Set a single pool to the linear kill-switch path
ductor pipeline-mode set --pool $POOL_ID --mode linear
# Set the tenant-wide default
ductor pipeline-mode set --tenant $DUCTOR_TENANT --mode dag--pool and --tenant are mutually exclusive; exactly one is required. The
command accepts inherit|linear|shadow|dag at the flag level, but a shadow
write is rejected downstream as retired (see above).
What's runtime-writable, at a glance
| Setting | Runtime-writable? | How |
|---|---|---|
| Feature flags | Yes | PUT /api/config/flags/{key} |
| Per-tenant config overrides | Yes | PUT /api/tenants/{id}/config |
| Routing pipeline mode | Yes | ductor pipeline-mode set … (CLI) |
| Tenant quotas / features | Yes | PATCH /api/tenants/{id} |
| Pool kill-switch | Yes | POST /api/pools/{id}:set_enabled |
| Routing rules | Yes | Rules API (hot-reloaded) |
| DB / cache URLs, ports | No | DUCTOR_* env, restart required |
connector.encryption_key | No (rotate via keyring) | DUCTOR_CONNECTOR_ENCRYPTION_KEY, restart |
workflow_runtime.enabled | No | DUCTOR_WORKFLOW_RUNTIME_ENABLED, restart |
Where to go next
Action Connections
Manage connector connections operationally — create, test, list, and resolve tenant-scoped encrypted credential bindings that connector steps dispatch through.
Cases
Manage cases end to end — the create → assign/claim → resolve/escalate lifecycle plus comments, tasks, attachments, the event timeline, and table-row links.