Give an Agent Custom Tools

View as Markdown

Agents that can only browse stop at the edge of the page. A webhook tool lets the agent call your API mid-task - create the order it just priced, look up a customer record, post a result - and every call is recorded for audit.

1import { Bctrl } from "@bctrl/sdk";
2
3const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });
4
5// 1. A tool that calls your backend.
6const tool = await bctrl.tools.create({
7 type: "webhook",
8 name: "create-order",
9 url: "https://api.acme.com/orders",
10 timeoutMs: 10_000,
11});
12
13// Try it before an agent does.
14const test = await bctrl.tools.test(tool.id, {
15 input: { sku: "WIDGET-1", quantity: 2 },
16});
17
18// 2. Bundle it with the builtins the task needs.
19const toolset = await bctrl.toolsets.create({
20 name: "ordering",
21 builtins: ["captcha", "vault"],
22 toolIds: [tool.id],
23});
24
25// 3. The agent browses AND acts on your system.
26const runtime = await bctrl.runtimes.create({ type: "browser", name: "ordering-agent" });
27const { runId } = await bctrl.runtimes.start(runtime.id);
28
29await bctrl.runtimes.invocations.createAndWait(
30 runtime.id,
31 {
32 action: "browserUse",
33 instruction:
34 "Find the current price for WIDGET-1 on supplier.com. If it's under $20, create an order for 2 units using the create-order tool.",
35 toolsetId: toolset.id,
36 },
37 { timeoutMs: 300_000 }
38);
39
40await bctrl.runtimes.stop(runtime.id);
41
42// 4. Audit: every tool call the agent made, with inputs and outputs.
43const { data: calls } = await bctrl.toolCalls.list({ runId });
44for (const call of calls) {
45 console.log(call.id, call.status);
46}

The tool call log is the part to lean on in production: when an agent does something surprising, toolCalls.list({ runId }) plus the recording reconstructs exactly what it saw and what it did about it.

Hosted tools

No endpoint to call? Ship the code to BCTRL instead and it runs server-side:

1const tool = await bctrl.tools.create({
2 type: "hosted",
3 name: "summarize",
4 source: "export default async (input) => ({ summary: input.text.slice(0, 280) })",
5});

Next