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
- Agent decides to call your tool
- 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
- Your server processes the request
- Your server returns JSON
- 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.
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"
}'
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:
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.
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
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"]
}
}'
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.
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):
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.
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}/.
Step 4: Build Your Webhook Handler
When an agent calls your tool, Bedrock sends a POST request to the tool’s url.
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:
{
"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.
Return JSON. The entire response body is serialized to a string and shown to the agent as the tool result.
{
"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:
{
"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
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
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);
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:
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"
}'
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.
Always verify X-Agent-Secret in your webhook handlers. Without this, anyone
who discovers your webhook URL could call it.
Tools are called during an agent’s runtime loop. Here’s the lifecycle:
- Agent wakes up (via API call, schedule, or incoming message)
- Runtime loads tools from every adapter attached to the agent (seeded from its template at creation time)
- LLM prompt is built with the tool descriptions and parameters
- LLM decides whether to call a tool based on the conversation and available tools
- Bedrock executes the tool call (webhook POST for external tools)
- Result is returned to the LLM as a tool response
- 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.
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:
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:
{
"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.
A single adapter can have many tools. Group related functionality together:
# 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:
@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
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
- 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