Action Connections
Creating and resolving connections, direct vs. route mode, connect sessions, and the management API with real request/response payloads.
A connection is one tenant's authenticated instance of a provider — the row a step actually dispatches against. This page covers the connection model, how a step picks which connection to use, and the management API you use to create and operate them.
The Connection model
A Connection (domain/connector/connection.go) is always partitioned by
TenantID and belongs to a (EnvironmentID, ProviderConfigKey) provider config.
The fields that matter to you:
| Field | Purpose |
|---|---|
ID | UUID; the handle you dispatch against. |
TenantID | Owning tenant — the isolation boundary. Every read and write is tenant-scoped. |
ProviderKey | Provider family (hubspot). |
EnvironmentID, ProviderConfigKey | The configured provider instance this connection belongs to. |
AuthType | The auth type this connection was created with. |
Label | Human-readable, tenant-chosen name. |
Status | Lifecycle state (below). |
ExternalAccountID | The provider-side account id (e.g. a HubSpot portal id) used to demux shared webhooks. |
ConnectionConfig | Non-secret per-connection config (subdomain, region, instance URL), stored as JSONB, referenceable from templated proxy fields via ${connectionConfig.x}. Not encrypted. |
Tags | Low-cardinality searchable attribution (env, region, customer_tier). |
Metadata | Non-secret operator/app context readable by hooks and routing CEL; not returned in ordinary list/get. |
CredentialsEnc, OAuthEnc | AEAD-encrypted credential and OAuth-token blobs. Set by the store; decrypted only in a request-scoped AuthContext. |
Config is wire-safe; credentials never are
ConnectionConfig, Tags, and Metadata are non-secret and travel over the
API. Credentials are a separate, always-encrypted blob — they are never
returned by any endpoint and only ever exist in plaintext inside the
short-lived AuthContext produced at dispatch. See
Authentication.
Status lifecycle
ConnectionStatus has five values:
| Status | Meaning |
|---|---|
active | Usable; the happy path. |
pending | OAuth started but the callback hasn't completed. |
expired | OAuth token expired and refresh failed. |
revoked | Revoked by the tenant or provider. |
errored | A test or dispatch surfaced a hard auth error. |
Only active connections resolve; the rest return a typed error at dispatch.
Choosing a connection: direct vs. route
A dispatch request carries a ConnectionMode (domain/connector/connection_mode.go):
direct(default) — resolve the namedConnectionID.route— let the connector routing interceptor select a connection at execution time, returning the chosen one inresolved_connection_id.
Route mode evaluates two mechanisms:
- Connection routes (
connection_route.go) — a per-(tenant, provider)ordered fallback chain. EachConnectionRoutehas aPriorityand aCELExpression; routes are scanned bypriority ASC, created_at ASC, and the first whose CEL is true (or empty, meaning match-all) is the primary, the rest are fallbacks. - Selection strategies (
connection_selection.go) — a pluggableConnectionSelectionPolicypicks a strategy:priority_chain(default),least_throttled,healthiest,scope_capability_match,region_affinity,cost_aware,weighted_canary,weighted_failover, orsticky_by_entity. Each strategy declares the facts it needs —least_throttledneeds theconnector_quotafact,scope_capability_matchneedsoauth_scopesandoperation_capabilities,cost_awareneedsusage_cost.
The two mechanisms in route mode compose in a fixed order — CEL routes narrow the candidate chain first, then the selection strategy picks within what survived:
Route mode is how you get failover and canaries
Direct mode is simplest and best when the caller knows exactly which account to use. Route mode is what you reach for when a tenant has several connections for the same provider and you want automatic failover, least-throttled selection, or a weighted canary — without the caller hard-coding a connection id.
The management API
Connector management lives under service ConnectorService
(api/proto/api/connector_service.proto), exposed over Connect-RPC and as REST
via grpc-gateway under /api/v2/connector/.... The examples below use the REST
surface. All requests are tenant-scoped by the caller's auth.
Create a connection
For auth types where you already hold the secret (API key, basic, secret text):
curl -X POST https://your-host/api/v2/connector/connections \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{
"provider_key": "hubspot",
"auth_type": "API_KEY",
"label": "Acme HubSpot (prod)",
"environment_id": "prod",
"provider_config_key": "hubspot-main",
"credentials": { "api_key": "pat-na1-xxxxxxxx" },
"connection_config": { "region": "na1" },
"tags": { "env": "prod", "customer_tier": "enterprise" }
}'credentials values whose fields are marked sensitive are encrypted and never
returned. connection_config is capped (max 16 keys, 4 KB per value, 64 KB
total). The response is a Connection with no credential material on the wire:
{
"id": "6f6d1c2e-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
"tenant_id": "acme",
"provider_key": "hubspot",
"environment_id": "prod",
"provider_config_key": "hubspot-main",
"auth_type": "API_KEY",
"label": "Acme HubSpot (prod)",
"status": "active",
"connection_config": { "region": "na1" },
"tags": { "env": "prod", "customer_tier": "enterprise" }
}For OAuth providers you don't hold the secret up front — you start an authorize flow and complete it on callback:
# 1. Start — returns an authorize URL, a pending connection, and a CSRF state.
curl -X POST https://your-host/api/v2/connector/oauth/start \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{
"provider_key": "salesforce",
"provider_config_key": "sf-main",
"environment_id": "prod",
"label": "Acme Salesforce",
"scopes": ["refresh_token", "api"],
"connection_config": { "instance": "acme" },
"tags": { "env": "prod" }
}'{
"authorize_url": "https://login.salesforce.com/services/oauth2/authorize?...",
"connection_id": "…pending-uuid…",
"state": "…csrf…"
}Redirect the user to authorize_url. When the provider calls back with a code,
complete the exchange:
# 2. Callback — exchanges the code and activates the pending connection.
curl -X POST https://your-host/api/v2/connector/oauth/callback \
-H 'Content-Type: application/json' \
-d '{ "state": "…csrf…", "code": "…authcode…", "redirect_url": "https://app.example.com/cb" }'The response is an activated Connection.
connection_config for templated OAuth URLs
Some providers template their authorize and token URLs with
${connectionConfig.x} (a per-customer subdomain or region). For those,
connection_config is required at oauth/start so the URLs can be
resolved.
Test, list, update, delete
# Test — actively probes the provider and reports a structured result.
curl -X POST https://your-host/api/v2/connector/connections/{id}/test \
-H 'Authorization: Bearer <token>'
# → { "ok": true, "tested_at": "...", "error_code": "", "retryable": false, "needs_reconnect": false }
# List — filter by provider, tags, environment, or provider config.
curl 'https://your-host/api/v2/connector/connections?provider_key=hubspot&environment_id=prod' \
-H 'Authorization: Bearer <token>'
# Update / delete
curl -X PATCH https://your-host/api/v2/connector/connections/{id} -d '{ "label": "..." }'
curl -X DELETE https://your-host/api/v2/connector/connections/{id}Execute an action against a connection
curl -X POST https://your-host/api/v2/connector/execute-action \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{
"action_key": "hubspot.create_contact",
"connection_id": "6f6d1c2e-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
"connection_mode": "direct",
"provider_config_key": "hubspot-main",
"inputs": { "email": "[email protected]", "firstname": "Jane" }
}'For route mode, send "connection_id": "", "connection_mode": "route", and a
route provider key; the response carries resolved_connection_id. To run the
same work durably in the background, POST /api/v2/connector/action-jobs instead
(see the async job model in Architecture).
Connect sessions: hosted OAuth for your end users
When you're embedding Ductor and want your end users to link their accounts
without your backend proxying secrets, use a connect session
(domain/connector/connect_session.go, service ConnectSessionService). You
create a short-lived, signed session server-side and hand the user a hosted link:
curl -X POST https://your-host/api/v2/connector/connect-sessions \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{
"tenant_id": "acme",
"end_user": { "external_id": "user-42", "email": "[email protected]", "organization": "Acme" },
"allowed_providers": ["hubspot", "salesforce"],
"policy": { "allowed_scopes": ["contacts.read"], "max_connection_ttl": "86400s" },
"environment_id": "prod",
"provider_config_key": "hubspot-main",
"ttl": "900s"
}'{
"session_id": "…",
"token": "dct_cs_…",
"connect_link": "https://connect.your-host/connect/…",
"expires_at": "2026-07-11T…Z"
}The token is returned exactly once — storage only holds its SHA-256 hash. The
user opens connect_link, which is served by a pre-auth /connect/* router
(transport/gateway/connectrouter/) that runs the OAuth flow and creates the
connection scoped to the session's allowed_providers and downscoped
allowed_scopes. Sessions are one-shot and revocable.
Guided setup
For artifacts that need more than a single credential to be usable — a provider
config plus a webhook subscription plus a field mapping — Ductor has a resumable
setup flow (domain/connector/setup.go, setupstore/). A
ConnectorSetupProfile declares the ordered ConnectorSetupRequirements
(authorization, connection config, webhook subscription, mapping profile,
permission, readiness check, …), and a ConnectorSetupRun walks a caller through
submitting values, validating, and applying them, with readiness gates per
surface (action, sync, webhook, workflow, routing, mcp, promotion).
The RPCs live under /api/v2/connector/setup/....
Where to go next
- Authentication — how a resolved connection becomes a usable token.
- Triggers & Polling — inbound events from a connection.
- Policies, Quotas & Audit — governing what a connection may reach.
Action Authentication & Credentials
Every connector auth type, how AuthContext is resolved at dispatch, and how credentials are AEAD-encrypted at rest with XChaCha20-Poly1305, BYOK, and key rotation.
Credential Lifecycle
The credential control plane — health facts, the requirement scanner, hosted Connect-Link authorization requests, and auto-resume on reconnect.