
LlamaIndex is built around grounding answers in real data. Usually that data is yours: documents you have indexed, embedded, and made searchable.
The awkward half is everything that is not yours and not static. Competitor pricing. A vendor's changelog. What was published this morning. You cannot index your way to that, because by the time you have, it has moved.
This tutorial adds the live half. Five tools that read the web on demand, sitting alongside whatever you have already indexed. My honest view is that most RAG systems need less of this than they think and more of it than they have.
What you need
- A Node project (the package ships dual ESM and CJS builds)
llamaindex0.12.x (the peer dependency is^0.12.0, so it is pinned to the 0.12 line)- A Tabstack API key from console.tabstack.ai
npm install @tabstack/llamaindex llamaindex zod @llamaindex/workflow @llamaindex/openaiexport TABSTACK_API_KEY="your-key-here"llamaindex and zod are peer dependencies, so your app's single instance of each is used. Zod 3 and Zod 4 both work here, since LlamaIndex's tool() is Zod-native and accepts either. Nothing to think about on upgrade.
@llamaindex/workflow and @llamaindex/openai are LlamaIndex's own packages rather than anything to do with Tabstack. The agent() helper lives in the first and the model provider in the second, and neither is re-exported from the llamaindex root. Workflow does arrive as a transitive dependency under npm's hoisted layout, so you can get away with omitting it there, but declare it anyway or the snippet breaks the moment you move to pnpm or Yarn PnP. Swap @llamaindex/openai for whichever provider you actually use.
Step 1: an agent that reads the web
import { tabstackTools } from "@tabstack/llamaindex";
import { agent } from "@llamaindex/workflow";
import { openai } from "@llamaindex/openai";
import { Settings } from "llamaindex";
Settings.llm = openai({ model: "gpt-4o" });
const researcher = agent({ tools: tabstackTools });
const result = await researcher.run(
"What are Vercel's current pricing plans? Cite your sources.",
);
console.log(result.data);tabstackTools is a pre-built array, which is the shape agent() expects. The tools resolve TABSTACK_API_KEY lazily on first call, so importing the package never requires a key to be set.
That is a working agent with live web access in about ten lines, and there is nothing running on your side. No Playwright, no browser binary, no headless Chrome to keep patched.
Step 2: the five tools
| Export | Tool name | What it does |
|---|---|---|
extractStructuredDataTool | extract_structured_data | Pull specific fields from a URL into a JSON shape you define |
extractPageContentTool | extract_page_content | Fetch a page as clean markdown |
researchQuestionTool | research_question | Synthesised answer with cited sources across multiple pages |
generateStructuredDataTool | generate_structured_data | Fetch a page, then AI-transform it into derived or reshaped JSON |
automateBrowserTaskTool | automate_browser_task | Run a multi-step, natural language browser task |
Import individually for a subset, or use the array for all of them:
import { researchQuestionTool, extractPageContentTool } from "@tabstack/llamaindex";
import { agent } from "@llamaindex/workflow";
const researcher = agent({ tools: [researchQuestionTool, extractPageContentTool] });Worth doing. Every tool you pass is another option the model weighs on every step, and another way for it to pick wrong. An agent that reads and researches does not need browser automation in its toolbox.
Two of these are especially worth knowing well.
extract_page_content returns clean markdown. That is the format you want when the output is heading somewhere else, whether that is a summarisation step, a prompt, or an ingestion pipeline. It is not HTML you then have to strip.
research_question returns a synthesised answer with the sources behind it. This is the one that is genuinely hard to build yourself, and the one to reach for when the answer is not on any single page you can name.
Step 3: extract or generate
The inputs, which the model fills in:
extract_structured_data:url,json_schema_json, a JSON encoded JSON Schema stringextract_page_content:urlresearch_question:querygenerate_structured_data:url,instructions,json_schema_jsonautomate_browser_task:task, plus optionalurl,guardrails,data,country,max_iterations,max_validation_attempts
The distinction people get wrong is between the two structured tools. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category, a sentiment, or a one-line summary nobody wrote down.
If the field exists in the HTML, extract it. If you are asking for judgement, generate it. Reaching for generate when extract would do costs you time and adds a layer where the answer can drift away from what the page actually said.
Inputs are validated against the core Zod schema before the SDK call, since LlamaIndex passes the schema straight through as the tool's parameters. Malformed model output is caught before a request goes out rather than after the API rejects it.
Step 4: the flags that matter in production
The model can pass these itself, and they are sent to Tabstack only when present, so leaving them out keeps the defaults.
On extract_structured_data, extract_page_content, and generate_structured_data:
effort:"min","standard", or"max". Use"max"for JavaScript-heavy pages, where it does full server-side browser rendering.nocache: bypass the cache.country: an ISO 3166-1 alpha-2 code for geotargeted fetches.
On research_question: mode as "fast" or "balanced", plus nocache.
If a page comes back thin or empty, it is nearly always a client-rendered app and effort: "max" is the fix. That single argument is the whole workaround, and it costs you a keyword rather than a browser dependency.
nocache is the one to remember for this audience specifically. If you are pulling live data precisely because it changes, a cached read defeats the purpose. Set it on anything you are monitoring.
Step 5: failures
When a tool call fails it throws TabstackToolError, carrying a normalised message plus an HTTP status for API errors, and LlamaIndex surfaces it in the tool result. The agent sees a readable failure and can retry or explain rather than the run falling over.
One catch covers transport problems, parsing problems, and API problems. status carries the HTTP status for API failures and is undefined for everything else, which is usually enough to decide between retrying and giving up.
One behaviour to know before shipping: automate_browser_task runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block an agent waiting on a person. It returns the final answer plus the data it extracted and the pages it visited. If you genuinely need a human in the middle of a browser session, that is the interactive mode flow in the API rather than this tool.
Step 6: custom keys and clients
For a key per tenant, a custom base URL, or reuse of an SDK client you already have:
import { createTabstackLlamaindexTools } from "@tabstack/llamaindex";
const tools = createTabstackLlamaindexTools({ apiKey: process.env.MY_KEY });Same array shape, same tool names, so it drops in exactly like the default export.
Where to take it next
The obvious move is combining these tools with your own. An agent holding both a query engine over your documents and research_question over the live web can answer questions neither source covers alone, which is usually the question someone actually wanted to ask.
extract_page_content is also worth a look as an ingestion step rather than an agent tool. Clean markdown is a far better starting point for chunking than raw HTML, and you get it without running a browser or maintaining a parser.
The tool names are identical in @tabstack/langchain, @tabstack/ai, @tabstack/eve, and the Python langchain-tabstack, so prompts carry over if this workload moves.
The package is on npm as @tabstack/llamaindex, and the full API reference is at docs.tabstack.ai.
Ready to build? Sign up for 10,000 free credits, no card required, and query your first live page alongside your index. Full API in the docs.