Managing Resources

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

TierHow it's setWhen it takes effectExamples
Static (env/file)configs/ductor.yaml + DUCTOR_* env vars, read once at bootProcess restartdatabase.url, cache.url, server.http_addr, connector.encryption_key, workflow_runtime.enabled
Dynamic (runtime-writable)API + CLI, backed by a store with pub/sub invalidationLive, across all podsFeature 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:

  1. 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.
  2. The file-based feature-flag / A-B system (internal/configflags.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".

OperationRPCHTTPAction
Snapshot the full configGetConfigSnapshotGET /api/configread
Get version + hashGetVersionGET /api/config/versionread
List feature flagsListFlagsGET /api/config/flagsread
Toggle a feature flagToggleFlagPUT /api/config/flags/{key}write
Force a reloadReloadConfigPOST /api/config/reloadadmin

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".

OperationRPCHTTPAction
Get tenant overrides (raw)GetTenantConfigGET /api/tenants/{tenant_id}/configread
Replace tenant configSetTenantConfigPUT /api/tenants/{tenant_id}/configwrite
Get resolved configGetResolvedConfigGET /api/tenants/{tenant_id}/config/resolvedread

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:

QuestionRPCHTTP
What refs does this artifact use?ListArtifactConfigRefsGET /api/runtime-config/refs
Are its required refs satisfiable?ValidateRuntimeConfigBindingsPOST /api/runtime-config/bindings:validate
What breaks if I change/rotate/delete X?PreviewRuntimeConfigImpactPOST /api/runtime-config/impact:preview
Dry-run resolution (redacted)ResolveRuntimeConfigDryRunPOST /api/runtime-config/resolve:dry-run
Who consumes this ref?ListRuntimeConfigConsumersGET /api/runtime-config/consumers
Why is a ref unresolved?ExplainMissingRuntimeConfigPOST /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.

concrete inherit concrete inherit concrete inherit routing request pool mode use resolved mode tenant mode global mode dag (default)
ModeMeaning
dagRun the request through the DAG runtime. The default when everything inherits.
linearLegacy inline pipeline in the routing goroutine — a kill-switch path.
inheritDefer to the next level in the resolution chain.
shadowRetired. 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

SettingRuntime-writable?How
Feature flagsYesPUT /api/config/flags/{key}
Per-tenant config overridesYesPUT /api/tenants/{id}/config
Routing pipeline modeYesductor pipeline-mode set … (CLI)
Tenant quotas / featuresYesPATCH /api/tenants/{id}
Pool kill-switchYesPOST /api/pools/{id}:set_enabled
Routing rulesYesRules API (hot-reloaded)
DB / cache URLs, portsNoDUCTOR_* env, restart required
connector.encryption_keyNo (rotate via keyring)DUCTOR_CONNECTOR_ENCRYPTION_KEY, restart
workflow_runtime.enabledNoDUCTOR_WORKFLOW_RUNTIME_ENABLED, restart

Where to go next