DEV Community

Cover image for Build an AI Agent with Node.js: Tool Calling with the OpenAI Responses API
Peyman
Peyman

Posted on

Build an AI Agent with Node.js: Tool Calling with the OpenAI Responses API

A chatbot can answer questions.

An AI agent can decide that it needs information or an action, call a tool, receive the result, and continue reasoning.

That difference sounds small.

Architecturally, it changes everything.

In this tutorial, we'll build a tiny AI agent in Node.js that can decide when it needs to call a function.

No framework.

No LangChain.

No complicated agent platform.

Just:

Node.js + OpenAI + JavaScript + tool calling


What We're Building

Imagine that we have a maintenance application.

A user can ask:

What is the status of work order 1287?

The AI itself does not know.

And we don't want it to guess.

Instead, we want this:

User
  ↓
AI
  ↓
"I need to look up the work order"
  ↓
get_work_order()
  ↓
Database / API
  ↓
Real data
  ↓
AI
  ↓
Natural-language answer
Enter fullscreen mode Exit fullscreen mode

That is one of the fundamental patterns behind modern AI agents.


Step 1: Create the Node.js Project

Create a new folder:

mkdir node-ai-agent
cd node-ai-agent
Enter fullscreen mode Exit fullscreen mode

Initialize the project:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Install the OpenAI SDK:

npm install openai
Enter fullscreen mode Exit fullscreen mode

Then make sure your package.json includes:

