Add a connector & connection
Register a provider config, establish an encrypted connection, and dispatch a step to a third-party system.
Connectors are the action inventory the executed stage draws on — how a Worker reaches third-party systems when it does the work. Each action carries typed, machine-readable semantics, and can carry a resolved price and an assurance tier — both opt-in, both off by default. Offering the inventory as a catalog an agent buys from against a spending cap remains on the roadmap. The model has three layers:
- Provider — a definition of a system Ductor can talk to (Stripe, an HTTP
API, a CRM…), keyed by
providerKey. Providers expose actions, each keyed by(providerKey, actionKey)in the Action Registry. - Provider config — a tenant's configuration of a provider (defaults, policy, credentials envelope).
- Connection — a concrete, authenticated link (an
AuthContext) that a step dispatches through. Credentials are AEAD-encrypted at rest.
This guide registers a provider config and establishes a connection so a workflow action step can call out.
Prerequisite: the connector encryption key
Connector credentials are encrypted at rest with XChaCha20-Poly1305. Ductor requires a base64-encoded 32-byte AEAD master key. Set it before creating any connection:
# generate a fresh 32-byte key
openssl rand -base64 32
export DUCTOR_CONNECTOR_ENCRYPTION_KEY="WfP5A9ipQBmbZlec5fZrMj9w/rSjlK5vSCbFvnr9l74="Treat this key like a root secret
The encryption key protects every stored connector credential. Losing it means
losing access to all encrypted connections; leaking it exposes them. In
production, inject it from a secret manager — never bake it into an image. For
rotation, Ductor supports a decrypt-only rotation keyring
(connector.rotation_keys) alongside the active connector.encryption_key_id.
1. Discover available providers and actions
List the providers your Ductor build ships, then inspect one to see its actions:
# all providers
curl -s http://localhost:8080/api/v2/connector/providers \
-H "Authorization: Bearer $DUCTOR_TOKEN"
# one provider's definition (and its actions)
curl -s http://localhost:8080/api/v2/connector/providers/stripe \
-H "Authorization: Bearer $DUCTOR_TOKEN"The providerKey and each actionKey you find here are what workflow action
steps reference as provider.resource.action (e.g. stripe.invoices.create).
2. Create a provider config
A provider config binds a provider to your tenant. Create one, then drive it
through its lifecycle (activate / disable / archive):
curl -s -X POST http://localhost:8080/api/v2/connector/provider-configs \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"provider_key": "stripe",
"display_name": "Stripe (production)"
}'The connector API surface is large — ConnectorService also covers provider
config get/list/update, external MCP provider specs, tool snapshots, discovery
receipts, and lifecycle transitions such as
.../provider-configs/{id}:activate.
3. Establish a connection
How a connection is created depends on the provider's auth model. There are two paths.
Supply the secret when you create the connection; Ductor encrypts it with the AEAD key and stores only the ciphertext. Nothing leaves your control plane.
For OAuth providers — or when you want an end user to authorize a connection
without your service ever touching their secret — use the connect-session
flow. Enable it with connector.connect_session.enabled=true and set
connector.connect_session.base_url to your connect-link origin, then mint a
session:
# start a hosted connect session
curl -s -X POST http://localhost:8080/api/v2/connector/connect-sessions \
-H "Authorization: Bearer $DUCTOR_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "provider_key": "stripe" }'
# revoke it later
curl -s -X POST http://localhost:8080/api/v2/connector/connect-sessions/$SESSION_ID/revoke \
-H "Authorization: Bearer $DUCTOR_TOKEN"The response carries a short-lived session token of the form
dct_cs_<random>, returned once at create time (only its hash is stored).
Hand that token to the hosted connect-link page; the end user completes the auth
there and Ductor persists the resulting encrypted connection. Sessions are
short-TTL by design — mint one per authorization attempt.
The resulting connection is resolved to an AuthContext at dispatch time by the
ConnectionService.
Expiring credentials pause a run — they don't fail it
With connector.connection_recovery.enabled=true, a step that hits
an expired or refresh-failed credential pauses on the DAG event-pause
substrate instead of failing. When the connection is repaired — a token
refresh or a fresh connect-link reconnect — a resume event unblocks the parked
step and the run continues from where it stopped. Refresh attempts are bounded
by connector.connection_recovery.max_refresh_attempts (default 4), after
which the connection short-circuits the refresh path and waits for an explicit
reconnect.
4. Dispatch a step through the connection
Reference the connection from a workflow action step. The connection_ref can
be a literal connection ID or a templated variable, with connection_ref_mode: direct:
steps:
- ref: create_invoice
type: action
action: stripe.invoices.create
connection_ref: "${{ VARS.stripe_connection_ref }}"
connection_ref_mode: direct
args:
customer_id: "${{ TRIGGER.payload.customer_id }}"
amount: "${{ TRIGGER.payload.amount_cents }}"
currency: "${{ TRIGGER.payload.currency }}"
retry_max_attempts: 3
timeout_ms: 10000
on_error: failWhen the Coordinator schedules this step, a Step Worker resolves the connection, decrypts the credential in memory, invokes the action, and records the outcome as an attempt.
Hardening options worth knowing
| Config | What it does |
|---|---|
connector.action_rate_limit.enabled | Per-tenant rate limiting of connector actions (with fail_open posture). |
connector.tenant_provider_policy.enabled | Enforce per-tenant provider allow/deny policy. |
egress.mode | Guard outbound calls (shadow / enforce / disabled) with domain and IP-net allowlists (egress.allowed_domains, egress.allowed_ip_nets). |
connector.synthetic_canaries.enabled | Periodically probe connections for health. |