Skip to main content

Connectors as Tools

View as Markdown

An agent can call official Connector operations as tools. Each call runs in its own tool Step, with the Connector's credentials and idempotency key.

How it works

ConnectorToolSource offers the operations you name, and the agent takes it as a tool source (tool_sources in Python, toolSources in TypeScript). Each call runs one operation in its own tool Step, so the Call ID, the idempotency key, and credentials work as in a Connector Step. The Call ID stays the same on every attempt.

The Connect a Connector section shows the wiring for each framework; the Built-in agent loop shows it without one. The Connector tools reference covers packages, tool names, result size, and connections.

Which operations are offered

The application names each operation an agent may use, and the Connector's manifest, dex-connector.json, must mark it as exposed for agents. Dex refuses any other operation when the agent is defined.

Dex checks the model's arguments against the operation's input schema first. A wrong or missing field fails the call, before the operation runs, with a message the model can correct.

Effect classes

The manifest decides whether a call can run again, and so which Step runs it:

OperationEffect classRuns in
QueryreadCallTool, with retries
Mutation that the manifest marks retry-safe (agent.retry: safe)idempotent_writeCallTool, with retries
Any other Mutationnon_idempotent_writeCallSingleAttemptTool, once

Retry-safe means a repeat with the same Call ID makes no second effect. A call whose attempts run out records failed for a Query and outcome_unknown for a Mutation.

A session's tool policy for a Connector tool changes only its approval. The effect class always comes from the manifest.

Approval

By default, a Connector tool waits for approval when its manifest recommends it (agent.approval: recommended), as for a refund, and other Mutations run directly. The application can override that for each tool, or pass a predicate that decides per call from its arguments, such as refunds above a limit. A predicate that raises asks for approval.

Outcomes

The operation ends in one of its branches, and the branch decides the tool call's record: succeeded, failed, or outcome_unknown when a write may have taken effect. Outcomes lists every branch and its record.

Reconcile outcome_unknown as for any tool; see Reconcile outcome_unknown.

Write order

Connector writes of one model response run one at a time, in the model's order. Reads, and calls of other tools, run in parallel. A write that needs approval asks for it only when its turn comes.

When a write fails, ends outcome_unknown, or is rejected, the later Connector writes of that response are skipped. Each records failed with skipped set and an error that starts "the tool did not run, so it is safe to call again", and Dex Web shows it as skipped.

Crashes

A Worker crash during a Connector write sends the write at most once. A retry-safe write retries with the same idempotency key, or looks for the earlier write at the provider first; a single-attempt write records outcome_unknown. Crashes shows each case.

Connect a Connector

The example adds the Slack tool slack_post_thread_reply, which replies in a Slack thread.

The Slack tool in the example

With the Slack connection support-slack configured, each example agent gets slack_post_thread_reply, the Slack operation postThreadReply. The Worker opens the connection from DEX_CONNECTOR_CONFIG_FILE locally, or from the DEX_PROJECT_* settings when deployed; Connections covers both, and the Slack tool in the examples covers the Slack app.

postThreadReply is a single-attempt Mutation, so a post runs once, in CallSingleAttemptTool, and a Worker crash never sends it twice. The example also sets requires_approval=True (requiresApproval: true in TypeScript), so every post waits in AwaitToolApproval for a person.

Offer the tool

Install the connectors extra of dex-agents and the Connector package, such as dex-connector-slack. The install commands are in Connector tools.

The Worker opens the Slack connection support-slack from the project configuration. connector_tool turns the operation into a tool, and ConnectorToolSource holds it. Without the connection, the agent gets no Slack tool.

slack_connector_tools.pyPython
def build_slack_tool_sources(connection: slack.Connection | None) -> list[ToolSource]:
"""Returns the Slack tool source, or no source when Slack is not configured."""
if connection is None:
return []
return [
ConnectorToolSource([
# The manifest does not ask for approval; this agent asks before every post.
connector_tool(
connection.post_thread_reply(), connection.reference, requires_approval=True
),
]),
]

# ...

async def open_slack_project_connection(
environment: Mapping[str, str] = os.environ,
) -> SlackProjectConnection | None:
# ...
# An incomplete or invalid configuration raises here, so the Worker fails at startup.
try:
project = await load_from_environment(environment)
except FileNotFoundError:
_log_slack_not_set_up("the connector configuration file does not exist")
return None
# ...
try:
connection = slack.new_project_connection(project, SLACK_CONNECTION_NAME)
except BaseException:
await project.close()
raise
return SlackProjectConnection(project, connection)

Example: examples/python/agent-harnesses/agent_harness_examples/slack_connector_tools.py

The Worker opens the connection once, at startup, and every agent uses it:

__main__.pyPython
async def main() -> None:
config = AgentHarnessExamplesConfig.from_env()
slack_setup = await open_slack_project_connection()
try:
app_state = AgentHarnessExamplesApp(config, slack_setup.connection if slack_setup else None)
# ...
finally:
if slack_setup is not None:
await slack_setup.aclose()

Example: examples/python/agent-harnesses/agent_harness_examples/__main__.py

The agent passes the tool source next to its own tools:

claude_agent_sdk_order_support.pyPython
    return claude_agent_session_flow(
build_options,
[look_up_order, cancel_order],
flow_type=CLAUDE_AGENT_SDK_ORDER_SUPPORT_FLOW_TYPE,
tool_sources=build_slack_tool_sources(slack_connection),
# ...
)

Example: examples/python/agent-harnesses/agent_harness_examples/claude_agent_sdk_order_support.py

How a call runs

Each call of slack_post_thread_reply runs once, in its own CallSingleAttemptTool Step, after a person approves it, as the Slack tool explains. Your framework hands the call to that Step this way:

In the Claude Agent SDK, a Connector tool call never runs inside the CLI, not even a read. Every call ends the segment and runs in its own tool Step, because the Connector needs that Step for its Call ID. The resumed CLI then gets the recorded result. The CLI sees the tool as mcp__tools__slack_post_thread_reply; tool policies, approvals, and records use slack_post_thread_reply.

One call of a model response at a time leaves the turn to run in its tool Step. The adapter refuses a later write of that response inside the turn, so it never runs and leaves no record; the model reads the earlier write's result and then decides whether to call the later write again. Keep Connector tools out of allowed_tools (allowedTools): their calls must leave the CLI, so such an entry fails the RunModel attempt.

Configure the connection

To try it, start the example with DEX_CONNECTOR_CONFIG_FILE set. Then ask the agent to reply in a Slack thread, with the channel ID and the thread timestamp:

Reply in Slack thread C0123456789 1710000000.000100: Order 1002 ships on Thursday.

The post waits for approval, like cancel_order.

Connections explains where the Worker finds the support-slack connection and what the Slack app needs.

Next