Native Tool Calling
Introduction: From Prompted JSON to Native Tool Calls
You've just learned how to prompt the model to return structured JSON by including a schema in your system prompt. Now you're ready to take the next step: using OpenAI's native tool-calling feature, which provides stronger guarantees and implements Factor 4 (Tools are just structured outputs) of the 12-Factor Agents methodology directly. By the end of this lesson, you'll be able to define typed tool schemas, require their use via client.responses.create(...), and handle the structured output reliably in TypeScript.
Defining a Tool Schema
A tool schema is a formal definition of a function that the model can "call" by returning structured data. You define the function's name, what it does, and what parameters it expects. The schema follows JSON Schema conventions, which means you specify types, descriptions, and which fields are required.
The schema is a typed object that defines the interface between your code and the model. By importing the Tool type from the OpenAI SDK, TypeScript will catch any structural mistakes at compile time before your code ever runs. Each field plays a specific role:
type: For tool definitions, this is always set to"function".name: The identifier your code will use to recognize this tool — in this case,"final_answer".description: A natural language explanation that helps the model understand when it is appropriate to use this tool.strict: Setting this totrueenables Structured Outputs for tools, constraining successful tool-call arguments to match your parameter schema more reliably.parameters: An object usingJSON Schemanotation to define the expected arguments, such as our"answer"string.required: A list of property names that the model must include for the tool call to be considered valid.additionalProperties: false: A strict constraint that prevents the model from adding unexpected fields to the output.
This explicit contract gives you stronger provider-side constraints on the shape of successful final_answer calls. By defining these constraints, you reduce the amount of validation your application needs, but you do not eliminate defensive checks in your code.
Creating a Simple System Prompt
With your tool schema defined, you can keep your instructions simple since the tools parameter will handle the structured output definition. In the TypeScript SDK, you pass the assistant's persona directly as a string through the instructions field of client.responses.create(...) — no separate variable needed.
Notice how much simpler this is than before, when you had to explicitly describe a JSON schema inside the prompt. The tool-calling feature handles that structure for you, so instructions can focus on the assistant's role and behavior rather than output format details.
Making the API Request with Tools
Now you pass the tool schema to the model via the tools parameter. The critical addition is the tool_choice parameter, which controls whether the model must use a tool.
The tools parameter receives your toolSchemas array, making all defined tools available to the model. The tool_choice: "required" parameter is critical: it tells the model that it must respond by calling one of the provided tools rather than sending back a regular text message.
This enforcement is what provides a stronger guarantee compared to prompting alone. If you omitted tool_choice or set it to "auto", the model might decide to respond conversationally instead of calling a tool, which would break your structured output expectation. With this setup, you're ready to handle the structured response.
Inspecting the Raw Response Structure
Before parsing specific data, it is incredibly useful to understand the structure of the response object returned by client.responses.create(...). The most important property is response.output, which is an array of output items. You can log it to inspect its shape:
The output will look similar to this:
response.output is an array that may contain a reasoning item (depending on the model and settings) and a function_call item. The function_call item is what carries the actual tool invocation: it has a name field identifying which tool was called and an arguments field containing the tool's parameters, serialized as a JSON string. This is the item you need to locate and parse.
Identifying Function-Call Items
When the model responds with a tool call, you search response.output for the item whose type is "function_call". Rather than writing a loop, the idiomatic TypeScript approach uses .find():
Because response.output can contain items of different types, TypeScript's type system needs a little help before it lets you access function_call-specific properties like name and arguments. You use an if guard to narrow the type:
The optional chaining call?.type safely handles the case where .find() returns undefined (i.e., no function call was present in the output). The second condition call.name === "final_answer" is essential routing logic: in systems that expose multiple tools, it ensures you only process the item for the tool you intend to handle.
Parsing and Using Tool Arguments
Once you've identified the function call item, you extract its arguments. The arguments property is a JSON string, so you parse it with JSON.parse. Because TypeScript doesn't know the shape of that parsed value at runtime, you add a type assertion to tell the compiler exactly what to expect:
Running this logic against the model's response produces the following output:
The type assertion as { answer: string } is safe here precisely because you set strict: true in the tool schema. The model is contractually required to return an object with an answer string, so you can confidently access args.answer, knowing it will always be present.
Summary: Structured Outputs Under Your Control
You've now implemented the complete pattern for native tool-calling in TypeScript: importing the Tool type, defining a formal schema, requiring tool use with tool_choice: "required", inspecting response.output, narrowing types with .find() and an if guard, and parsing validated arguments with JSON.parse. This implements Factor 4 directly by treating tools as structured outputs enforced by the model provider. In the practice exercises, you'll define your own tool schemas and gain hands-on experience with this robust workflow.
