Build an agent with a tool
Give an agent a tool it can call. defineTool handles the result envelope and descriptor defaults;
the registry enforces authorization, validation, idempotency and approval on every call.
1. Define a tool
You write execute(input, context) => data and throw on failure — the shared success/error envelope
is applied for you. The effect classification drives the approval policy.
import { defineTool, toolProvider } from "@forge/agentkit/tools";
const getWeather = defineTool({
name: "get_weather",
description: "Look up the current weather for a city.",
effect: "read", // read → no approval needed
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async ({ city }, ctx) => {
// wrap your existing service; ctx carries the tenant + principal
return weatherService.current(ctx.tenantId, city);
},
});
External writes are approval-gated automatically:
const publishPost = defineTool({
name: "publish_post",
description: "Publish a saved draft to a connected channel.",
effect: "external-write", // → approvalPolicy "always", idempotency required
inputSchema: {
type: "object",
properties: { draftId: { type: "string" }, channel: { type: "string" } },
required: ["draftId", "channel"],
},
execute: ({ draftId, channel }, ctx) => publishingService.publish(ctx.tenantId, draftId, channel),
});
2. Give the tools to the agent
import { createAgent } from "@forge/agentkit/providers";
const agent = createAgent({
manifest: {
id: "social-assistant",
name: "Social assistant",
instructions: "Help the user check the weather and draft posts.",
modelPolicy: { role: "smart" },
},
tools: [toolProvider("demo", [getWeather, publishPost])],
});
const result = await agent.run({ conversationId: "c1", message: "What's the weather in Berlin?" });
console.log(result.text);
The model discovers get_weather, calls it, and the registry runs it through authorization +
validation before returning the result to the model — all within the single run.
Approval-gated tools
publish_post is external-write, so it cannot execute without an approval grant. In the embedded
profile a call to it returns an approval_required result to the model. In the server profile the
run pauses at WaitingForApproval, and you resolve it with the HITL service — the stored input then
executes once, with an idempotency key, so there is never a double publish:
await approvals.decide({ tenantId, interactionId, runId, decision: "allow-once" });
See Approvals & safety and Human-in-the-loop.
What happened
- The tool was only discoverable because the caller was authorized for it.
readran inline;external-writeis gated behind approval.- On approval, the stored input executes once with an idempotency key — no double side effect.