
Most agent tutorials stop at the point where the model says something plausible. I want to go one step further, because the interesting part is never the answer. It is the list of URLs underneath it.
We are going to build a small research agent in Python. By the end you will have an agent that can read a specific page, extract typed fields from it, and answer open questions using multiple sources with citations attached. Roughly forty lines of code, no browser to install.
If you have written LangChain before, this will feel familiar. If you have not, everything you need is here.
What you need
- Python 3.9 or later
- A Tabstack API key from console.tabstack.ai
- An OpenAI key, or any other chat model LangChain supports
pip install langchain-tabstack langchain langchain-openaiexport TABSTACK_API_KEY="your-key-here"
export OPENAI_API_KEY="your-openai-key"langchain-tabstack pulls in the official Tabstack SDK for you. There is nothing else to install, and specifically there is no Playwright and no browser binary. Page rendering happens on Tabstack's side.
Step 1: call a tool on its own
Before wiring anything into an agent, prove the plumbing works. Every tool in the package is exported individually and can be invoked directly, which makes debugging much easier later.
Create research.py:
import json
from langchain_tabstack import research_question_tool
raw = research_question_tool.invoke(
{"query": "What are the latest developments in quantum error correction?"}
)
result = json.loads(raw)
print(result["answer"])
print()
for source in result["sources"]:
print(f"- {source['title']}: {source['url']}")Run it:
python research.pyYou get a synthesised answer, then the pages it was built from. That second part is the bit worth pausing on. The tool did not just fetch a search results page and hand the model a wall of text. It read across multiple sources and returned the answer alongside the specific pages behind it, so you can put those in front of a user.
Two things about the return shape. It is a JSON string, not a dict, so json.loads is doing real work. That is the LangChain Python convention for tool output rather than a Tabstack quirk. And sources is a list of {"title": ..., "url": ...} objects, which drops straight into a citation list in whatever you are building.
The tool read TABSTACK_API_KEY from the environment on its own. The client is created lazily, so importing the package without a key set does not blow up, which keeps your imports safe in tests.
Step 2: extract typed data from a known page
Research is for open questions. When you already know which page holds the data, use extract_structured_data instead. You describe the shape you want, and you get that shape back.
import json
from langchain_tabstack import extract_structured_data_tool
schema = {
"type": "object",
"properties": {
"stories": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"points": {"type": "number", "description": "Story points"},
},
},
}
},
}
raw = extract_structured_data_tool.invoke(
{
"url": "https://news.ycombinator.com",
"json_schema_json": json.dumps(schema),
}
)
for story in json.loads(raw)["stories"]:
print(story["points"], story["title"])Note json_schema_json. The schema goes in as a JSON encoded string rather than a dict, which looks odd for about ten seconds until you remember who else calls this tool. When an agent invokes it, the model has to produce that argument, and a string is a shape models handle reliably.
Write real descriptions on your fields. "description": "Story points" is not decoration, it is the instruction that tells the extractor which number on the page you actually meant. Schemas with vague or missing descriptions are the single most common cause of disappointing extraction results.
Step 3: hand the whole toolset to an agent
Now the part you came for. Instead of deciding which tool to call, let the model decide.
from langchain.agents import create_agent
from langchain_tabstack import TABSTACK_TOOLS
agent = create_agent(
"openai:gpt-4o",
tools=TABSTACK_TOOLS,
system_prompt=(
"You are a research assistant with web intelligence tools. "
"Use extract_structured_data for specific fields from a URL, "
"extract_page_content for full page text, and research_question "
"for multi-source research. Always cite the sources you used."
),
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What are Vercel's pricing plans?"}]}
)
print(result["messages"][-1].content)One version note, because it is the first thing that will bite you. create_agent is the LangChain 1.x replacement for AgentExecutor plus create_tool_calling_agent. If you are still on 0.x you will get an import error here, and the fix is upgrading LangChain rather than anything to do with Tabstack.
TABSTACK_TOOLS is all five tools, ready to pass straight in:
| Tool | When the model reaches for it |
|---|---|
extract_structured_data | Specific fields from a URL you already know |
extract_page_content | The whole page as clean markdown |
research_question | An open question needing several sources |
generate_structured_data | Fetch a page, then AI-transform it into derived JSON |
automate_browser_task | A multi-step browser task described in plain English |
You do not have to pass all five. TABSTACK_TOOLS is a list, so slice it or import individual tools if your agent only needs two. Fewer tools means fewer chances for the model to pick the wrong one, and on smaller models that matters.
Ask it something that needs more than one page and watch which tool it selects. That decision is driven by the tool descriptions shipped in the package, which is why they are written for the model to read rather than for you.
Step 4: the flags you will need in production
Four optional inputs cover almost everything you will hit. They are only sent when you pass them, so leaving them out keeps Tabstack's defaults.
from langchain_tabstack import extract_page_content_tool
markdown = extract_page_content_tool.invoke(
{
"url": "https://example.com/app",
"effort": "max", # full server-side browser rendering
"nocache": True, # skip the cache, fetch fresh
"country": "GB", # fetch as if from the UK
}
)effort takes "min", "standard", or "max". If a page comes back suspiciously empty, it is almost always a JavaScript-rendered app and "max" is the fix. This is the flag that replaces an entire Playwright setup, and it costs you one keyword argument instead of a browser binary in your Dockerfile.
nocache matters when you are monitoring something that changes, like prices or stock. country matters when the page you want varies by region, which for pricing pages is most of the time.
research_question takes mode as "fast" or "balanced" instead of effort. Start with "fast" while you are iterating, move to "balanced" when quality matters more than latency.
Step 5: handle failures like an adult
Pages go away. Sites rate limit. Networks wobble. Every tool in the package raises one error type:
from langchain_tabstack import TabstackToolError, extract_page_content_tool
try:
markdown = extract_page_content_tool.invoke({"url": "https://example.com"})
except TabstackToolError as err:
print(f"Tabstack failed ({err.status or 'no status'}): {err}")One except clause instead of catching transport errors, parse errors, and API errors separately. err.status carries the HTTP status for API failures and is None for everything else, which is usually enough to decide between retrying and giving up.
Step 6: go async
If you are fetching several pages, do not do it one at a time. Every tool supports ainvoke natively, backed by an async Tabstack client, so it runs in your event loop rather than bouncing through a thread pool:
import asyncio
from langchain_tabstack import extract_page_content_tool
async def fetch_all(urls):
return await asyncio.gather(
*(extract_page_content_tool.ainvoke({"url": url}) for url in urls)
)
pages = asyncio.run(
fetch_all(
[
"https://example.com/one",
"https://example.com/two",
"https://example.com/three",
]
)
)Three pages in the time of the slowest one. This is the difference between an agent that feels responsive and one people abandon.
Where to take it next
You have the whole surface now. The obvious next moves:
Swap research_question output into your UI as a real citation list rather than printing it. The sources array is already the right shape for that.
Try generate_structured_data when the field you want is not written on the page. Extraction pulls out what exists. Generation produces what does not, like a category or a one-line summary, in a schema you define.
If you move this agent to TypeScript later, the tool names and inputs are identical in @tabstack/langchain and @tabstack/ai. Your prompts carry over unchanged, because the model sees exactly the same tools.
The full API reference lives at docs.tabstack.ai, and the package is on PyPI as langchain-tabstack.
Ready to build? Sign up for 10,000 free credits, no card required, and give your agent its first cited answer. Full API in the docs.