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/product 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-product 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 product adds this adapter, the portal will prompt for these config values. The config is validated against the schema.
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: Assign the Adapter to Your Product
Add the adapter to your product so agents can use it. You can do this in the portal (Adapters tab on the product page) or via the API:
curl -X PATCH https://api.bedrock.orinlabs.org/api/products/products/PRODUCT_ID/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"adapters": ["EXISTING_ADAPTER_ID_1", "YOUR_NEW_ADAPTER_ID"]
}'
The adapters field is a full replacement — include all adapter IDs you want
assigned, not just the new one.
If your adapter has a config_schema, you also need to create a config:
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",
"product": "YOUR_PRODUCT_ID",
"config": {
"api_key": "crm-api-key-here",
"instance_url": "https://yourcompany.crm.com"
}
}'
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 | Your product’s 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"
},
"product": {
"id": "product-uuid",
"name": "Sales Assistant"
},
"agent": {
"id": "agent-uuid",
"name": "Alice's Sales Agent",
"model": "claude-sonnet-4",
"timezone": "America/New_York",
"status": "running"
},
"arguments": {
"query": "Acme Corp",
"status": "qualified",
"limit": 5
}
}
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-product-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-product-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 product. It’s sent with every webhook call so your server can verify the request came from Bedrock.
Set it in the portal (Product Settings > Tool Call Secret) or via the API:
curl -X PATCH https://api.bedrock.orinlabs.org/api/products/products/PRODUCT_ID/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"tool_call_secret": "a-strong-random-secret"
}'
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 all adapters assigned to the agent (or its product)
- 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