Software Guide 2 of 3
What Tool Use Is
Last verified against its sources on .
You have a question a language model cannot answer from what it learned: where your order is, what today's figure is, what your database says. The model has never seen your data, and it cannot run your code. Tool use is how you hand it both. You describe an operation the model may ask for, the model asks, your code runs the operation, and you tell the model what came back. Everything an "agent" does is built from that one exchange.
On OpenAI's platform the mechanism is called function calling. Function calling (also known as tool calling) provides a powerful and flexible way for OpenAI models to interface with external systems and access data outside their training data. The two words are used together on purpose: atool is any operation you let the model request, and a function is a tool you describe with a JSON schema — a short, machine-readable statement of what the operation needs as input. Every tool in this guide is a function.
This guide builds one tool from nothing: a lookup for an imaginary shop's orders. You write one file, run it three times, and read what comes back each time. The outputs on this page are not illustrations; each one is what the file printed when it was run, on the date its stamp shows. By the end you have the loop that every tool-using program is made of, and you know why it has the shape it has.
By the end of this guide you will
- Define one tool, send it with a question, and see the model ask for it instead of guessing — in a file you wrote and ran yourself.
- Execute the model's request in your own code and hand the result back so the model can finish its answer.
- Run the loop that keeps going until the model has nothing more to ask, including a turn where it asks for two lookups at once.
- Recognise the three mistakes that make a tool-using program fail silently, run forever, or ignore its own tool.
Before you start
You need four things. Check each one before you write any code.
- Node.js 22 or newer. Run
node --version. It prints a version such asv24.14.1, which is what the runs on this page used. If it prints a lower number or an error, install Node.js from the official site. - An OpenAI API key. Create one on the API keys page of your OpenAI dashboard and copy it somewhere safe. It never goes into the code.
- A folder with the SDK installed. In a terminal, run
mkdir tool-use-demo, thencd tool-use-demo, thennpm init -y, thennpm install openai@7.15.0. The last command installs theopenaipackage at the version these runs used. - The key in your environment, in the same terminal window you will run the file from. On Windows PowerShell:
$env:OPENAI_API_KEY = "your key here". On macOS or Linux:export OPENAI_API_KEY="your key here". The SDK reads that variable by itself, which is why the code below never mentions the key. If you open a new window, set it again.
One more thing to know before you choose a model. Everything on this page ran on gpt-5.6-luna. GPT-5.6 Luna, model id gpt-5.6-luna, is the GPT-5.6 model OpenAI describes as optimized for cost-sensitive workloads. The steps are the same on any OpenAI model that supports function tools; the outputs you compare yours against are Luna's.
Step 1 — Define a tool and watch the model ask for it
Create a file named tool-use.mjs and put this in it, exactly:
first-request import OpenAI from 'openai';
import { toResponseInputItems } from 'openai/lib/responses/ResponseInputItems';
const client = new OpenAI(); // reads OPENAI_API_KEY from the environment; the key is never in the code
const tools = [
{
type: 'function',
name: 'get_order_status',
description:
'Look up one order in the shop\'s order system by its order number. Returns the order\'s current ' +
'status (packed, shipped or delivered), the carrier once it has shipped, and the expected delivery ' +
'date. Use it whenever the user asks where an order is or when it will arrive: the order system is ' +
'the only place that information exists. Returns an error for an order number that does not exist.',
parameters: {
type: 'object',
properties: {
order_id: { type: 'string', description: 'The order number as the customer sees it, digits only, for example "1042".' },
},
required: ['order_id'],
additionalProperties: false,
},
strict: true,
},
];
const question = 'Where is my order 1042?';
const first = await client.responses.create({
model: 'gpt-5.6-luna',
tools,
input: [{ role: 'user', content: question }],
});
for (const item of first.output) {
if (item.type === 'function_call') console.log(`function_call: ${item.name} ${item.arguments}`);
else if (item.type === 'message') console.log(`message: ${first.output_text}`);
}
console.log(`status: ${first.status}`);
Output
function_call: get_order_status {"order_id":"1042"}
status: completed
Ran node@24.14.1 openai 7.15.0
Run it with node tool-use.mjs. Two lines come back; read them one at a time.
function_call: get_order_status {"order_id":"1042"}— the model did not answer the question. It emitted a function call: the name of your tool and the arguments it wants, as a JSON string. The1042came from the question; nothing in your code mentioned it. This is the whole trick. The model cannot look anything up, so it asks you to, in the exact shape you told it a lookup takes.status: completed— the response finished normally. Completed does not mean the question is answered. It means the model has said everything it wants to say for this turn, and what it wants is a lookup.
Now look at what the code did to get there.
tools is a list with one entry, the tool. Its type is function. Its name is what the model will say when it asks for it. Its description is four sentences: what the tool does, what it returns, when to use it, and what happens for a bad input. That description is the only thing the model knows about your tool, so it is the most important text in the file. parameters is the JSON schema of the input: one property, order_id, a string with its own description, listed under required, with additionalProperties: false so the model cannot invent extra fields. And strict: trueat the end makes the schema a promise rather than a hint. Setting strict to true will ensure function calls reliably adhere to the function schema, instead of being best effort; OpenAI recommends always enabling strict mode. Strict mode has two requirements, and the schema above meets both: under strict mode, additionalProperties must be set to false for each object in the parameters, and all fields in properties must be marked as required.
client.responses.create(...) is the request. It carries the model, the tools, and input: a list with one message from the user, the question. new OpenAI() above it reads your key from the environment.
The for loop at the bottom reads the response. first.outputis a list of items the model produced. The response output array contains an entry with the type having a value of function_call; each entry has a call_id (used later to submit the function result), a name, and JSON-encoded arguments. Your loop prints the name and the arguments of anyfunction_call item, and the text of any message item through first.output_text. In this run there was no message — the model went straight to the lookup. The last line prints first.status.
Note what has not happened. No order was looked up. The model does not know that order 1042 exists, and it never will unless you tell it. Step 2 is where you do.
Step 2 — Run the tool and hand the result back
Add the following below what you wrote in Step 1, in the same file:
return-the-result const ORDERS = {
'1042': { status: 'shipped', carrier: 'DHL', expected_delivery: '2026-09-14' },
'1043': { status: 'packed', carrier: null, expected_delivery: '2026-09-16' },
};
function getOrderStatus(args) {
const order = ORDERS[args.order_id];
return JSON.stringify(order ?? { error: `no order numbered ${args.order_id}` });
}
const input = [{ role: 'user', content: question }, ...toResponseInputItems(first.output)];
for (const item of first.output) {
if (item.type !== 'function_call') continue;
const output = getOrderStatus(JSON.parse(item.arguments));
console.log(`function_call_output: ${output}`);
input.push({ type: 'function_call_output', call_id: item.call_id, output });
}
const second = await client.responses.create({ model: 'gpt-5.6-luna', tools, input });
console.log(`status: ${second.status}`);
console.log(second.output_text);
Output
function_call_output: {"status":"shipped","carrier":"DHL","expected_delivery":"2026-09-14"}
status: completed
Order **1042** has shipped via **DHL**. It’s expected to arrive on **September 14, 2026**.
Ran node@24.14.1 openai 7.15.0
Run the file again with node tool-use.mjs. Because the file runs from the top, Step 1's two lines print first, exactly as before; then three new lines follow. Read the new ones.
function_call_output: {"status":"shipped","carrier":"DHL","expected_delivery":"2026-09-14"}— your code answered the model's request.ORDERSstands in for a real order system: two orders, in a plain object.getOrderStatusreads theorder_idthe model asked for, finds the order, and returns it as a JSON string — or an error object for a number that is not there. The shape is yours to choose. The result you pass in the function_call_output message should typically be a string, where the format is up to you (JSON, error codes, plain text, etc.), and the model will interpret that string as needed.status: completed— the second response finished.Order **1042** has shipped via **DHL**. It’s expected to arrive on **September 14, 2026**.— the model's answer, printed throughsecond.output_text. The two stars around words are Markdown bold; the model writes Markdown unless you tell it not to. Notice that every fact in the sentence — the carrier, the date, the word shipped — exists only because your code returned it. The model turned your JSON into a sentence and added nothing.
The second request is the part people get wrong, so look at how input was built. It starts with the original question. Then it carries the model's own output items from the first response, converted by toResponseInputItemsinto the form the API accepts as input; the function call the model made is among them, and so is anything else the model produced. For reasoning models, any reasoning items returned in model responses with tool calls must also be passed back with tool call outputs. The helper takes care of that. Then, for everyfunction_call in the first response, the loop pushes one function_call_output item carrying two things: the call_id copied from the call, and output, the string your tool returned. The tool call output can either be structured JSON or plain text, and it should contain a reference to a specific model tool call, referenced by call_id. That id is how the model matches your answer to its question when it has asked more than one.
The request then goes out with the same tools list as before. The tools are sent every time; the model has no memory of your last request beyond what input carries.
Step 3 — Let the loop run until the model stops asking
Steps 1 and 2 are one round trip, written out by hand. A real program cannot know in advance how many round trips a question needs, so it loops. Add this below Step 2's code:
the-loop async function runAgent(userQuestion) {
const history = [{ role: 'user', content: userQuestion }];
for (let turn = 1; turn <= 5; turn += 1) {
const response = await client.responses.create({ model: 'gpt-5.6-luna', tools, input: history });
history.push(...toResponseInputItems(response.output));
const calls = response.output.filter((item) => item.type === 'function_call');
if (calls.length === 0) {
console.log(`turn ${turn}: no function call, status ${response.status}`);
return response.output_text;
}
for (const call of calls) {
const output = getOrderStatus(JSON.parse(call.arguments));
console.log(`turn ${turn}: ${call.name}(${call.arguments}) -> ${output}`);
history.push({ type: 'function_call_output', call_id: call.call_id, output });
}
}
throw new Error('the model was still asking for tools after 5 turns');
}
console.log(await runAgent('Which of my orders 1042 and 1043 arrives first, and how many days apart are they?'));
Output
turn 1: get_order_status({"order_id":"1042"}) -> {"status":"shipped","carrier":"DHL","expected_delivery":"2026-09-14"}
turn 1: get_order_status({"order_id":"1043"}) -> {"status":"packed","carrier":null,"expected_delivery":"2026-09-16"}
turn 2: no function call, status completed
Order **1042** arrives first, with expected delivery on **September 14, 2026**. Order **1043** is expected on **September 16, 2026**, so they are **2 days apart**.
Ran node@24.14.1 openai 7.15.0
Run the file a third time. After the five lines from Steps 1 and 2, the loop prints its own account of what happened.
turn 1: get_order_status({"order_id":"1042"}) -> {"status":"shipped","carrier":"DHL","expected_delivery":"2026-09-14"}and, on the next line, the same for1043— in its first turn the model asked for twolookups at once, one per order in the question. Your loop answered both before sending anything back. The model may choose to call multiple functions in a single turn; you can prevent this by setting parallel_tool_calls to false, which ensures exactly zero or one tool is called. There is no reason to prevent it here: two lookups in one turn is one request fewer.turn 2: no function call, status completed— on the second turn the model had both results and nothing more to ask. The loop saw nofunction_callitem and returned the text.Order **1042** arrives first, with expected delivery on **September 14, 2026**. Order **1043** is expected on **September 16, 2026**, so they are **2 days apart**.— the final answer. The two dates came from your tool; the subtraction the model did itself.
Read runAgent once from the top and the shape of every tool-using program is in front of you. history is the running list of everything said so far: the question, then, turn by turn, the model's items and your outputs. Each turn sends the whole historywith the tools. When the model calls a function, you must execute it and return the result; since model responses can include zero, one, or multiple calls, it is best practice to assume there are several. So the loop collects everyfunction_call item of the turn into calls, and the exit test is calls.length === 0: a turn with no calls is the model's final answer. A turn with calls runs each one, pushes each output under its call_id, and goes round again. With Responses, your application can continue this flow for as many tool calls as the task requires. Thefor loop's limit of 5 turns is the one thing the platform does not give you; "Where people get it wrong" says why you want it.
Put the three steps together and you have the five steps of the platform's own description. The tool calling flow has five high level steps: make a request to the model with tools it could call; receive a tool call from the model; execute code on the application side with input from the tool call; make a second request to the model with the tool output; receive a final response from the model (or more tool calls). Step 1 was the first two; Step 2 the next three; Step 3 the parenthesis.
The one rule
The model never runs anything. Every tool call is a round trip — the model asks, your code executes, you report back under the same call_id, the model continues — and the model, not your code, decides when it is done. Everything else in this guide is bookkeeping around that rule.
| Turn | What the model sends | What you do |
|---|---|---|
| 1 | one or more function_call items |
run each, push one function_call_output per call, send the whole history back |
| 2 | more calls, or a message with no calls | more calls: same again; no calls: that message is the answer |
If you keep the rule in mind, the two most common bugs become obvious: answering a call the model never made, and not answering one it did.
When to use it, and when not
| Situation | Decision | Reason |
|---|---|---|
| The answer lives in data the model has never seen — your orders, a live price, a file on your disk | Use it | The model has no other way to reach it; a tool is the only bridge |
| The question needs an action with a side effect — send an email, issue a refund, write a file | Use it, with care | The model can only describe the action; your code performs it, and your code can refuse |
| You must rely on the shape of the answer — a JSON object with fixed fields | Use it | A strict function schema makes the shape a guarantee |
| Summarising, translating, or answering from general knowledge | Don't | There is nothing to execute; a tool round trip only adds a request |
| A quick one-shot answer where the second request costs more than it is worth | Don't | Every tool call is at least one more request before the answer |
| Dozens of tools the model must choose between on every turn | Not yet | Accuracy falls as the list grows; start small and add |
One cost is easy to miss when the table says "use it". Under the hood, functions are injected into the system message in a syntax the model has been trained on, so callable function definitions count against the model’s context limit and are billed as input tokens. A long description is worth paying for; a tool the model never needs is not.
Terms that came up
- Tool — an operation you let the model request. It runs in your code, never in the model.
- Function — a tool described with a JSON schema; the kind of tool this guide uses.
- JSON schema — a machine-readable description of a value's shape: its fields, their types, which are required.
- Function call (
function_call) — an item in the model's output that names a tool and gives its arguments as a JSON string. - Function call output (
function_call_output) — the item you send back with the result, under the call'scall_id. call_id— the identifier that ties one output to one call; a call also has anid, which is not the same field.- Strict mode (
strict: true) — the setting that makes the model's arguments always match your schema. - Turn — one request and its response; a tool-using program takes several turns to answer one question.
- The loop — the code that sends the history, answers every call, and repeats until a turn has no calls.
- Responses API — the OpenAI endpoint this guide calls,
client.responses.create, whoseoutputlist holds the items and whoseoutput_textcollects the text. status— the field that says whether a response finished;completedmeans the turn ended normally, whether or not it asked for a tool.
In short
Tool use is one exchange, repeated: the model asks for an operation it cannot perform, your code performs it and reports back under the call's id, and the model continues until a turn has nothing to ask. The one file you ran three times on this page is that exchange written out once by hand and once as a loop. Describe the tool well, answer every call, and cap the loop, and the rest is your own code.
What changed recently
No source of this guide has changed since the last check.