Managing Resources
The Ductor management API surface — how to provision and operate tenants, keys, pools, rules, workflows, experiments, connections, and configuration over REST and Connect-RPC.
Everything Ductor routes, runs, and dispatches is a resource you manage through the API. These are the resources that shape the clearing surface: pools are the markets Work enters, recipients are the Workers a decision selects, and rules, workflows, publishers, and connections define how work is priced, routed, and delivered. The dashboard is optional — all of them are created, updated, inspected, and retired over the same REST + Connect-RPC surface your application already calls. This section is the operator's reference for that surface — what each resource is, why and when you manage it, how to do it with real API calls, and where it lives (endpoint, aggregate, storage).
What this section is not
This is the management (control-plane) surface — the CRUD and lifecycle operations that shape your routing and workflow topology. The execution surface (submitting an event to be routed, triggering a run) is covered under Core Concepts. Managing what a decision chooses from lives here; making the decision lives there.
The resource map
Each resource has its own page. Read them in roughly this order — later resources reference earlier ones.
Tenants
The top-level isolation boundary. Provision a tenant, set its quotas, metadata, and feature flags before anything else exists under it.
API Keys
Mint, list, and revoke the tenant-scoped keys that authenticate every other call. Roles, scopes, expiry, and environment constraints.
Pools & Recipients
Routing targets. A pool is a container with a strategy and kill-switch; recipients are the concrete endpoints with capacity and state.
Rules
CEL-based routing rules attached to a pool — priority, enable/disable, and the cross-instance cache invalidation that makes edits take effect live.
Route Authoring
Route DSL v2 — the single public format for authoring routes as one versioned RouteAggregate per tenant that compiles down to the workflow DAG.
Workflows
Workflow definitions (draft → publish → immutable version) and run operations (pause, resume, cancel, signal, continue-as-new).
Experiments
Durable experiments for routing strategies, workflow steps, and agents, with sticky assignments, exposure receipts, and delayed scores.
Connections
Tenant-scoped, encrypted credential bindings to third-party providers that connector steps dispatch through.
Configuration
The runtime-writable config surface — system config snapshots, feature flags, per-tenant overrides, and the routing pipeline mode.
Cases
Create, assign, claim, resolve, and escalate human or agent-assisted operational work.
Environments
Model dev, staging, and production boundaries with protection and readiness policies.
Secrets, Variables & Contexts
Store encrypted secrets, plaintext variables, and tenant-scoped context data.
Publishers
Govern inbound traffic sources with caps, quality thresholds, pricing, and delivery statistics.
Two API generations: /api and /api/v2
Ductor's REST surface is generated from the same protobuf definitions as the
Connect-RPC API, so every operation exists in both dialects. As the platform
grew, newer services were introduced under a /api/v2 prefix while the
established resource CRUD kept its /api paths. Both are current and supported —
the prefix simply tells you which generation a service belongs to.
| Generation | Used by | Path style |
|---|---|---|
/api | Tenants, pools, recipients, rules, experiments, workflow definitions, workflow run reads, system config | Plural resource nouns: POST /api/pools, GET /api/tenants/{tenant_id} |
/api/v2 | API keys, workflow run control + signals, connections + connect sessions, runtime config | Sub-resources and RPC-style verbs: POST /api/v2/workflow-runs/{run_id}:pause |
The :verb suffix you'll see on some /api/v2 paths (:trigger, :pause,
:approve) is the standard grpc-gateway custom-method syntax. It's a literal
part of the URL — POST /api/v2/workflow-runs/{run_id}:cancel is one path,
not a path plus a query.
Authentication and tenancy on every call
Every management endpoint is authenticated and tenant-scoped. Two things travel with each request:
Authorization: Bearer <token>— a JWT from your OIDC provider, or a Ductor API key as the fallback credential.X-Tenant-ID: <uuid>— the tenant the call operates within.
export DUCTOR_API_KEY="duk_live_..."
export DUCTOR_TENANT="9c8b7a6d-1234-4e5f-8a9b-0c1d2e3f4a5b"
curl -s https://api.ductor.io/api/pools \
-H "Authorization: Bearer $DUCTOR_API_KEY" \
-H "X-Tenant-ID: $DUCTOR_TENANT"The tenant comes from the request context, never the body
Where a resource is tenant-scoped, the server takes the tenant identity from
the authenticated request — the X-Tenant-ID header and token claims — and
ignores any tenant_id in the payload. You cannot create a resource in a
tenant you are not authenticated against. This is enforced uniformly; see
Tenancy for why isolation is an end-to-end
invariant. Full details of the auth model — JWT verification, API-key
fallback, RBAC — live in the Authentication section.
Authorization: resource + action + roles
Each RPC declares its own authorization requirement. The interceptor chain evaluates three things against your token:
resource_type— what kind of object the call touches (pool,recipient,rule,workflow,workflow_run,workflow_definition,connector,tenant,config, …).action—read,write,delete, oradmin.roles— an any-of role gate on sensitive operations. Ductor's built-in roles areviewer,operator,admin, andservice. Tenant provisioning and API-key minting, for example, requireadmin.
Each resource page lists the exact resource_type:action (and any role gate)
for its operations so you can scope keys to least privilege.
Conventions shared across every resource
Pagination
List endpoints take a pagination object and return a cursor. Page size caps at
1000, defaults to 50.
# First page
curl -s "https://api.ductor.io/api/pools?pagination.page_size=25" \
-H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"
# Next page — feed back the token you were handed
curl -s "https://api.ductor.io/api/pools?pagination.page_size=25&pagination.page_token=eyJvZmZzZXQiOjI1fQ==" \
-H "Authorization: Bearer $DUCTOR_API_KEY" -H "X-Tenant-ID: $DUCTOR_TENANT"Every list response carries a pagination block:
{
"pagination": {
"next_page_token": "eyJvZmZzZXQiOjUwfQ==",
"total_count": 250
}
}next_page_token is an opaque cursor — treat it as a blob, don't parse it.
It's empty on the last page. total_count may be -1 when the total is too
expensive to compute.
Response metadata
Every response — success or list — includes a metadata object for
observability:
{
"metadata": {
"trace_id": "trace-abc123def456",
"request_id": "req-789ghi012",
"fetched_at": "2026-07-11T10:30:00Z"
}
}trace_id is the OpenTelemetry trace ID — quote it in a support ticket and the
whole request is reconstructable end to end.
Errors are Problem JSON
REST errors follow RFC 9457 Problem Details. Domain sentinels map to stable HTTP statuses, and field-level validation failures are itemized:
{
"type": "https://ductor.io/errors/validation",
"title": "Invalid argument",
"status": 400,
"detail": "name: must not be empty",
"errors": [
{ "field": "name", "description": "must not be empty", "reason": "FIELD_VIOLATION" }
]
}| HTTP | Meaning | Typical cause |
|---|---|---|
400 | Invalid argument | Malformed body, failed field validation |
401 | Unauthenticated | Missing/expired token |
403 | Permission denied | Token lacks the required role or scope |
404 | Not found | Resource ID doesn't exist in this tenant |
409 | Conflict | Uniqueness violation, or a state transition that isn't allowed |
412 | Failed precondition | Optimistic-lock version mismatch (see below) |
429 | Quota exceeded | A tenant quota or rate limit was hit |
The Connect-RPC surface returns the equivalent Connect/gRPC codes for the same sentinels.
Optimistic concurrency
Mutable resources that a coordinator or multiple operators can touch carry a
version counter (record_version on pools, draft_revision on workflow
drafts, db_record_version on workflow runs). Where an operation accepts an
expected_version, the server rejects the write with 412 Failed Precondition
if the stored version has moved on — you re-read, reconcile, and retry. This is
the same optimistic-locking discipline the coordinator uses internally; see
Optimistic Locking.
Where to start
If you're standing up a tenant from scratch, the path is linear:
1. Provision a tenant
Create the isolation boundary and set its quotas.
2. Mint an API key
Get a credential scoped to the roles your integration needs.
3. Build routing topology
Create pools and recipients, then attach rules.
4. Publish a workflow
Draft, validate, and publish a definition; then trigger and operate runs.
Agent Work Steps
The worker-side AI step types — ai_action for a single inference and ai_agent for an execution-bound agentic loop — that run inside a DAG workflow run.
Tenants
Provision and manage tenants — the top-level isolation boundary — with quotas, metadata, feature flags, and soft-delete lifecycle.