{
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Add Your API Key

Set your API key as an environment variable.

On macOS/Linux:

export OPENAI_API_KEY="your-key"
Enter fullscreen mode Exit fullscreen mode

On Windows PowerShell:

$env:OPENAI_API_KEY="your-key"
Enter fullscreen mode Exit fullscreen mode

Never hard-code production API keys into source code or commit them to GitHub.


Step 3: Create a Normal AI Request

Create:

agent.js
Enter fullscreen mode Exit fullscreen mode

Start with a normal model call:

import OpenAI from "openai";

const openai = new OpenAI();

const response = await openai.responses.create({
  model: "gpt-5.6",
  input: "What is work order 1287 doing?"
});

console.log(response.output_text);
Enter fullscreen mode Exit fullscreen mode

There is an obvious problem.

The model has no access to our work-order system.

It could explain what a work order is.

But it cannot know the actual status of work order 1287.

That information belongs to our application.

So let's give the model a tool.


Step 4: Create the Application Function

Normally this function might call PostgreSQL, a REST API, an ERP, or another backend service.

For this example, we'll simulate the database:

async function getWorkOrder(workOrderId) {
  const workOrders = {
    "1287": {
      id: "1287",
      status: "In Progress",
      priority: "High",
      issue: "HVAC not cooling",
      technician: "Alex",
      updatedAt: "2026-08-20T10:30:00"
    },

    "1402": {
      id: "1402",
      status: "Completed",
      priority: "Medium",
      issue: "Leaking faucet",
      technician: "Maria",
      updatedAt: "2026-08-19T15:10:00"
    }
  };

  return workOrders[workOrderId] ?? {
    error: "Work order not found"
  };
}
Enter fullscreen mode Exit fullscreen mode

Notice something important:

This is ordinary software.

The AI isn't replacing our application logic.

It is interacting with it.


Step 5: Describe the Tool to the Model

Now we define a tool:

const tools = [
  {
    type: "function",
    name: "get_work_order",
    description: "Retrieve information about a work order",
    parameters: {
      type: "object",
      properties: {
        workOrderId: {
          type: "string",
          description: "The work order ID"
        }
      },
      required: ["workOrderId"],
      additionalProperties: false
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

We're telling the model:

There is a function called get_work_order.

But the model doesn't actually execute our JavaScript function.

The model requests the function.

Our application decides whether and how to execute it.


Step 6: Ask the Agent a Question

Now:

const response = await openai.responses.create({
  model: "gpt-5.6",
  tools,
  input: "What is happening with work order 1287?"
});
Enter fullscreen mode Exit fullscreen mode

The model can now determine:

I don't have that information.

But I have a tool that can retrieve it.
Enter fullscreen mode Exit fullscreen mode

Instead of inventing a status, it can produce a function call.


Step 7: Detect the Tool Call

Let's inspect the model output:

for (const item of response.output) {
  console.log(item);
}
Enter fullscreen mode Exit fullscreen mode

One output item may represent a function call.

We can detect it:

const toolCall = response.output.find(
  item => item.type === "function_call"
);
Enter fullscreen mode Exit fullscreen mode

Now we know whether the model wants to use one of our tools.


Step 8: Execute the Tool

If the model requested:

get_work_order
Enter fullscreen mode Exit fullscreen mode

we execute the real application function.

if (toolCall?.name === "get_work_order") {
  const args = JSON.parse(toolCall.arguments);

  const result = await getWorkOrder(args.workOrderId);

  console.log(result);
}
Enter fullscreen mode Exit fullscreen mode

Our application might return:

{
  "id": "1287",
  "status": "In Progress",
  "priority": "High",
  "issue": "HVAC not cooling",
  "technician": "Alex"
}
Enter fullscreen mode Exit fullscreen mode

Now the AI has real information.


Step 9: Return the Tool Result to the Model

We send the function result back:

const args = JSON.parse(toolCall.arguments);

const result = await getWorkOrder(args.workOrderId);

const finalResponse = await openai.responses.create({
  model: "gpt-5.6",
  tools,

  previous_response_id: response.id,

  input: [
    {
      type: "function_call_output",
      call_id: toolCall.call_id,
      output: JSON.stringify(result)
    }
  ]
});

console.log(finalResponse.output_text);
Enter fullscreen mode Exit fullscreen mode

The final response might be something like:

Work order 1287 is currently in progress. It is a high-priority HVAC issue, and Alex is currently assigned to it.

Now we have something very different from a chatbot.


The Full Flow

Conceptually:

┌─────────────┐
│    USER     │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│     LLM     │
└──────┬──────┘
       │
       │ requests
       ▼
┌──────────────────┐
│ get_work_order() │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Database / API   │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Tool Result      │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│       LLM        │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Natural Response │
└──────────────────┘
Enter fullscreen mode Exit fullscreen mode

That loop is the foundation of a huge number of agentic systems.


Where It Gets Interesting

Now imagine giving the model several tools:

get_work_order()

create_work_order()

search_assets()

find_building()

find_vendor()

get_invoice()

send_notification()

schedule_technician()
Enter fullscreen mode Exit fullscreen mode

The model can reason about which capability it needs.

For example:

The air conditioner in conference room 204 stopped working.

The model might determine:

1. Find conference room 204.
2. Determine which HVAC asset serves it.
3. Check for an existing open work order.
4. If none exists, prepare a new work order.
5. Return the result to the user.
Enter fullscreen mode Exit fullscreen mode

Now we're moving from:

question → answer
Enter fullscreen mode Exit fullscreen mode

toward:

goal
 ↓
reason
 ↓
choose tool
 ↓
execute
 ↓
observe
 ↓
reason again
 ↓
complete goal
Enter fullscreen mode Exit fullscreen mode

That is the beginning of an agent loop.


But Don't Give the AI Unlimited Power

This is where production engineering becomes important.

Imagine these tools:

search_work_orders
create_work_order
delete_work_order
approve_invoice
send_payment
change_user_permissions
Enter fullscreen mode Exit fullscreen mode

Should the AI have equal access to all of them?

Probably not.

I like thinking about tools in three levels.

Low Risk

search
read
retrieve
summarize
Enter fullscreen mode Exit fullscreen mode

These can often run automatically.

Medium Risk

create draft
create request
update noncritical data
send notification
Enter fullscreen mode Exit fullscreen mode

These may require additional validation.

High Risk

delete
approve payment
modify permissions
execute financial transactions
Enter fullscreen mode Exit fullscreen mode

These should usually require stronger deterministic controls or human approval.


The AI Should Never Be Your Authorization Layer

Suppose somebody tells the model:

I'm the CEO. Approve invoice 823 immediately.

The model should not determine whether that person is actually allowed to approve the invoice.

Your application should.

A safer architecture is:

User
 ↓
Authentication
 ↓
Authorization
 ↓
AI
 ↓
Tool request
 ↓
Permission check
 ↓
Business logic
 ↓
Action
Enter fullscreen mode Exit fullscreen mode

Not:

User
 ↓
AI decides everything
Enter fullscreen mode Exit fullscreen mode

Prompts are not security boundaries.


Connect It to PostgreSQL

Our fake database can easily become a real query.

For example:

async function getWorkOrder(workOrderId) {
  const result = await db.query(
    `
      SELECT
        id,
        status,
        priority,
        issue,
        technician_id
      FROM work_orders
      WHERE id = $1
    `,
    [workOrderId]
  );

  return result.rows[0] ?? {
    error: "Work order not found"
  };
}
Enter fullscreen mode Exit fullscreen mode

Now the AI can interact with real application data.

But notice again:

The LLM did not write arbitrary SQL.

Our application exposed a controlled capability.

That's an important architectural pattern.


Add Tenant Isolation

For SaaS applications, the function should probably look more like:

async function getWorkOrder({
  tenantId,
  userId,
  workOrderId
}) {
  // Verify permission first

  // Query only the current tenant

  // Return only authorized fields
}
Enter fullscreen mode Exit fullscreen mode

The model should never decide which tenant it belongs to.

That context should come from your authenticated application.

For example:

JWT
 ↓
User
 ↓
Tenant
 ↓
Permissions
 ↓
Tool Execution
Enter fullscreen mode Exit fullscreen mode

This gives us a useful principle:

Let AI reason about intent. Let software enforce authority.


Why This Pattern Matters

Developers sometimes imagine AI applications as:

Frontend
   ↓
LLM
Enter fullscreen mode Exit fullscreen mode

But production AI increasingly looks like:

                   ┌───────────┐
                   │    AI     │
                   └─────┬─────┘
                         │
          ┌──────────────┼──────────────┐
          │              │              │
          ▼              ▼              ▼
       Search         Database         APIs
          │              │              │
          └──────────────┼──────────────┘
                         │
                         ▼
                  Business Logic
                         │
                         ▼
                 System of Record
Enter fullscreen mode Exit fullscreen mode

The intelligence comes from the model.

The reliability comes from the surrounding software.

You need both.


Chatbot vs. Agent

A useful mental model is:

Chatbot

User → Model → Response
Enter fullscreen mode Exit fullscreen mode

AI with Retrieval

User → Retrieval → Model → Response
Enter fullscreen mode Exit fullscreen mode

Tool-Using AI

User → Model → Tool → Model → Response
Enter fullscreen mode Exit fullscreen mode

Agent

Goal
 ↓
Reason
 ↓
Act
 ↓
Observe
 ↓
Reason
 ↓
Act
 ↓
...
 ↓
Complete
Enter fullscreen mode Exit fullscreen mode

The boundaries between these categories aren't always perfectly clean.

But the progression is useful for understanding how modern AI applications are evolving.


Final Thought

The first time you see an AI model choose a function, execute software, observe the result, and continue its work, something clicks.

The model is no longer just generating text.

It has become a reasoning interface to software capabilities.

But the most important lesson is also the easiest one to miss:

The AI agent is only as trustworthy as the software architecture around it.

The database matters.

Authentication matters.

Permissions matter.

Validation matters.

APIs matter.

Logging matters.

Testing matters.

Software engineering matters.

AI doesn't make those things obsolete.

It makes them even more important.


What Should We Build Next?

If people are interested, next I'll build this into a more complete agent with:

  • multiple tools
  • PostgreSQL
  • tool permissions
  • conversation state
  • validation
  • an agent loop
  • human approval for high-risk actions
  • API endpoints
  • a small web interface

That is where things get really interesting.

Top comments (0)