> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bedrock.orinlabs.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Adapters

> Build your own adapters to connect agents to any API or service

# Custom Adapters

Custom adapters let you connect Bedrock agents to your own APIs, databases, or third-party services. When an agent calls one of your tools, Bedrock sends a webhook to your server with the arguments, and your server returns the result.

## How It Works

1. **Agent decides** to call your tool
2. **Bedrock POSTs** to your webhook URL with headers (`X-Agent-Secret`, `X-Agent-Identity`) and a body containing function info, arguments, and agent/template context
3. **Your server** processes the request
4. **Your server returns** JSON
5. **Agent receives** the result and continues

## Step 1: Create an Adapter

An adapter is a named container for related tools. Create one via the API or the portal UI.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.bedrock.orinlabs.org/api/toolbox/adapters/ \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "CRM",
      "description": "Connect to our CRM to manage leads, contacts, and deals"
    }'
  ```

  ```python Python theme={null}
  import requests

  API_KEY = "your-bedrock-api-key"
  BASE_URL = "https://api.bedrock.orinlabs.org"
  headers = {"Authorization": f"Bearer {API_KEY}"}

  adapter = requests.post(
      f"{BASE_URL}/api/toolbox/adapters/",
      headers=headers,
      json={
          "name": "CRM",
          "description": "Connect to our CRM to manage leads, contacts, and deals",
      }
  ).json()

  print(f"Adapter ID: {adapter['id']}")
  ```
</CodeGroup>

The adapter is automatically set to `private` visibility and owned by your organization.

### Optional: Config Schema

If your adapter needs per-template configuration (API keys, instance URLs, etc.), define a `config_schema` using JSON Schema:

```bash theme={null}
curl -X POST https://api.bedrock.orinlabs.org/api/toolbox/adapters/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CRM",
    "description": "Connect to our CRM to manage leads, contacts, and deals",
    "config_schema": {
      "type": "object",
      "properties": {
        "api_key": {
          "type": "string",
          "description": "CRM API key"
        },
        "instance_url": {
          "type": "string",
          "description": "CRM instance URL (e.g. https://yourcompany.crm.com)"
        }
      },
      "required": ["api_key", "instance_url"]
    }
  }'
```

When a template enables this adapter, the portal will prompt for these config values. The config is validated against the schema before the `AdapterConfig` is saved.

## Step 2: Add Tools to the Adapter

Tools are the individual functions agents can call. Each tool needs:

* **name** — Function name the LLM sees (e.g., `search_leads`)
* **description** — What the tool does (included in the LLM prompt, so be thorough)
* **url** — Your webhook endpoint
* **parameters** — JSON Schema defining the arguments

```bash theme={null}
curl -X POST https://api.bedrock.orinlabs.org/api/toolbox/tools/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adapter": "YOUR_ADAPTER_ID",
    "name": "search_leads",
    "description": "Search for leads in the CRM by name, email, or company. Returns matching leads with their status and contact info.",
    "url": "https://api.yourcompany.com/bedrock/search-leads",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "string",
          "description": "Search query (matches name, email, or company)"
        },
        "status": {
          "type": "string",
          "enum": ["new", "contacted", "qualified", "closed"],
          "description": "Filter by lead status"
        },
        "limit": {
          "type": "integer",
          "description": "Max results to return (default 10)"
        }
      },
      "required": ["query"]
    }
  }'
```

<Tip>
  Write detailed, specific descriptions. The description is included in the LLM
  prompt and directly affects how well the agent uses your tool. Include what it
  does, what it returns, and when the agent should use it.
</Tip>

## Step 3: Attach the Adapter to Your Template

Attach the adapter to a template by creating an `AdapterConfig`. This single action enables the adapter **and** stores its per-template configuration (even if the config is empty):

```bash theme={null}
curl -X POST https://api.bedrock.orinlabs.org/api/toolbox/adapter-configs/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adapter": "YOUR_ADAPTER_ID",
    "template": "YOUR_TEMPLATE_ID",
    "config": {
      "api_key": "crm-api-key-here",
      "instance_url": "https://yourcompany.crm.com"
    }
  }'
