Skip to content

Connectors and credentials

How Axon connects to external services (Gmail, Telegram, CRM, drives, webhooks) and where the credentials live. This manual answers: what a "connector" and a "credential" are and why they're not the same thing; who can do what; what the Allowlist and Add instance buttons in the Connectors section do; how to walk the full path from a new project to a working integration.


1. What it is and why

In Axon, a single integration with an external service splits along two orthogonal axes:

  • Connector — this is code ("how to talk to the service"): an adapter class (GmailConnector, TelegramConnector…) that can call the service API, normalize inbound events, verify webhook signatures. Plus a declarative description of the credential format (CredentialType). Connectors are the same for all projects; they're not in the DB — they're deployed with the platform.
  • Credential — this is the data of a specific account ("with whose authority"): the encrypted secret (OAuth token, API key) + its lifecycle (active / expired / revoked, when it was last refreshed, when it expires). A credential is project-scoped, lives in the DB (app.connector_credentials), and is created and managed via the Console.

Between them there is an intermediate entity — the connector instance ("a configured connection") = adapter + credential + non-secret settings. A workflow references instances (via named slots), not the connector code and not the credential directly.

Why split them (instead of "one entity, an integration"): - the adapter code and the token have different lifecycles — the code changes per release, the token expires/gets revoked/refreshed all the time; - security boundary: app-api works freely with the adapter, but never sees the plaintext secret — decryption is only possible in the worker (invariant I-1); - 1:N: one credential (one Gmail account) can feed several instances with different settings; one adapter — many instances; - project isolation: a credential is strictly bound to a project, can't be shared across projects; the adapter is shared; - audit and governance: every touch of the secret is a separate audit-log entry, separate permissions, a separate approval gate.

A 60-second analogy: CredentialType = the ID-card blank (which fields exist). Credential = your actual ID card in the safe (the number is encrypted; on the spine you see "valid until", "check OK"). Connector (adapter) = the skill of driving this service's car. ConnectorInstance = "this particular car I drive with this ID, with the mirrors adjusted for me". BindingProfile = the dispatcher's note "for this run, take that car". The safe (the worker) is opened with its own key; the dispatcher (app-api) sees "the ID exists, valid, bound to car #3" — but not the digits themselves.


2. Roles and access

Source: core/models/auth.py. owner / admin are instance-scoped (act across all projects). manager / operator / reviewer / read_only are project-scoped (only in projects from their project_scopes). system — service actors (auto-refresh, health, retention); these rights are not granted to humans (invariant I-9: HUMAN ∩ SYSTEM_ONLY = ∅).

Action Permission owner admin manager operator reviewer read_only system
View connectors / instances / health read*
View credentials (redacted) credential:read
Allowlist toggle (enable/disable a connector for a project) project_connector:manage
Add instance / update / delete connector instance connector:manage
Configure a workflow binding profile (slot → instance) workflow:configure_bindings
New credential / OAuth connect credential:create
Update a credential (metadata/payload) credential:update
Revoke a credential credential:revoke
Test connection credential:test
Use a credential in a run credential:use
Approve a high-stakes credential proposal approve
Start a workflow (uses the bound instances) start_workflow
Reauthorize OAuth / refresh-rotate credential:rotate ✅† ✅† ✅†
Auto-healing / maintain credential:maintain
Hard-purge / GDPR erasure credential:purge
Webhook ingress verifier upsert/disable ingress_verifier:manage

* there is no separate connector:read permission in core/models/auth.py — viewing connectors/instances/health is gated by read + having project access (the canonical query-route docs sometimes write connector:read as shorthand; the RBAC layer covers it via read). † credential:rotate is held by owner/admin (via HUMAN_PERMISSIONS), by manager (via _CREDENTIAL_MANAGER_PERMISSIONS) and by system. Automatic OAuth refresh (refresh_oauth_credential) runs only as system (handler-level allowlist (command_type, actor_id, source)); manual reauthorize (complete_oauth_credential in reauthorize mode) — manager+. There is no separate rotate_credential command in the current version. ‡ credential:purge (and credential:maintain) — in SYSTEM_ONLY_PERMISSIONS, not granted to humans (I-9). The purge_credential command itself (and delete_credential with deletion_reason) is not yet implemented — that's the beyond-Stage-3 backlog (CONCEPT-CONNECTORS.md §0.1); the GDPR DDL columns (deleted_at/deletion_reason/purge_after/purged_at) already exist.

