Connectors & HITL

A connector is how your agent talks to an outside service (Salesforce, GitHub, your own internal API) without ever holding that service's credentials itself. The plane holds the real secret, mints a short-lived session for one specific run, and can pause a call for a human to approve first. This page is the full setup: configure one, control which of its tools an agent can use, and turn on human approval.

Why route through a connector instead of just calling the API directly?

Nothing stops your agent code from calling an external API directly with its own hardcoded key — that's not something the plane can prevent. The reason to go through a connector instead: the real credential never has to live in your agent's process or environment variables, every call is automatically tied to the run that made it (so "who called Salesforce and why" always has an answer), and you get tool-level allow/deny lists and human-approval gates for free, enforced by the plane instead of by code inside every agent.

Step 1 — configure the connector

A connector is a small YAML file, referenced by name from langgraph.json. Here's a complete, working example for a Salesforce connector using OAuth client-credentials:

# connectors/salesforce.yaml
auth:
  type: oauth2_client_credentials
  token_url: https://login.salesforce.com/services/oauth2/token
  client_id: ${SF_CLIENT_ID}
  client_secret: ${SF_CLIENT_SECRET}
mcp:
  url: https://salesforce-mcp.internal/sse
tools:
  allow: [soqlQuery, getObjectSchema]
  deny: [deleteRecord, bulkUpdate]
errors:
  INSUFFICIENT_ACCESS: "You don't have permission for this Salesforce object."

Then point langgraph.json at it:

{
  "connectors": {
    "salesforce": { "config_ref": "./connectors/salesforce.yaml" }
  }
}

What each part means:

Step 2 — a network hiccup shouldn't take down every agent

Every OAuth connector automatically gets a circuit breaker around its token-fetch call — you don't configure this to turn it on, only to tune it:

circuit_breaker:
  failure_threshold: 5    # consecutive failures before it trips open (default: 5)
  cooldown_seconds: 30    # how long it stays open before trying again (default: 30)

In plain terms: if Salesforce's login endpoint is down and 5 calls in a row fail, the breaker "opens" — every further call fails instantly (no network attempt, no waiting for a timeout) for 30 seconds, so one broken downstream service can't slow down every agent that happens to use it. After the cooldown, it lets one call through as a test; if that succeeds, it closes and traffic flows normally again. You can watch this state live in Admin → Connectors.

Step 3 — call it from your agent code

Use the SDK helper, not a raw HTTP call — it automatically attaches the headers that tie the request to your currently-running run (explained below):

# Python (inside a graph node)
from runkite_runner.connectors import get_connector_session

async def my_node(state, config):
    sess = await get_connector_session(config, "salesforce")
    # sess.credentials["access_token"] is ready to use right now
// TypeScript
import { getConnectorSession } from "runkite-runner";

const sess = await getConnectorSession(config, "salesforce");
Admin → Connectors — configured connectors and their health
Runkite Admin Connectors

What "run-bound" actually means (and why you'll see a 401 if you skip it)

A connector session is only ever minted for a run that's actually in progress right now — the plane will not hand out credentials to a bare request that just has a runner token and nothing else. This is why the SDK helpers above matter: they automatically read the current run's ID and "generation" (a fencing number that changes if the run gets reassigned to a different worker) from the graph config and attach them as headers. If you ever bypass the helper and call the session endpoint directly without those headers, you'll get 401 run_binding_required — that's the plane refusing to hand out a credential with no run to attach it to, not a bug.

Step 4 — decide who's allowed to use this connector at all

Configuring a connector doesn't automatically let every agent use it — by default, a connector call is denied until you explicitly grant it. You can grant in config (fixed at deploy time) or in Admin (changeable live, no redeploy):

# langgraph.json — static grant, baked in at deploy time
"policy": {
  "grants": [
    { "tenant_id": "default", "agent_id": "sales-assistant", "connector": "salesforce" }
  ]
}

Or open Admin → Grants, click "New grant," and pick the tenant / agent / connector. Admin grants roll out to every control-plane replica within about 15 seconds — no restart needed. Use config for grants you always want; use Admin for grants you're experimenting with or need to change without a deploy.

Step 4b (optional) — gate on the argument, not just the tool name

Grants are name-only. To pending or deny from a field inside tools/call (for example amount > 100), add policy.predicates in langgraph.json and restart the control plane. This is config-only — there is no Admin form for it. It applies only to connector MCP, before the downstream call. Under the threshold the tool still runs; over it, Admin → Pending, and the amount is on Admin → Audit (hover Args). Full example: Grants & HITL.

# langgraph.json — after the grant above
"predicates": [
  {
    "id": "update-over-100",
    "tenant_id": "default",
    "agent_id": "sales-assistant",
    "connector": "salesforce",
    "tool": "updateRecord",
    "when": { "path": "amount", "op": "gt", "value": 100 },
    "effect": "pending",
    "reason_code": "predicate_amount"
  }
]

Step 5 (optional) — force a human to approve first

For anything you never want an agent doing fully on its own — sending an email, approving a refund, deleting a record — add a mandatory human-in-the-loop (HITL) rule. Once this is in place, a matching call doesn't fail and doesn't succeed — it pauses until a human looks at it.

# langgraph.json — every call to salesforce's deleteRecord tool must be approved
"policy": {
  "mandatory_hitl": [
    { "tenant_id": "default", "connector": "salesforce", "tools": ["deleteRecord"] }
  ]
}

Leave out tools to require approval for every tool on that connector. You can also create these rules from Admin → Mandatory HITL without a redeploy.

What actually happens when this fires, end to end:

  1. Your agent tries to call the gated tool (e.g. deleteRecord).
  2. Instead of running, the call is refused with JSON-RPC pending and shows up in Admin → Pending (connector, tool, reason, and the triggering display map on the Args column).
  3. A human clicks Approve or Deny in Admin (or your own tooling can call the same API).
  4. Approved — the plane mints a one-shot permission for the next matching call with that same argument digest. A retry with a different amount does not consume it. Denied — the run resumes with a denial instead, and your agent code decides what to do (retry differently, apologize to the user, escalate).
  5. This is a one-time unlock, not a standing exception — the very next time the agent tries the same tool, it pauses again. If you want it to stop pausing permanently, remove the mandatory HITL rule instead.
Admin → Pending — see exactly what the agent wants to do before approving it
Runkite Admin Pending Actions

What to expect

Reference: docs/connectors.md · docs/trust-governance.md · Secrets (env / file / Vault) · Admin UI guide · Try HITL path