
Mastra gives you a clean Agent abstraction and expects you to bring the tools. This tutorial brings the ones nearly every agent ends up needing: read a page, pull typed fields off it, research a question across several sources, and drive a browser when the data is behind an interaction.
It is one import and one line in your agent definition. That is the entire integration, so most of what follows is about the decisions you make afterwards rather than the wiring.
What you need
- A Node project (the package ships dual ESM and CJS builds)
- Mastra v1 or later
- A Tabstack API key from console.tabstack.ai
npm install @tabstack/mastra @mastra/core zodexport TABSTACK_API_KEY="your-key-here"@mastra/core and zod are peer dependencies, so your app's single instance of each is shared. That is the detail that keeps you out of the genuinely miserable bug where two copies of Zod disagree about whether your schema is a schema.
Step 1: a web-connected agent
import { Agent } from "@mastra/core/agent";
import { tabstackTools } from "@tabstack/mastra";
const agent = new Agent({
id: "web-researcher",
name: "Web Researcher",
instructions: "Answer questions with current information, and always cite your sources.",
model: "anthropic/claude-sonnet-4-6",
tools: tabstackTools,
});
const result = await agent.generate("What are Vercel's pricing plans, with sources?");
console.log(result.text);That is a working agent with web access. tabstackTools reads TABSTACK_API_KEY from the environment, and the client is created lazily, so importing the package never requires a key to be set.
tabstackTools is a named object keyed by tool name, which is exactly the shape Mastra's tools expects. No adapter, no mapping step.
Notice what the instructions are doing. They describe the job, not the tools. Each tool ships with a description written for the model to read, so you do not need to explain in your prompt what research_question is for. Keep your instructions about the outcome you want and let the descriptions do the routing. It keeps prompts short, and it means description improvements reach you on upgrade rather than requiring a prompt rewrite.
Step 2: know your five
| Tool | What it does |
|---|---|
extract_structured_data | Pull specific fields from a URL into a JSON shape you define |
extract_page_content | Fetch a page as clean markdown |
research_question | Synthesised answer with cited sources across multiple pages |
generate_structured_data | Fetch a page, then AI-transform it into derived or reshaped JSON |
automate_browser_task | Run a multi-step, natural language browser task |
The model fills in the arguments, but knowing the shapes helps when you are reading a trace and wondering why it chose badly:
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 one distinction worth internalising is extract versus generate. Extract pulls out what is already written on the page. Generate fetches the page and produces something derived from it, like a category or a one-line summary nobody wrote. 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.
Step 3: give it fewer tools
Passing all five is the easy default, and often the wrong one. Every tool you expose is another option the model weighs on every step, and another way for it to pick wrong. On smaller models, trimming the toolset is one of the highest-leverage changes available to you.
Each tool is exported individually, and the package ships a toolNames map so you do not hand-write the string keys:
import { Agent } from "@mastra/core/agent";
import { extractPageContentTool, researchQuestionTool, toolNames } from "@tabstack/mastra";
const agent = new Agent({
id: "web-researcher",
name: "Web Researcher",
instructions: "Summarize pages and research questions.",
model: "anthropic/claude-sonnet-4-6",
tools: {
[toolNames.researchQuestion]: researchQuestionTool,
[toolNames.extractPageContent]: extractPageContentTool,
},
});Use toolNames rather than typing "research_question" by hand. Those strings are the contract shared across every Tabstack integration, including the Python package, and a typo produces a tool your prompt references and your agent does not have.
Step 4: the flags that matter in production
The model can pass these itself, and they are only sent to Tabstack when present, so leaving them alone 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, such as"US", 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 replaces installing Playwright, shipping a browser binary in your container, and keeping both patched. There is nothing running on your side at all.
nocache matters when you are watching something that moves, like prices or availability. country matters when the page varies by region, which for pricing pages is most of the time.
The clean way to apply these is in your agent's instructions rather than in code:
If a page returns little or no content, retry once with effort set to max. Use fast mode for research while iterating.
Step 5: failures
When a tool call fails it throws TabstackToolError, carrying a normalised message plus an HTTP status for API errors. Mastra surfaces it in the tool result, so the model sees a readable failure and can retry or explain rather than the run collapsing.
If you invoke a tool's execute directly, catch it yourself:
import { TabstackToolError, tabstackTools, toolNames } from "@tabstack/mastra";
try {
await tabstackTools[toolNames.extractPageContent].execute?.(
{ url: "https://example.com" },
{} as never,
);
} catch (err) {
if (err instanceof TabstackToolError) {
console.error(`Tabstack failed (${err.status ?? "no status"}): ${err.message}`);
}
}One error type instead of separate handling for transport, parsing, 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 you ship: automate_browser_task runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block your agent waiting on someone. It returns the final answer along with the data it extracted and the pages it visited. If you 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 in scope:
import { createTabstackMastraTools } from "@tabstack/mastra";
const tools = createTabstackMastraTools({ apiKey: process.env.MY_KEY });Same object shape, same tool names, so it drops into tools exactly like the default export.
Where to take it next
If I were starting a Mastra agent from scratch today, I would wire in research_question and extract_page_content and nothing else, then add the structured tools when a concrete use case asks for them. Most agents never need all five.
Render the citations. research_question returns sources alongside the answer, and showing them is what separates an assistant people trust from one they quietly double-check.
If part of this workload moves elsewhere, the tool names and inputs are identical in @tabstack/langchain, @tabstack/ai, and the Python langchain-tabstack. Your instructions carry over, because the model sees the same tools.
The package is on npm as @tabstack/mastra, and the full API reference is at docs.tabstack.ai.
Ready to build? Sign up for 10,000 free credits, no card required, and hand your Mastra agent its first live page. Full API in the docs.