The key point: manager is not "view-only". A manager in their project can create credentials, create connector instances, toggle the allowlist, configure binding profiles, and use all of it in runs. Truly "view-only" are reviewer and read_only. operator — view + test + use + start workflows, but does not create credentials/instances and does not touch the allowlist. Creating projects and users (create_project, manage_users) is owner / admin only.

credential:use ≠ the right to decrypt. It's a "project membership capability" — needed so the runtime can use the credential on the actor's behalf. The plaintext is still decrypted only in the worker; the Console shows redacted data (no tokens).

2.1 Architect / Engineer — a role outside RBAC

"Architect" (the programmer) is not one of the core/models/auth.py roles; it's a persona that works at the git + deploy level, not in the Console. The "code ↔ Console" boundary in this domain:

Done in code (modules/, IDE, git) Done in Console (as manager+)
Writes connector adapters — modules/connectors/{gmail,telegram,...}/ (subclasses of ConnectorBase: API + MCP tools + normalize() + verify_signature() + action manifest)
Writes CredentialTypemodules/credential_types/*.py (field format, auth_inject, OAuth2 metadata, test_request, extends) → CredentialTypeRegistry
Writes WorkflowDefinition with ConnectorRequirement[] — named slots + logical types + required_actions Publishes the definition (publish_definition) — if they have a Console role of manager+; otherwise a separate manager publishes
Connects credentials, creates connector instances, configures the allowlist and binding profiles

So "a connector as a capability" and "the credential format" appear in code (architect + deploy), while "a specific credential", "an instance", "the allowlist", "the binding to a workflow" — in the Console (manager+). All adapters and CredentialTypes are shared by all projects and are not stored in the DB.


3. Where it is in Console

Two adjacent sections in the sidebar under the Administration group:

3.1 The "Connectors" section — /{project_id}/connectors

Consists of three blocks.

The "Project connector allowlist" block — a table "which connector keys are allowed in this project".

Column / element What it is
Connector connector_key — a concrete connector key (e.g. gmail_oauth2_test2). Block caption: "Concrete connector keys only" — these are concrete keys here, not logical types.
Credential Which credential is associated with this key directly (or No active credential if the binding lives only in a connector instance below).
Allowlist (toggle) Enable/disable the connector for the project. ON = the connector is allowed (inbound webhooks on this key are accepted, the binding resolver sees the connector). OFF = fail-closed: webhooks are rejected (generic 401, no event created), the connector is unavailable for bindings. Caption: "Credentials stay intact when a connector is disabled" — disabling does not touch credentials and instances; it's a "kill switch", not "deletion". Available to manager+ (project_connector:manage); under the hood — the set_project_connector_enabled command, writes a row in app.project_connector_allowlist.
Updated When the toggle was last changed.
New credential button (top of the block) Jumps into the credential-creation flow (see §3.2 / §5).

The "Connector instances" block — a table of configured connections (app.connector_instances).

Column / element What it is
Instance Display name + connector_key as a subtitle (e.g. gmail_oauth2 (migrated) / gmail_oauth2_test2). The (migrated) tag — the instance was carried over from the old file-mount flow.
Credential Which credential the instance references (credential_id is mandatory — an instance without a credential makes no sense).
Adapter adapter_key — the concrete adapter factory (gmail, telegram, bitrix24…).
Status The instance lifecycle status: active / paused / error. Below in small print — health_status: healthy / degraded / down / unknown (operational health; unknown until the runtime has reported a status).
Actions The adapter's action manifest (D45): which concrete operations this instance publishes (search_email, send_email…). The workflow validator checks a step's required actions against this list.
Manage (...) Menu: change display_name / config; pause / resume (status change via update_connector_instance); delete. credential_id / connector_type / adapter_key / connector_key are immutable after creation (update_connector_instance mutates only display_name/config/status) — to change the credential or adapter, create a new instance. Delete = soft-delete/tombstone (D41): the row stays for audit/replay; blocked if the instance is referenced by a binding profile or an active/pending run snapshot. The UI offers "find-and-replace" before deletion.
Add instance button (top of the block) Create a new connector instance — see §5, flow 3.

The "Connector health" block — a per-connector operational-health table. It appears after a connector has reported a runtime status (probe / actual use). Empty for now — "No connector health rows / Health appears after a connector reports runtime status". Written by the system via the internal update_connector_health command (internal, 403 from external) → app.connector_health_snapshots + connector_instances.health_status. Auto-pause of an instance on sustained down — an auto-healing-monitor feature (designed, see CONCEPT-CONNECTORS.md §0.1 backlog); in the current version a human sets the pause via update_connector_instance (status='paused').

3.2 The "Credentials" section — /{project_id}/credentials

A list of the project's credentials (redacted — no secrets) + the create button.

Column / element What it is
Display name A human-readable name (Sales Inbox). Unique within (project, credential_type_key) among non-deleted rows.
Type CredentialType (Gmail (OAuth 2.0), Generic HMAC Webhook, …) with an icon.
Status active / expired / revoked / error.
Last test When and with what result the last test_credential ran (success / failure). On failure — a compact error message (≤500 chars, no token leak).
Last used / Last refresh When the credential was last used / the last successful OAuth refresh. As refresh_failure_count > 0 accumulates, the credential is flagged "distressed".
Expires If known (refresh tokens often don't expire).
Actions Test connection; Reauthorize (for OAuth — a fresh consent); Revoke (soft tombstone — takes the credential out of circulation; dependent connector instances then become unusable per D42; blocked if the credential is referenced by an active/paused workflow or an active/pending run that uses a dependent instance). A full "delete"/GDPR purge — beyond Stage 3, not yet implemented.
New credential button The creation wizard — see §5, flow 2.

4. Concepts (mental model)

A layered model of five entities:

CODE LAYER (shared by all projects, not in the DB)
  ├─ Connector adapter        modules/connectors/{gmail,telegram,...}/
  │     ConnectorBase: API + MCP tools + normalize() + verify_signature()
  │     identity: connector_key (legacy) / adapter_key (concrete factory)
  └─ CredentialType           modules/credential_types/*.py → CredentialTypeRegistry
        field format, auth_inject, OAuth2 metadata, test_request
        inheritance: extends: ["generic_oauth2"]   (a new OAuth provider ~50-100 lines)

DATA LAYER (project-scoped, app.*, mutations only via CommandEnvelope)
  ├─ Credential               app.connector_credentials   id: cred_<ulid>
  │     encrypted secret (envelope: DEK under KEK, AES-256-GCM)
  │     + lifecycle: status, last_test_*, last_used_at, last_refresh_at,
  │       refresh_failure_count, expires_at, soft-delete + GDPR (purge_after/purged_at)
  ├─ ConnectorInstance        app.connector_instances     id: ci_<ulid>
  │     = adapter_key + credential_id (NOT NULL) + connector_type (logical)
  │       + connector_key (runtime registry key, unique per project)
  │       + config (ONLY non-secret: polling_interval_seconds, webhook_url,
  │         label_ids, allowed_chat_ids) + status + health_status
  ├─ project_connector_allowlist   (project_id, connector_key) → enabled   (SoT toggle)
  └─ connector_ingress_verifiers   secret projection for webhook verification (D54)

WORKFLOW-BINDING LAYER
  ConnectorRequirement[]  (in WorkflowDefinition, written by the engineer: "need email + crm",
        named slots + logical type + required_actions)
    → WorkflowBindingProfile  (admin/manager, once: main_email → ci_gmail_sales)
      → workflow_run_bindings_snapshot  (pinned per run, D17/D36)

Three different "names" of a connector (don't confuse them): - connector_type — a logical capability: email, crm, storage. This is what the workflow declares ("I need email"). - adapter_key — a concrete provider/factory: gmail, imap, smtp, bitrix24. This is what the admin picks when creating an instance. - connector_key — the runtime registry key of a specific instance (gmail_sales, gmail_support). Unique within the project among non-deleted rows (connector_instances_unique_active_key_idx). The allowlist operates on connector_key.

The connector_type ≠ adapter_key decoupling (decision D35) means: the workflow is not tied to Gmail — it asks for "email", and the admin can satisfy that with Gmail, or IMAP, or SMTP. Action compatibility is not derived from the type: the adapter must publish an action manifest, and the validator checks a step's required_actions against it (D45) — an email connector without send_email won't satisfy a send_email step's requirement.

1:N. One credential → many instances: one Gmail account → instance gmail_sales (labels "Sales") and instance gmail_support (labels "Support"), each with its own connector_key and its own config but one credential_id. That's why connector_instances is a separate table with an FK to credential_id (the composite FK (project_id, credential_id) prevents cross-project leaks at the DB level).

Three override layers at run start (D17): profile.bindings ← case.connector_overrides ← run.binding_overrides. The profile — the baseline (90% of cases); a case override — a rare exception (multi-bot Telegram, BYOK SaaS); a run override — test/one-off runs.


5. Flows: step-by-step scenarios

Flow 1 — The full path: from a new project to a configured integration

Phase What's done Who (role / permission)
0. Platform code Write a connector adapter (modules/connectors/...) and a CredentialType (modules/credential_types/...); deploy. This is shared by all projects, not done in the Console. Platform engineer (git + deploy)
1. Create the project An app.projects row, a basic ProjectConfig. owner / admin (create_project)
2. Provision people Create users, grant roles and project_scopes (e.g. manager on this project — they'll configure the rest). owner / admin (manage_users)
3. Publish workflow definitions The engineer writes a WorkflowDefinition in code with ConnectorRequirement[] (named slots + logical types + required_actions). Publishing via the API. On the first publish, if the project already has exactly one usable instance per required type → a binding profile is auto-created (status=complete, auto_configured=true, "Review" banner). Engineer (code) → publish: manager+ (publish_definition)
4. Connect credentials Console → Credentials → New credential → pick a CredentialType → display name → "Connect with Google" (OAuth consent → callback) → auto test_credential → credential saved (status=active). Repeat for each account. For high-stakes types (requires_approval=true) — a credential_proposal is created; the credential materializes only after approve. manager+ (credential:create); approval — reviewer/manager+ (approve)
5. Create connector instances Console → Connectors → Add instance → adapter (gmail) + credential (Sales Inbox) + non-secret config (label_ids, polling_interval_seconds, webhook_url) + display name → the create_connector_instance command → an app.connector_instances row + outbox events (connector_instance.created, and where needed an ingress-verifier projection and provider webhook registration — asynchronously in the worker). Repeat for each instance. manager+ (connector:manage)
6. Enable connectors in the allowlist Console → Connectors → Allowlist toggle ON for each connector_key → the set_project_connector_enabled command. Without it webhooks are fail-closed and the connector is unavailable for bindings. manager+ (project_connector:manage)
7. Configure the binding profile Console → Workflows → <definition> → Bindings → "Auto-map" (ConnectorMapper proposes instances by type) or pick an instance per slot manually → the set_workflow_bindings command → profile status=complete. Set-once-use-many. manager+ (workflow:configure_bindings)
8. Run workflows Console → Run Wizard → pick a case → bindings are pulled from the profile automatically; optionally override for this run. The run pins workflow_run_bindings_snapshot (D36, system). On a connector step the worker resolves the instance → takes the credential → decrypts it in worker memory → policy gate / approval / audit / idempotency → side effect. operator+ (start_workflow + credential:use)
9. Operations Automatic OAuth refresh (system, refresh_oauth_credential); reauthorize on a refresh failure (deeplink alert via Telegram → manager/admincomplete_oauth_credential); health monitoring (system → update_connector_health); revoke a credential / pause-resume-delete an instance (manager+). GDPR tooling (delete_credential with deletion_reason, hard-purge sweeper on purge_after) — beyond Stage 3, not yet implemented. system + manager+

The minimal "happy path": owner creates the project and provisions a manager → manager: New credentialAdd instanceAllowlist ON → Bindings → operator: Run. If the project has one instance per type — the Bindings step is automated.

Flow 2 — Connect Gmail in ~90 seconds (manager+)

  1. Console → Credentials → New credential.
  2. Category "Email" → "Gmail (OAuth 2.0)".
  3. Display name: Sales Inbox.
  4. Connect with Google → Google consent screen → allow → callback to app-api → the code goes to the worker, gets exchanged for tokens, the tokens are encrypted (envelope) → an app.connector_credentials row (cred_<ulid>, status=active).
  5. Auto test_credential (worker-side probe) → last_test_result=success → ✅.
  6. Done. The credential is available for creating Gmail connector instances in this project.

If the CredentialType has requires_approval=true — after step 4 a credential_proposal is created; the credential appears only after approve by a reviewer/manager+ (needed for production CRM, payment providers).

Flow 3 — Create a connector instance (Add instance, manager+)

  1. Console → Connectors → Add instance.
  2. Adapter — pick the concrete adapter (gmail).
  3. Credential — pick the project's credential (Sales Inbox). Validated: same project, usable (status='active', not soft-deleted, not purged, encrypted payload/DEK present — the D42 condition); adapter_key is supported by this CredentialType.
  4. Display name (Sales Inbox Gmail); a connector_key is formed (e.g. gmail_sales) — unique within the project.
  5. Config — non-secret parameters only: label_ids (Gmail), allowed_chat_ids (Telegram), polling_interval_seconds, webhook_url. No secrets herewebhook_secret, API keys, HMAC secrets live in the encrypted credential payload; for webhook verification the system separately projects connector_ingress_verifiers.
  6. Save → the create_connector_instance command → an app.connector_instances row + outbox: connector_instance.created, optionally …ingress_verifier_projection_requested (the worker projects the signature verifier), optionally …webhook_registration_requested (the worker calls setWebhook on the provider — for Telegram-like ones). Provider APIs are not called inline.

Flow 4 — One connector + two different credentials in one workflow

Scenario: a workflow sends mail both from the admin mailbox and from the support mailbox.

  1. The engineer declares two named requirements of the same type in the WorkflowDefinition: ConnectorRequirement(name="admin_mailbox", connector_type="email") and ConnectorRequirement(name="support_mailbox", connector_type="email").
  2. The workflow steps reference the right slot (connector_requirement / connector_instance_id in step.config).
  3. manager+: create two credentials (Admin Inbox, Support Inbox) and two instances (gmail_admin, gmail_support — both adapter_key=gmail, both connector_type=email, different connector_key, each with its own credential_id).
  4. manager+: in the workflow's Bindings, map admin_mailbox → gmail_admin, support_mailbox → gmail_support. (You can even use different adapters: admin_mailbox → Gmail, support_mailbox → IMAP, as long as both satisfy the email type and publish the needed actions — D45.)
  5. At run time each step resolves its own instance → its own credential.

Constraint: you cannot bind two different credentials to the same slot in one run — a slot → exactly one connector_instance → exactly one credential (Mode XOR, D27). Need two mailboxes → use two slots.

Flow 5 — Reauthorize an expired OAuth (manager / admin)

  1. On a refresh failure the worker sends a deeplink alert to the owner/admin via Telegram.
  2. Console → Credentials → find the credential in error/distressed state → Reauthorize.
  3. A fresh Google consent → callback → the complete_oauth_credential command (reauthorize mode; worker_apply_then_signal, permission credential:rotate) → new payload (new payload_version) → dependent ingress verifiers and caches are invalidated automatically → status=active, refresh_failure_count=0.

Flow 6 — Temporarily disable a connector (manager+)

Console → Connectors → the allowlist block → toggle Allowlist OFF for the needed connector_key. Effect: inbound webhooks on this key are rejected (fail-closed), the connector is unavailable for new bindings. Credentials and instances stay untouched — toggle it back ON and everything works again.


6. Options reference

6.1 Credential (app.connector_credentials)

Field What it does Default / constraints
display_name A human-readable name required; unique within (project, credential_type_key) among non-deleted rows
credential_type_key Which CredentialType (gmail_oauth2, …) required; must exist in CredentialTypeRegistry
encrypted_data / payload_nonce / payload_auth_tag / encrypted_dek / dek_nonce / dek_auth_tag / kek_version The encrypted secret (envelope: payload under DEK, DEK under KEK, AES-256-GCM) filled only in the worker; not visible in the Console; NOT NULL before purge, NULL after purge (tombstone)
status active / expired / revoked / error changed only by commands; RAW UPDATE is forbidden
last_test_at / last_test_result / last_test_error The test_credential result last_test_error ≤ 500 chars, no token leak
last_used_at Last use updated only by the coalesced record_credential_usage command (no more often than ~1/60s)
last_refresh_at / refresh_failure_count / expires_at OAuth refresh state refresh_failure_count resets on success; > 0 = distressed
deleted_at / deletion_reason / purge_after / purged_at Soft-delete + GDPR (schema ready, commands partial) revoke_credential sets deleted_at (soft tombstone); deletion_reason ∈ {user_request, project_archived, gdpr_erasure, incident}; delete_credential-with-deletion_reason and the hard-purge sweeper on purge_after (purged_at zeroes payload/DEK) — beyond Stage 3, not yet implemented
version Optimistic concurrency all mutating commands require expected_version

6.2 Connector instance (app.connector_instances)

Field What it does Default / constraints
connector_type A logical capability (email/crm/storage) — matches ConnectorRequirement.connector_type required; email ≠ gmail
adapter_key The concrete adapter factory (gmail/telegram/bitrix24) required; must be supported by the credential's CredentialType
connector_key The runtime registry key (gmail_sales) unique within the project among non-deleted rows
credential_id Which credential to use NOT NULL; composite FK (project_id, credential_id)
display_name The name in the UI required
config (JSONB) Non-secret parameters only polling_interval_seconds, webhook_url, label_ids (Gmail), allowed_chat_ids (Telegram), … Secrets here are forbidden (the handler runs display_name/config through credential-scan guards)
status active / paused / error changed via update_connector_instance (connector:manage). credential_id/connector_type/adapter_key/connector_key — immutable after creation
health_status healthy / degraded / down / unknown written by the system via the internal update_connector_health command
deleted_at / deletion_reason Soft-delete/tombstone (D41) deletion_reason ∈ {user_request, project_archived, migration}; delete_connector_instance is blocked if referenced by a binding profile or an active/pending run snapshot; no restore command

When an instance is considered "usable" (D42): not tombstoned (deleted_at IS NULL) + status='active' + the backing credential is in the same project + credential.status='active' + not soft-deleted + not purged + encrypted payload/DEK present. Any validator that accepts a connector_instance_id checks this composite condition, not just ci.status.

6.3 Project connector allowlist (app.project_connector_allowlist)

Field What it does
(project_id, connector_key) Natural key — which connector key the toggle applies to
enabled (bool) The SoT flag "the connector is allowed in the project". The DB row beats the legacy YAML enabled_connectors. No row + LEGACY_YAML_CONNECTORS=false → the connector is considered disabled (fail-closed)

Changed by the set_project_connector_enabled command (sync_apply, public, project_connector:manage); read via the read.project_connector_allowlist projection (the Console never reads app.* directly).

Column values are taken from core/api/commands/execution_paths.py (path), core/api/commands/policy.py (side-effect class), _COMMAND_PERMISSION_MAP there (permission). "secret-bearing" is a separate note that the handler decrypts/handles secret material (hence worker_apply_then_signal).

Command Execution path Side-effect class Permission Access Purpose
create_credential worker_apply_then_signal (secret-bearing) internal credential:create manager+ Create a credential; encryption/provider — only in the worker
update_credential dual-path: sync_apply (metadata-only) / worker_apply_then_signal (if the payload has credential_payload_ref — secret-bearing) internal credential:update manager+ Update metadata / payload; connector_key immutable
revoke_credential sync_apply internal credential:revoke manager+ Soft tombstone (sets deleted_at + retention purge_after), encrypted payload retained; dependent CIs become unusable (D42); blocked while an active/paused workflow or an active/pending run on a dependent CI exists
test_credential worker_apply_then_signal (secret-bearing) external_low credential:test operator+ Provider probe (adapter.health_check()) — only in the worker
complete_oauth_credential worker_apply_then_signal (secret-bearing) external_low credential:create (purpose=create) / credential:rotate (purpose=reauthorize) internal-only (dispatched by the OAuth callback; the user's grant is re-checked in the worker) Complete the OAuth callback / reauthorize
refresh_oauth_credential worker_apply_then_signal (secret-bearing) external_low credential:rotate internal-only (system actor allowlist) Auto-refresh the OAuth token
mark_credential_distressed / mark_credential_expired / record_credential_usage sync_apply internal credential:maintain internal-only (system actor allowlist) Lifecycle/telemetry (D40); record_credential_usage coalesced ≈1/60s
create_connector_instance / update_connector_instance / delete_connector_instance sync_apply internal connector:manage manager+ Instance CRUD; update mutates only display_name/config/status; delete = soft-tombstone (blocked if referenced by a binding profile or an active/pending run snapshot); create emits outbox events (does not call the provider inline)
update_connector_health sync_apply — (not in the credential side-effect registry) edit_project internal-only (403 from external) Health snapshot (D40)
set_project_connector_enabled sync_apply internal project_connector:manage manager+ (public) Allowlist toggle
set_workflow_bindings / clear_workflow_bindings / auto_map_workflow_bindings sync_apply internal workflow:configure_bindings manager+ Binding profile
upsert_connector_ingress_verifier worker_apply_then_signal (secret-bearing) internal ingress_verifier:manage internal-only (system actor allowlist) Webhook verifier projection (D54) — decrypts the durable credential payload
disable_connector_ingress_verifier sync_apply internal ingress_verifier:manage internal-only (system actor allowlist) Disable a verifier (D54)
pin_workflow_run_bindings sync_apply internal workflow_run:pin_bindings internal-only (sub-step of start_workflow dispatch) Pins the run's binding snapshot (D36)

Not implemented in the current version (beyond Stage 3 / permission reserved): delete_credential (soft-delete with deletion_reason), purge_credential (hard-purge sweeper, permission credential:purge), a separate rotate_credential, a separate pause_connector_instance (pause is via update_connector_instance). See CONCEPT-CONNECTORS.md §0.1 backlog.


7. Lifecycle and maintenance

Credential status machine: active → (expired | revoked | error). revoked/expired immediately makes all dependent connector instances unusable (D42), even if referenced by explicit ID or a run override. The CI itself is not mutated — pausing an instance (if needed) is done by a separate update_connector_instance command (status='paused').

OAuth refresh (automatic): system only (handler-level actor allowlist; permission credential:rotate), the refresh_oauth_credential command. Success → last_refresh_at, refresh_failure_count=0. Failure → refresh_failure_count++; at a threshold (see credential_refresh_dispatcher) → mark_credential_distressed + a deeplink alert via Telegram → a manual Reauthorize (Flow 5, complete_oauth_credential).

Health monitoring: scheduled probe → update_connector_healthconnector_health_snapshots + connector_instances.health_status (audit is written only on a status transition). Auto-pause of an instance on sustained down — an auto-healing-monitor feature (designed, see CONCEPT-CONNECTORS.md §0.1 backlog); in the current version a human sets the pause.

Taking out of circulation: - a credential — revoke_credential (soft tombstone, status→revoked, deleted_at set, encrypted payload retained for audit/possible re-enable; dependent CIs become unusable per D42). Blocked if the credential is referenced by an active/paused workflow or an active/pending run that uses a dependent instance — finish/cancel those first; - a connector instance — delete_connector_instance (soft/tombstone, D41); the row stays for audit/replay; blocked if referenced by a binding profile or an active/pending run snapshot; the D54 verifier is disabled before deletion; the UI offers find-and-replace; - a full delete_credential (with deletion_reason, cascade-disable of D54 verifiers) and a hard-purge sweeper on purge_after (purged_at zeroes payload/DEK, the tombstone row stays) — beyond Stage 3, not yet implemented (DDL columns already exist).

Encryption-key rotation: versioned (kek_version); re-encrypt — a maintenance CLI (worker-side), the runbook is operator-driven. (Full rotation orchestration — beyond-scope, see CONCEPT-CONNECTORS.md §0.1 / §26.)


8. Troubleshooting

Symptom Cause What to do
In the allowlist block a connector shows No active credential No credential is directly associated with the connector_key in the allowlist table — the binding lives in a connector instance This is fine if a connector instance with the needed credential_id exists. Check the "Connector instances" block.
An inbound webhook returns 401 (generic) The connector is disabled in the allowlist / no ingress verifier / the verifier is stale (credential_secret_versionpayload_version) / the signature didn't match Set Allowlist ON; check the credential is active and not distressed; Reauthorize if needed (that re-projects the verifier via outbox). The specific cause is in the audit (it does not leak into the response body).
Add instance rejects the chosen credential The credential is from another project / not usable (expired/revoked/soft-deleted/purged) / adapter_key is not supported by this CredentialType Use an active credential from the same project; check adapter_key is in the type's supported_adapter_keys.
A workflow won't start — "binding profile incomplete" Not all ConnectorRequirement slots are mapped to usable instances Console → Workflows → <definition> → Bindings → "Auto-map" or pick an instance per slot manually; make sure instances are usable (D42) and the connectors are Allowlist ON.
Revoke of a credential fails ("credential_in_use") It is referenced by an active/paused workflow or an active/pending run that uses a dependent instance Finish/cancel those workflows/runs, then Revoke. Idle instances do not block revoke — they just become unusable per D42. (A full "delete"/GDPR purge is not yet implemented — use Revoke to take a credential out of circulation.)
Can't delete a connector instance It is referenced by a binding profile or an active/pending run snapshot Re-bind the slot in the binding profile to another instance (the UI offers find-and-replace); wait for active runs to finish.
A credential is in error state, OAuth isn't refreshing refresh_failure_count crossed the threshold (distressed) Reauthorize (a fresh consent). See Flow 5. A deeplink alert should arrive via Telegram.
A send_email step fails "action not available" The instance under the slot doesn't publish this action (the action manifest, D45) Map the slot to an adapter instance that publishes send_email (visible in the Actions column).

9. Constraints and invariants

  • App-api never sees the plaintext secret (I-1). The durable KEK AXON_CREDENTIAL_ENCRYPTION_KEY is mounted only in app-worker / the maintenance CLI; in app-api this variable is forbidden. Secret-bearing credential commands go via worker_apply_then_signalhandler.apply() (decryption/provider) runs only in the worker. App-api gets only a transient secret-ref key (AXON_TRANSIENT_SECRET_REF_KEY) and the public OAuth descriptor.
  • The OAuth client_secret is also worker-side (I-20, Strategy A). It is not in the Console or in logs.
  • The webhook ingress verification key is a separate root (AXON_INGRESS_VERIFICATION_KEY), mounted in app-api + app-worker, used only for the connector_ingress_verifiers projection (HKDF SHA-256 subkeys per RFC 5869). It does not intersect with the durable KEK and does not decrypt the credential payload (invariant I-DOMAIN-CONNECTORS-STAGE2-INGRESS-KEY).
  • Secrets are never in connector_instance.config or in step.config. Only in the encrypted credential payload / a credential proposal.
  • Project-scope hard isolation. A credential can't be shared across projects. Need one Gmail in two projects → two credential rows. The composite FK (project_id, credential_id) prevents cross-project leaks at the DB level.
  • Mode XOR (D27). In a connector-backed step.config, exactly one of connector_requirement / connector_instance_id / connector_key. More than one → publish-time 422 + runtime ValueError.
  • Fail-closed. Connector disabled in the allowlist / verifier missing / verifier stale / signature mismatch → the webhook is rejected, no inbound_events row is created, generic 401, the reason is only in the audit.
  • maintain / purge / ingress_verifier:manage — system-only (I-9). Auto-healing and hard-purge are system behaviours by design; these permissions are not granted to humans (HUMAN ∩ SYSTEM_ONLY = ∅).
  • All mutations go via CommandEnvelope. RAW UPDATE on app.connector_* is forbidden; operator YAML edits of connectors (legacy file-mount) bypass the invariant and are scheduled for removal (the V6.4 GA window).
  • Ambiguous ingress binding → reject, not "a random project". If one inbox/bot is bound to several projects and the hints don't resolve it — the event is written with project_id=NULL, resolution_status='ambiguous'; an operator resolves it manually. Cross-project data leakage is forbidden.
  • Connector-as-Workflow (conceptually "everything is a workflow") is not done in V6.2 — ConnectorInstance stays a separate entity; see CONCEPT-CONNECTORS.md §26.

  • Roles-And-Permissions.md — the full role/permission matrix.
  • Workflows.mdConnectorRequirement[], binding profiles, Run Wizard, overrides.
  • Security-And-Audit.md — handling secrets, audit log, explainability.
  • First-Project-Walkthrough.md — the new-project end-to-end scenario (includes Flow 1 of this manual).
  • Canon: ARCHITECTURE-V6.md §11 (Connectors and Tools); CONCEPT-CONNECTORS.md §1-§5/§10/§22 (long-form architecture); CONCEPT-CONNECTORS-3-STAGES.md (stages); CONCEPT-CONNECTORS-SECOND.md (Tier 2+, coverage expansion); OWNERSHIP-MAP.md (zones 1/4/5); core/models/auth.py (RBAC).