LlamaIndex.TS agents give you a typed, composable way to wire LLM reasoning to executable tools inside Node.js. In this tutorial we build a working llamaindex.ts agents example from an empty directory to a multi-tool conversational agent, using only the public LlamaIndex.TS API and nothing fabricated.
Prerequisites
- Node.js 18.18+ (needed for global
fetchand stable async context). - TypeScript 5.2+ and
tsxfor running TS directly, or a standardtscbuild step. - An OpenAI API key (or any OpenAI-compatible key). We repoint the client later.
- Comfort with
async/await, ES modules, and reading JSON schemas.
Create a working directory and install the minimal set:
mkdir llamaindex-ts-agent && cd llamaindex-ts-agent
npm init -y
npm install llamaindex
npm install -D typescript tsx @types/node
Export your key before running anything:
export OPENAI_API_KEY="sk-..."
Project setup
Use ES modules. Edit package.json to add "type": "module" and a start script:
{
"type": "module",
"scripts": {
"start": "tsx agent.ts"
}
}
Create tsconfig.json with strict mode and bundler resolution so tsx and tsc agree:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}
Define a tool
A tool is a typed function the agent can call. LlamaIndex.TS uses the tool() helper to declare the JSON schema and the execute body. The schema is sent to the model verbatim, so precision matters.
Create tools.ts:
import { tool } from "llamaindex";
export const addTool = tool({
name: "add",
parameters: {
type: "object",
properties: {
a: { type: "number", description: "first operand" },
b: { type: "number", description: "second operand" },
},
required: ["a", "b"],
},
execute: ({ a, b }: { a: number; b: number }) => a + b,
});
execute may be sync or return a Promise. Return values are serialized back to the model as a tool result message.
Create and run an agent
The agent() factory takes an LLM instance and a tool list. The openai() adapter returns a compliant LLM.
Create agent.ts:
import { agent, openai } from "llamaindex";
import { addTool } from "./tools.js";
const llm = openai({ model: "gpt-4o-mini" });
const myAgent = agent({
llm,
tools: [addTool],
systemPrompt: "You are a concise math assistant.",
});
const result = await myAgent.run("What is 21 plus 21?");
console.log(result.data);
Run it:
npm start
Expected output (wording may vary):
21 plus 21 is 42.
The agent emitted a tool call add({a:21,b:21}), received 42, and synthesized the sentence. That loop is the core of llamaindex.ts agents.
Inspecting agent internals
To see what the model actually did, print the message list:
console.log(JSON.stringify(result.messages, null, 2));
You will find an assistant message with a toolCalls array containing the function name and arguments, followed by a tool message with the returned value. This is the cheapest debugging surface you have—log it in development.
Multi-turn chat with memory
run is stateless. For conversation, use chat, which appends to an internal session history.
const turn1 = await myAgent.chat("What is 2 + 3?");
console.log("Turn 1:", turn1.data);
const turn2 = await myAgent.chat("Now multiply that by 4.");
console.log("Turn 2:", turn2.data);
Expected output:
Turn 1: 2 + 3 is 5.
Turn 2: 5 multiplied by 4 is 20.
The agent referenced the prior result without you passing it explicitly. If you need explicit control, pass turn1.messages into the next call’s messages field.
Composing multiple tools
Real agents route between many tools. Add a string reverser and a stubbed weather tool.
Append to tools.ts:
export const reverseTool = tool({
name: "reverse",
parameters: {
type: "object",
properties: { s: { type: "string" } },
required: ["s"],
},
execute: ({ s }: { s: string }) => s.split("").reverse().join(""),
});
export const weatherTool = tool({
name: "weather",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
execute: async ({ city }: { city: string }) => `${city}: 22°C, cloudy`,
});
Recreate the agent with all three:
const multiAgent = agent({
llm,
tools: [addTool, reverseTool, weatherTool],
});
const r = await multiAgent.run(
"What is the reverse of 'llamaindex' and the weather in Berlin?"
);
console.log(r.data);
Expected output:
The reverse of 'llamaindex' is 'xedinamall'. Berlin weather: 22°C, cloudy.
The model issued two tool calls in one turn. LlamaIndex.TS waits for both and feeds results back together.
Streaming token output
Stream the final synthesis to avoid blocking the CLI:
const stream = await myAgent.run("Count from 1 to 3 slowly.", { stream: true });
for await (const chunk of stream) {
process.stdout.write(chunk.data ?? "");
}
Tokens print incrementally. Tool-execution steps are not streamed—only the final assistant text.
Swapping the model provider
The openai adapter accepts baseURL. This is where an OpenAI-compatible gateway drops in without code changes. For instance, n4n.ai exposes one OpenAI-compatible endpoint that fronts 240+ models and applies automatic fallback when a provider is rate-limited or degraded; pointing baseURL there keeps the same agent and tool definitions.
const llm = openai({
model: "anthropic/claude-3.5-sonnet",
apiKey: process.env.N4N_API_KEY,
baseURL: "https://api.n4n.ai/v1",
});
Tool schemas, streaming, and chat memory behave identically. That is the practical payoff of building on a standard interface.
Wrapping tools in an Express endpoint
Engineers rarely run agents from a bare script. Here is a minimal HTTP surface:
import express from "express";
import { agent, openai } from "llamaindex";
import { addTool } from "./tools.js";
const app = express();
app.use(express.json());
const llm = openai({ model: "gpt-4o-mini" });
const apiAgent = agent({ llm, tools: [addTool] });
app.post("/ask", async (req, res) => {
const { question } = req.body;
const result = await apiAgent.run(question);
res.json({ answer: result.data });
});
app.listen(3000, () => console.log("listening on :3000"));
Install express and npm install @types/express for types. This turns your llamaindex.ts agents experiment into a service with a single route.
Error handling and timeouts
Tools fail in production. Return a structured error string from execute; the agent will reason about it.
execute: async ({ city }) => {
if (!city) return "Error: city required";
return `${city}: 22°C`;
}
LlamaIndex.TS does not ship a built-in agent timeout. Enforce one at the LLM client layer (e.g., fetch signal via a custom openai client) so a hung provider does not block your process.
Where to go next
You now have a runnable agent with typed tools, multi-turn memory, streaming, and multi-provider support. For production, split tools into their own modules, validate execute inputs manually or with a schema library, and persist messages to your own store if you need cross-process sessions. The workflow primitives in LlamaIndex.TS let you orchestrate multiple agents when a single loop gets crowded.
Keep the agent loop strict: never let the model call a tool that mutates external state without a human checkpoint if the blast radius is real. The rest is packaging and observability.