Guides

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.

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: fail

When 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

ConfigWhat it does
connector.action_rate_limit.enabledPer-tenant rate limiting of connector actions (with fail_open posture).
connector.tenant_provider_policy.enabledEnforce per-tenant provider allow/deny policy.
egress.modeGuard outbound calls (shadow / enforce / disabled) with domain and IP-net allowlists (egress.allowed_domains, egress.allowed_ip_nets).
connector.synthetic_canaries.enabledPeriodically probe connections for health.

Next steps