What Actually Happens When an AI Model Uses Tools
Learn the mechanics of tool use in AI models with a hands-on agent loop, tool schemas, and permission config.
The problem: your AI says it can call tools, but nothing happens
You ask an AI assistant to look up a user by email, but it responds with a guess instead of actually querying your database. Or you build a chatbot that claims it can check the weather, yet it just makes up a forecast. The gap between demo and production is the tool-use loop: the model needs to decide to call a tool, format the call correctly, receive the result, and then continue. Most tutorials stop at showing a single API call, but real tool use requires a loop.
In this article I will walk through a minimal agent loop that actually works. You will see the exact JSON the model sends, how the runtime executes the tool, and how the result is fed back. By the end you will have a working Node.js script that uses the OpenAI API to call a custom tool, plus a permission model that keeps it safe.
- Understand the three phases: intent, execution, and continuation.
- See a real tool schema and a real function call response.
- Build a minimal agent loop with OpenAI's function calling.
- Add a permission layer so the model cannot do harm.
Before you start: what you need
This tutorial assumes you have Node.js 18 or later installed and an OpenAI API key. We will use the official openai npm package. The code is intentionally minimal, no frameworks, so you can see every moving part.
Create a new directory and install the dependency.
mkdir ai-tool-loop
cd ai-tool-loop
npm init -y
npm install openai@4
- Keep your API key in an environment variable, never in code.
- The OpenAI Node SDK handles the HTTP details, but you still need to manage the loop yourself.
Step 1: Define a tool the model can call
The first step is to tell the model what tools are available. This is done with a JSON schema. The schema describes the function name, its parameters, and what each parameter means. The model reads this schema and decides when to call the function.
We will make a simple tool that returns the current time in a given timezone. It is a realistic example because it requires a parameter and returns a value that the model cannot guess.
const tools = [
{
type: "function",
function: {
name: "get_current_time",
description: "Get the current time in a specified timezone",
parameters: {
type: "object",
properties: {
timezone: {
type: "string",
description: "IANA timezone name, e.g. America/New_York"
}
},
required: ["timezone"]
}
}
}
];
- The schema must be precise. If the model sends a parameter that is not in the schema, the API call will fail.
- Describe parameters clearly so the model knows what to pass.
Step 2: Implement the tool function
Now write the actual JavaScript function that will be called. This function must match the schema exactly: it takes an object with a timezone property and returns a string. In a real application, this could be a database query, an API call, or any side effect.
function getCurrentTime({ timezone }) {
try {
const now = new Date();
const formatted = new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
dateStyle: "full",
timeStyle: "long"
}).format(now);
return `The current time in ${timezone} is ${formatted}.`;
} catch (error) {
return `Error: invalid timezone ${timezone}`;
}
}
Step 3: Build the agent loop
The core of tool use is the loop. You send the user message plus the tool schemas to the model. The model either responds with a normal message or with a tool call. If it is a tool call, you execute the function, append the result as a new message with role 'tool', and send the whole conversation back to the model. This continues until the model gives a final answer.
The following script implements this loop. It keeps the conversation history in an array and handles multiple tool calls in one response.
import OpenAI from "openai";
const openai = new OpenAI();
async function runAgent(userMessage) {
const messages = [{ role: "user", content: userMessage }];
for (let i = 0; i < 5; i++) { // safety limit
const response = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages,
tools,
tool_choice: "auto"
});
const message = response.choices[0].message;
if (message.tool_calls) {
messages.push(message);
for (const toolCall of message.tool_calls) {
const result = getCurrentTime(JSON.parse(toolCall.function.arguments));
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result
});
}
} else {
return message.content;
}
}
throw new Error("Agent loop exceeded max iterations");
}
runAgent("What time is it in Tokyo?").then(console.log);
- The loop limit prevents infinite loops if the model keeps calling tools.
- You must preserve the full message history, including tool results.
- The tool result message must include the tool_call_id to match the call.
Step 4: Run it and see what happens
Run the script with your API key set. You should see the model first call get_current_time with timezone 'Asia/Tokyo', then produce a final answer. To see the raw messages, add a console.log before the API call. The output will show the assistant message with tool_calls, then the tool result, then the final answer.
This is the exact flow that happens under the hood when any AI tool is used.
export OPENAI_API_KEY=your-key-here
node agent.js
- If you get an error about invalid timezone, the model might have passed a timezone abbreviation like JST. That is why the schema description matters.
- The model may also ask for clarification instead of calling the tool, depending on the prompt.
Why the loop matters: the model does not execute anything
A common misconception is that the model itself runs the tool. It does not. The model only outputs a structured request. Your code receives that request, executes the function, and sends back the result. The model then reasons over the result. This separation is what makes tool use safe and auditable.
This also means the quality of your tool schemas and the robustness of your execution code directly affect the reliability of the agent. A poorly described parameter leads to bad calls, and a buggy function can break the loop.
- The model is a planner, not an executor.
- Every tool call is a separate API round trip, so latency and cost add up.
- You can log every tool call for debugging or compliance.
Adding permissions: what the model is allowed to do
In production, you do not want a model to call any function without restrictions. Permissions are enforced in your code, not in the model. You decide which tools are available based on the user's role, and you can add runtime checks before executing a tool.
For example, a tool that deletes a database record should require an explicit confirmation step. The following snippet shows a wrapper that checks a permission flag before executing the function.
const allowedTools = {
get_current_time: true,
delete_user: false
};
function executeTool(toolName, args) {
if (!allowedTools[toolName]) {
throw new Error(`Tool ${toolName} is not allowed`);
}
switch (toolName) {
case "get_current_time":
return getCurrentTime(args);
default:
throw new Error(`Unknown tool ${toolName}`);
}
}
- Never expose destructive tools to the model unless absolutely necessary.
- Use a whitelist approach: only list tools that are safe for the current context.
- For sensitive operations, require a human approval step before executing.
Observability: logging the tool calls
When debugging an agent, you need to know what the model requested and what your code did. Add logging around each tool call. The log should include the tool name, arguments, result, and timestamps. This is your audit trail.
Here is a simple logging wrapper.
async function executeToolWithLogging(toolName, args) {
console.log(`[${new Date().toISOString()}] Calling ${toolName} with`, args);
const start = Date.now();
const result = executeTool(toolName, args);
console.log(`[${new Date().toISOString()}] Result (${Date.now() - start}ms):`, result);
return result;
}
- Include a request ID to correlate logs across the loop.
- Log the raw arguments and result, but redact sensitive data.
- Consider using structured logging for easier analysis.
Recommended setup for production
For a production agent, I would not use a single script. I would break it into modules: one for tool definitions, one for tool implementations, one for the agent loop, and one for permissions. I would also add retry logic for transient API errors and a timeout for the whole loop. Here is a minimal project structure and a starter config for environment variables.
mkdir -p src/tools src/agent
# src/tools/time.js
# src/tools/index.js
# src/agent/loop.js
# src/index.js
# .env
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
MAX_LOOP_ITERATIONS=5
ALLOWED_TOOLS=get_current_time
- Use environment variables for configuration, not hardcoded values.
- Set a sensible max loop iterations to prevent runaway costs.
- Test each tool independently before integrating into the agent.
Troubleshooting common issues
If the model never calls a tool, check that the tool schema is valid and that the prompt actually requires the tool. If the model calls the tool with wrong arguments, improve the parameter descriptions. If the loop breaks, verify that you are appending the tool result correctly and that the tool_call_id matches.
Here is a quick checklist.
- Confirm the API key is set and has access to the model.
- Log the raw API response to see the exact tool_calls structure.
- Make sure the tool function returns a string, not an object, unless you stringify it.
- If the model alternates tool calls forever, increase the max iterations or add a stop condition.
- If the tool returns an error, the model may try again or apologize; that is expected.
FAQ
- Q: Does the model actually run the tool? A: No, the model only outputs a structured request. Your code executes the function and returns the result.
- Q: How many tools can I give the model? A: You can give many, but the model has a context limit. Keep the schemas concise and only include relevant tools.
- Q: What if the model calls a tool that does not exist? A: Your code should handle unknown tools gracefully, either by ignoring or returning an error.
- Q: Can I use this with other models? A: Yes, most providers have similar function calling or tool use APIs. The loop pattern is the same.
- Q: How do I prevent the model from calling a tool too many times? A: Set a max iteration count and enforce it in the loop, as shown.
Next step: build your own tool
Now that you understand the loop, take it further. Replace get_current_time with a tool that queries a real database or calls an external API. Add a second tool and see how the model decides between them. The key is to practice with small, safe tools first.
Run the script one more time with a different prompt, like 'What time is it in London and New York?' The model should call the tool twice. That is how you know the loop works.
node agent.js
Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on ai for?
- Working developers who need a practical take on what actually happens when an ai model uses tools — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published September 26, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.