# Add a connector & connection (/docs/guides/add-connector)



Connectors are the action inventory the **executed** stage draws on — how a
<Term name="Worker" /> reaches third-party systems when it does the work. Each
action carries typed, machine-readable semantics, and can carry a resolved
[price](/docs/connectors/action-pricing) and an
[assurance tier](/docs/connectors/assurance) — 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 [#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:

```bash
# generate a fresh 32-byte key
openssl rand -base64 32

export DUCTOR_CONNECTOR_ENCRYPTION_KEY="WfP5A9ipQBmbZlec5fZrMj9w/rSjlK5vSCbFvnr9l74="
```

<Callout type="warn" title="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`.
</Callout>

## 1. Discover available providers and actions [#1-discover-available-providers-and-actions]

List the providers your Ductor build ships, then inspect one to see its actions:

```bash
# 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 [#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`):

```bash
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 [#3-establish-a-connection]

How a connection is created depends on the provider's auth model. There are two
paths.

<Tabs items="[&#x22;API-key / static credential&#x22;, &#x22;Hosted connect-link (OAuth)&#x22;]">
  <Tab value="API-key / static credential">
    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.
  </Tab>

  <Tab value="Hosted connect-link (OAuth)">
    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:

    ```bash
    # 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.

```mermaid
sequenceDiagram
    participant App as Your service
    participant Ductor
    participant Portal as Connect-link page
    participant User as End user
    App->>Ductor: POST connect-sessions
    Ductor-->>App: session token dct_cs_
    App->>Portal: hand off token
    Portal->>User: prompt to authorize
    User-->>Portal: authorize
    Portal->>Ductor: complete authorization
    Ductor->>Ductor: persist encrypted connection
```
  </Tab>
</Tabs>

The resulting connection is resolved to an `AuthContext` at dispatch time by the
`ConnectionService`.

<Callout title="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.
</Callout>

## 4. Dispatch a step through the connection [#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`:

```yaml
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 [#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.                                                                                                       |

## Next steps [#next-steps]

<Cards>
  <Card title="Connectors" href="/docs/concepts/connectors">
    The provider / action / connection model in depth.
  </Card>

  <Card title="Credential lifecycle" href="/docs/connectors/credential-lifecycle">
    Encryption, rotation, refresh, and the pause/resume recovery flow.
  </Card>

  <Card title="Sync engine" href="/docs/connectors/sync-engine">
    How connectors pull and reconcile records from external systems.
  </Card>

  <Card title="Security & auth" href="/docs/operations/security">
    Credential encryption, key rotation, and egress controls.
  </Card>
</Cards>