```

If the adapter has no required config, pass `config: {}`. To remove the adapter from the template later, `DELETE` the `AdapterConfig`.

<Note>
  Existing agents created from the template keep their snapshot; only **new** agents will pick up the attach/detach. Existing agents can be edited directly via `PATCH /api/cloud/agents/{id}/`.
</Note>

## Step 4: Build Your Webhook Handler

When an agent calls your tool, Bedrock sends a POST request to the tool's `url`.

### Request Format

**Headers:**

| Header             | Description                                                                    |
| ------------------ | ------------------------------------------------------------------------------ |
| `X-Agent-Secret`   | The agent's template `tool_call_secret` — verify this to authenticate requests |
| `X-Agent-Identity` | The UUID of the agent making the call                                          |
| `Content-Type`     | `application/json`                                                             |

**Body:**

```json theme={null}
{
  "function": {
    "id": "tool-uuid",
    "name": "search_leads",
    "description": "Search for leads in the CRM...",
    "parameters": {...},
    "url": "https://api.yourcompany.com/bedrock/search-leads"
  },
  "template": {
    "id": "template-uuid",
    "name": "Sales Assistant"
  },
  "agent": {
    "id": "agent-uuid",
    "name": "Alice's Sales Agent",
    "model": "claude-sonnet-4-5",
    "timezone": "America/New_York",
    "status": "running"
  },
  "arguments": {
    "query": "Acme Corp",
    "status": "qualified",
    "limit": 5
  }
}
```

The `template` block is only included when the agent was created from a template. Ad-hoc agents (dev / eval without a template) will have it absent.

### Response Format

Return JSON. The entire response body is serialized to a string and shown to the agent as the tool result.

```json theme={null}
{
  "leads": [
    {
      "id": 42,
      "name": "Jane Smith",
      "company": "Acme Corp",
      "email": "jane@acme.com",
      "status": "qualified",
      "last_contact": "2026-02-10"
    }
  ],
  "total": 1
}
```

Return useful, structured data. The agent reads this text to decide what to do next.

### Error Handling

If something goes wrong, return a clear error message. The agent will see it and can adapt:

```json theme={null}
{
  "error": "No CRM access configured. Ask the user to connect their CRM account."
}
```

If your endpoint returns a non-JSON response or throws an error, the agent sees a generic "Error calling tool" message.

### Example: FastAPI Handler

```python theme={null}
from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()

TOOL_CALL_SECRET = "your-template-tool-call-secret"

@app.post("/bedrock/search-leads")
async def search_leads(
    request: Request,
    x_agent_secret: str = Header(...),
    x_agent_identity: str = Header(...),
):
    if x_agent_secret != TOOL_CALL_SECRET:
        raise HTTPException(status_code=401, detail="Invalid secret")

    body = await request.json()
    args = body["arguments"]
    agent_id = body["agent"]["id"]

    query = args.get("query", "")
    status = args.get("status")
    limit = args.get("limit", 10)

    # Your business logic here
    leads = crm_client.search(query=query, status=status, limit=limit)

    return {
        "leads": [lead.to_dict() for lead in leads],
        "total": len(leads),
    }
```

### Example: Express.js Handler

```javascript theme={null}
const express = require("express");
const app = express();
app.use(express.json());

const TOOL_CALL_SECRET = "your-template-tool-call-secret";

app.post("/bedrock/search-leads", (req, res) => {
  if (req.headers["x-agent-secret"] !== TOOL_CALL_SECRET) {
    return res.status(401).json({ error: "Invalid secret" });
  }

  const { arguments: args, agent } = req.body;
  const { query, status, limit = 10 } = args;

  // Your business logic here
  const leads = searchLeads({ query, status, limit });

  res.json({
    leads,
    total: leads.length,
  });
});

app.listen(3000);
```

## Setting Up the Tool Call Secret

The `tool_call_secret` is set on your **template**. It's sent with every webhook call by agents created from that template, so your server can verify the request came from Bedrock.

Set it in the portal (Template Detail > Tool Call Secret) or via the API:

```bash theme={null}
curl -X PATCH https://api.bedrock.orinlabs.org/api/templates/TEMPLATE_ID/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tool_call_secret": "a-strong-random-secret"
  }'
