StudyVisuallyStart studying
AI agents

Tool calling in JavaScript, explained with one example

By the StudyVisually teamPublished

Tool calling (also called function calling) lets a language model ask your code to run a function. You describe the function to the model with a name, a description and a JSON schema. When the model decides it needs the function, it replies with the function's name and arguments instead of text. Your code runs it, sends the result back, and calls the model a second time to get the final answer. The model never runs anything itself.

What does a tool look like?

A tool is a normal JavaScript function plus a description the model can read. The description has three parts: a name, a sentence about when to use it, and a JSON schema for its arguments.

TypeScript
const tools = [
{
type: "function",
function: {
name: "getOrder",
description: "Get an order's status by order ID. Use when the user asks where an order is.",
parameters: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
},
},
];

The description matters more than it looks. It is the model's only clue about when to use the tool. "Gets order data" leads to wrong choices. "Use when the user asks where an order is" doesn't. Rewriting a vague description is often the whole fix when an agent picks the wrong tool (tool description lesson).

What does the model send back?

Send the tools along with the conversation. If the model wants a tool, the reply has no text. It has a tool_calls list instead.

TypeScript
const response = await client.chat.completions.create({
model,
messages: [{ role: "user", content: "Where is order A12?" }],
tools,
});
const message = response.choices[0].message;
// message.content is null
// message.tool_calls[0].function.name is "getOrder"
// message.tool_calls[0].function.arguments is '{"id":"A12"}'

Notice that arguments is a string of JSON, not an object. You have to parse it, and it can be malformed or simply wrong (arguments lesson).

How do I run the tool safely?

Parse the arguments, then check them before you run anything. The model can send a value that is out of range, or one that someone smuggled into the conversation on purpose. Zod makes the check one line.

TypeScript
import { z } from "zod";
const GetOrderArguments = z.object({ id: z.string().min(1).max(20) });
const call = message.tool_calls[0];
const parsed = GetOrderArguments.safeParse(JSON.parse(call.function.arguments));
const result = parsed.success ? await getOrder(parsed.data.id) : { error: "Invalid order ID" };

Sending an error back to the model is usually better than throwing. The model reads it and can try again with better arguments. Treat every tool argument like req.body: untrusted until checked (validation lesson).

How do I send the result back?

Add two messages to the conversation, in this order:

  1. The model's own message with the tool_calls, unchanged.
  2. A message with role: "tool", the tool_call_id from the call, and the result as a string.
TypeScript
messages.push(message);
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });

The tool_call_id links the answer to the request, like a reply in the right email thread (tool message lesson).

Why does it take two model calls?

The first call decides which tool to use. The second call, with the result now in the messages, writes the answer for the user.

TypeScript
const followUp = await client.chat.completions.create({ model, messages, tools });
console.log(followUp.choices[0].message.content);
// "Order A12 shipped yesterday and should arrive on Friday."

Keep passing tools on the second call. The model might need a second tool before it can answer, and that is how a single round trip grows into an agent. Put this in a loop and you have the agent loop.

Can the model call several tools at once?

Yes. One reply can hold several entries in tool_calls. Run them together with Promise.all, then add one tool message per call. OpenAI's guide says the model "may choose to call multiple functions in a single turn", and you can turn that off with parallel_tool_calls: false (function calling guide).

You can also steer tool use with tool_choice. The default "auto" lets the model decide, "none" forbids tools, "required" forces one, and naming a function forces that function.

Chat Completions or Responses API?

Everything above uses Chat Completions, which OpenAI says "remains supported". For new projects OpenAI recommends the Responses API, and some of its newest models need Responses for tool calling (migration guide). The steps are the same, with different names:

StepChat CompletionsResponses API
Tool definitionname and parameters nested under functionname and parameters at the top level
Model asks for a toolmessage.tool_calls[]a function_call output item
Link between call and resulttool_call_idcall_id
You send the resulta message with role: "tool"a function_call_output input item

If you understand one, you understand both.

Practise it

The tool calling part breaks this into ten cards, from the schema to parallel calls. In the app, Quiz shows you real tool-calling code and asks what it returns before you see the answer. That is the fastest way to find out whether you really know where arguments stops being a string.

Read it? Now test yourself on it.

Sources

Try it with today's mission.

Open the app and spend 20 minutes on a free subject or your own notes. Start without signing up, no card needed.

Start studying

See you tomorrow?