6. Tool Calling and Reliable Workflow Execution
Let models request tools while your application validates arguments, enforces permissions, handles retries, and records side effects.
How Tool Calling Works
In tool calling, the model proposes a structured call to a named function; your application validates the arguments and decides whether to execute it. The model does not automatically run your JavaScript function unless your code implements the execution loop. Tool results are returned to the model, which may then answer or request another tool.
A safe loop has bounded steps, allowed-tool lists, schema validation, authorization, timeouts, idempotency, and clear handling of malformed or repeated calls. Never expose a general-purpose shell, arbitrary SQL, or unrestricted HTTP fetch tool to an untrusted agent without strong isolation and policy controls.
const toolRegistry = {
getOrderStatus: async ({ orderId }, context) => {
const order = await orders.findVisibleToUser({
orderId,
userId: context.userId
});
if (!order) return { found: false };
return { found: true, status: order.status };
}
};- Tool calls are requests, not permission grants.
- Validate arguments and user access at execution time.
- Limit the number of tool-loop steps and apply timeouts.
Who should execute and authorize a tool call proposed by the model?
Tool Schemas, Validation, and Idempotency
Use narrow tool schemas with enums, length limits, required fields, and explicit descriptions. Validate again in application code, including business constraints that schemas cannot express. For side-effecting operations, add idempotency keys or a deduplication record so retries do not create duplicate payments, tickets, or emails.
Separate read tools from write tools. A write tool should have explicit authorization and often require user confirmation. Record who requested the action, what arguments were accepted, what policy was applied, and the result. Avoid storing unnecessary sensitive arguments in audit logs.
async function createSupportTicket(input, context) {
const parsed = TicketInput.safeParse(input);
if (!parsed.success) throw new Error("Invalid ticket arguments");
return db.withIdempotencyKey(
context.idempotencyKey,
() => tickets.create({
userId: context.userId,
subject: parsed.data.subject,
body: parsed.data.body
})
);
}TicketInput and withIdempotencyKey represent application-owned validation and deduplication helpers. Implement them using a trusted schema validator and a durable unique key in the database.- Use narrow schemas and validate again in the executor.
- Make side effects idempotent where possible.
- Audit sensitive actions and require confirmation where appropriate.
Why use an idempotency key for a side-effecting tool?
Human Approval and Multi-Step Workflows
For consequential actions, let the model prepare a proposal and ask a human to approve it before execution. The approval should bind to the exact action payload, target, and relevant version; otherwise an attacker or race could change the payload after approval. Re-check permissions and state at execution time.
A workflow state machine is often more reliable than an unconstrained agent: drafted → awaiting_approval → approved → executing → completed with explicit failure and cancellation states. Store state durably so a process restart does not lose the workflow. Define who can approve, how approvals expire, and how rejected requests are handled.
drafted
-> awaiting_approval
-> rejected
-> approved
-> executing
-> completed
-> failed / retryable- Use state machines for durable multi-step workflows.
- Approval must bind to the exact action.
- Re-check permissions and current state before execution.
What should an approval bind to for a high-impact action?