```

<Note>
  The secret is snapshotted onto each agent when the agent is created, so rotating the template's secret does not affect agents that already exist.
</Note>

<Warning>
  Always verify `X-Agent-Secret` in your webhook handlers. Without this, anyone
  who discovers your webhook URL could call it.
</Warning>

## When Are Tools Called?

Tools are called during an agent's **runtime loop**. Here's the lifecycle:

1. **Agent wakes up** (via API call, schedule, or incoming message)
2. **Runtime loads tools** from every adapter attached to the agent (seeded from its template at creation time)
3. **LLM prompt is built** with the tool descriptions and parameters
4. **LLM decides** whether to call a tool based on the conversation and available tools
5. **Bedrock executes** the tool call (webhook POST for external tools)
6. **Result is returned** to the LLM as a tool response
7. **LLM continues** — it may call more tools, respond, or sleep

An agent can call multiple tools in a single turn, and the loop repeats until the agent decides to sleep or hits `max_turns`.

## Dynamic Tool Descriptions with Detail URLs

If your tool's description needs to change at runtime (e.g., showing current inventory levels or available time slots), use the `detail_url` field.

When an agent run starts, Bedrock GETs the `detail_url` and appends the response to the tool description:

```bash theme={null}
curl -X POST https://api.bedrock.orinlabs.org/api/toolbox/tools/ \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "adapter": "YOUR_ADAPTER_ID",
    "name": "book_appointment",
    "description": "Book an appointment for the customer.",
    "url": "https://api.yourcompany.com/bedrock/book",
    "detail_url": "https://api.yourcompany.com/bedrock/book/detail",
    "parameters": {
      "type": "object",
      "properties": {
        "date": {"type": "string", "description": "ISO date"},
        "time": {"type": "string", "description": "Time slot (e.g., 2:00 PM)"}
      },
      "required": ["date", "time"]
    }
  }'
```

Your `detail_url` endpoint should return:

```json theme={null}
{
  "detail": "Available slots today: 10:00 AM, 2:00 PM, 4:30 PM. Tomorrow: 9:00 AM, 11:00 AM, 3:00 PM."
}
```

The detail URL receives the same `X-Agent-Secret` and `X-Agent-Identity` headers, so you can personalize the response per agent.

## Multiple Tools Per Adapter

A single adapter can have many tools. Group related functionality together:

```python theme={null}
# Create all tools for your CRM adapter
tools = [
    {
        "name": "search_leads",
        "description": "Search for leads by name, email, or company",
        "url": "https://api.yourcompany.com/bedrock/search-leads",
        "parameters": {...},
    },
    {
        "name": "create_lead",
        "description": "Create a new lead in the CRM",
        "url": "https://api.yourcompany.com/bedrock/create-lead",
        "parameters": {...},
    },
    {
        "name": "update_lead_status",
        "description": "Update the status of an existing lead",
        "url": "https://api.yourcompany.com/bedrock/update-lead",
        "parameters": {...},
    },
]

for tool in tools:
    requests.post(
        f"{BASE_URL}/api/toolbox/tools/",
        headers=headers,
        json={"adapter": adapter_id, **tool},
    )
```

You can point all tools at the same server and route by the `function.name` field in the request body:

```python theme={null}
@app.post("/bedrock/crm")
async def crm_handler(request: Request, x_agent_secret: str = Header(...)):
    if x_agent_secret != TOOL_CALL_SECRET:
        raise HTTPException(status_code=401)

    body = await request.json()
    tool_name = body["function"]["name"]
    args = body["arguments"]

    if tool_name == "search_leads":
        return search_leads(args)
    elif tool_name == "create_lead":
        return create_lead(args)
    elif tool_name == "update_lead_status":
        return update_lead_status(args)
    else:
        return {"error": f"Unknown tool: {tool_name}"}
```

## Best Practices

### Tool Descriptions

The description is the most important field — it's what the LLM reads to decide when and how to use your tool.

* **Be specific**: "Search CRM leads by name, email, or company name. Returns matching leads with their status, contact info, and last interaction date."
* **Not vague**: "Search the CRM"

### Return Values

* Return structured JSON that gives the agent actionable information
* Include IDs so the agent can reference items in follow-up tool calls
* For lists, include a count so the agent knows if there are more results
* For errors, return a human-readable message the agent can relay to the user

### Security

* Always verify `X-Agent-Secret` — this is your only authentication layer
* Don't expose internal system details in error messages
* Rate-limit your webhook endpoints
* Use HTTPS for all webhook URLs

### Performance

* Keep webhook response times under 30 seconds
* For long-running operations, return immediately with a status and use notifications to update the agent later
* Cache expensive operations where possible
