# Tabstack: Full Content > Web browsing infrastructure for AI systems. Tabstack gives agents finished output from every web interaction: structured data, cited research answers, and completed browser tasks, each from a single API call. Built by Mozilla, with ephemeral processing, no model training on your data, and robots.txt compliance by default. This is the expanded version of https://tabstack.ai/llms.txt, with the agent quickstart, full product descriptions, every framework adapter, and complete blog post content. # Products ## Structured Data Extraction URL: https://tabstack.ai/structured-extraction Define a schema, pass a URL, and get back JSON that matches. No parsing code, no downstream LLM call, no extraction layer to maintain. Endpoints: /extract/json, /extract/markdown, and /generate/json. ## Autonomous Research URL: https://tabstack.ai/web-research Run a research agent with cited answers in a single API call. Live-web sourcing, multi-source synthesis, and SSE streaming, without building or maintaining a research pipeline. Endpoint: /research. ## Browser Automation URL: https://tabstack.ai/browser-automation Automate the web without running browser infrastructure. Navigation, clicks, forms, and multi-step flows on JS-heavy pages, in one API call. Endpoint: /automate. # Documentation - [Agent quickstart](https://tabstack.ai/agents.md): Everything an agent needs for a first API call: base URL, auth, and a runnable example for every endpoint, in clean markdown. - [OpenAPI description](https://tabstack.ai/openapi.json): Machine-readable OpenAPI 3.0.3 description of every endpoint, request body, and streaming event. Generate a client from it, or read the YAML twin at /openapi.yaml. - [Documentation](https://docs.tabstack.ai/): Guides, API reference, and SDK quickstarts. - [TypeScript SDK](https://docs.tabstack.ai/sdks/typescript/quickstart/): Install and call Tabstack from TypeScript. - [Python SDK](https://docs.tabstack.ai/sdks/python/quickstart/): Install and call Tabstack from Python. - [MCP](https://docs.tabstack.ai/getting-started/mcp/): Use Tabstack as an MCP server in any agent. # More - [Pricing](https://tabstack.ai/#pricing): Starter, Team, and Pro plans starting at $10/month, with 10,000 free credits to start. - [Pilo on GitHub](https://github.com/mozilla/pilo): Open-source browser engine behind Tabstack automation; self-hostable. The agent quickstart follows verbatim, also served on its own at https://tabstack.ai/agents.md. Its own `# Tabstack for Agents` heading opens the section; headings are left untouched so the two copies stay byte-identical. # Tabstack for Agents > Tabstack gives AI agents and apps finished output from the live web in a single API call. Extract structured data to a schema you define, convert pages to clean Markdown, run cited multi-source research, and automate browser tasks. Every call returns exactly what you asked for, ready to use. Built for developers shipping autonomous agents and those adding web interaction to an existing app or agent. Built by Mozilla, with ephemeral processing, no model training on your data, and robots.txt compliance by default. Version: 1.2 Last updated: 2026-08-04 Canonical URL: https://tabstack.ai/agents.md This document is everything an agent needs to make a first successful call, on one surface. Base URL, auth, install, and a runnable example for every endpoint are all below. For depth, follow the links at the end. ## Base URL and Auth - Base URL: `https://api.tabstack.ai/v1/` - Get an API key: https://console.tabstack.ai - Authenticate every request with an `Authorization: Bearer` header: `Authorization: Bearer $TABSTACK_API_KEY`. - Using an SDK? Set `TABSTACK_API_KEY` in the environment and it is read by default. - Generating a client instead of reading prose? The full OpenAPI 3.0.3 description is at https://tabstack.ai/openapi.json (YAML twin: https://tabstack.ai/openapi.yaml). Not using an SDK? Every endpoint is a plain HTTPS POST with a JSON body. Here is the full wire contract for a first call: ```bash curl https://api.tabstack.ai/v1/extract/json \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://store.example.com/products/aurora-trail-jacket", "json_schema": { "type": "object", "properties": { "name": { "type": "string" }, "price": { "type": "number" } } } }' ``` ```typescript import Tabstack from '@tabstack/sdk' // Reads TABSTACK_API_KEY from the environment by default. const client = new Tabstack() ``` ```python from tabstack import Tabstack # Reads TABSTACK_API_KEY from the environment by default. client = Tabstack() ``` ## Install ```bash # TypeScript npm i @tabstack/sdk # Python pip install tabstack ``` ## Framework Adapters Already inside a framework? Install the first-party adapter instead of calling the API directly. Every adapter exposes the same five tools under the same names, so behaviour matches this document whichever one you use. | Framework | Language | Package | Setup | | --- | --- | --- | --- | | LangChain | TypeScript, Python | `@tabstack/langchain` | https://tabstack.ai/integrations/langchain | | Vercel AI SDK | TypeScript | `@tabstack/ai` | https://tabstack.ai/integrations/vercel-ai-sdk | | OpenAI Agents | TypeScript | `@tabstack/openai-agents` | https://tabstack.ai/integrations/openai-agents | | Claude Agent | TypeScript | `@tabstack/claude-agent` | https://tabstack.ai/integrations/claude-agent | | Mastra | TypeScript | `@tabstack/mastra` | https://tabstack.ai/integrations/mastra | | LlamaIndex | TypeScript | `@tabstack/llamaindex` | https://tabstack.ai/integrations/llamaindex | | Pi Agent | TypeScript | `@tabstack/pi-agent` | https://tabstack.ai/integrations/pi-agent | | eve | TypeScript | `@tabstack/eve` | https://tabstack.ai/integrations/eve | | Hermes Agent | Python | `tabstack-hermes` | https://tabstack.ai/integrations/hermes-agent | Each setup page has a markdown twin at `https://tabstack.ai/integrations/.md` if you would rather read it the way you are reading this one. ## CLI Prefer to shell out? The `tabstack` CLI is a single static binary that covers every endpoint. Install it, then sign in once: ```bash # Install (macOS / Linux) curl -fsSL https://tabstack.ai/install.sh | sh # Sign in with your browser. This creates your account and provisions an API key in one step. tabstack auth login # First call tabstack extract json https://example.com \ --schema '{"type":"object","properties":{"title":{"type":"string"}}}' ``` `tabstack auth login` runs an OAuth 2.1 authorization code flow with PKCE: it opens https://console.tabstack.ai in your browser, signs you in, and stores an org-scoped API key for you. There is no separate signup step and no key to copy by hand. On a headless machine with no browser, set `TABSTACK_API_KEY` instead (the right credential for CI). API keys are scoped to one organisation; the CLI stores one key per org and `tabstack auth switch` chooses which one product commands send. ## Endpoints | Endpoint | SDK Method | Purpose | | --- | --- | --- | | `/extract/json` | `client.extract.json()` | Fetch a URL and return JSON matching a schema you define. | | `/extract/markdown` | `client.extract.markdown()` | Fetch a URL and return its content as clean Markdown. | | `/generate/json` | `client.generate.json()` | Fetch a URL, transform its content per instructions, return JSON matching a schema. | | `/automate` | `client.agent.automate()` | Run an interactive multi-step browser task from a plain-language description. Streams (SSE). | | `/research` | `client.agent.research()` | Answer a question with cited multi-source synthesis from the live web. Streams (SSE). | Every example below is runnable as written once `TABSTACK_API_KEY` is set. ### `/extract/json` Fetch a URL and return JSON matching a schema you define. ```typescript import Tabstack from '@tabstack/sdk' const client = new Tabstack() async function main() { try { const product = await client.extract.json({ url: 'https://store.example.com/products/aurora-trail-jacket', json_schema: { type: 'object', properties: { name: { type: 'string', description: 'Product name' }, price: { type: 'number', description: 'Current price' }, currency: { type: 'string', description: 'ISO 4217 currency code' }, in_stock: { type: 'boolean', description: 'Whether the product is available' }, sizes: { type: 'array', description: 'Per-size availability', items: { type: 'object', properties: { size: { type: 'string' }, in_stock: { type: 'boolean' }, }, }, }, }, }, }) // Unpopulated fields come back as null rather than failing the call. console.log(product) } catch (err) { console.error(err) } } main() ``` ```python from tabstack import Tabstack client = Tabstack() try: product = client.extract.json( url="https://store.example.com/products/aurora-trail-jacket", json_schema={ "type": "object", "properties": { "name": {"type": "string", "description": "Product name"}, "price": {"type": "number", "description": "Current price"}, "currency": {"type": "string", "description": "ISO 4217 currency code"}, "in_stock": {"type": "boolean", "description": "Whether the product is available"}, "sizes": { "type": "array", "description": "Per-size availability", "items": { "type": "object", "properties": { "size": {"type": "string"}, "in_stock": {"type": "boolean"}, }, }, }, }, }, ) print(product) except Exception as err: print(err) ``` ### `/extract/markdown` Fetch a URL and return its content as clean Markdown. ```typescript import Tabstack from '@tabstack/sdk' const client = new Tabstack() async function main() { try { const { content } = await client.extract.markdown({ url: 'https://example.com/blog/article', }) console.log(content) } catch (err) { console.error(err) } } main() ``` ```python from tabstack import Tabstack client = Tabstack() try: result = client.extract.markdown(url="https://example.com/blog/article") print(result.content) except Exception as err: print(err) ``` ### `/generate/json` Fetch a URL, transform its content per instructions, return JSON matching a schema. ```typescript import Tabstack from '@tabstack/sdk' const client = new Tabstack() async function main() { try { const data = await client.generate.json({ url: 'https://news.ycombinator.com', instructions: 'For each story, categorize it and write a one-sentence summary.', json_schema: { type: 'object', properties: { summaries: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, category: { type: 'string' }, summary: { type: 'string' }, }, }, }, }, }, }) console.log(data) } catch (err) { console.error(err) } } main() ``` ```python from tabstack import Tabstack client = Tabstack() try: data = client.generate.json( url="https://news.ycombinator.com", instructions="For each story, categorize it and write a one-sentence summary.", json_schema={ "type": "object", "properties": { "summaries": { "type": "array", "items": { "type": "object", "properties": { "title": {"type": "string"}, "category": {"type": "string"}, "summary": {"type": "string"}, }, }, } }, }, ) print(data) except Exception as err: print(err) ``` ### `/automate` Run an interactive multi-step browser task from a plain-language description. Streams (SSE). ```typescript import Tabstack from '@tabstack/sdk' const client = new Tabstack() async function main() { // /automate always streams Server-Sent Events. const stream = await client.agent.automate({ task: 'Find the Enterprise plan price and what is included', url: 'https://example.com/pricing', }) try { for await (const event of stream) { // Read the final answer from task:completed. if (event.event === 'task:completed') console.log(event.data.finalAnswer) if (event.event === 'error') console.error(event.data.error.message) if (event.event === 'done') break // termination: task:validated -> task:completed -> complete -> done } } catch (err) { console.error(err) } } main() ``` ```python from tabstack import Tabstack client = Tabstack() stream = client.agent.automate( task="Find the Enterprise plan price and what is included", url="https://example.com/pricing", ) for event in stream: # Read the final answer from task:completed, matching the Streaming Model section. if event.event == "task:completed": print(event.data["final_answer"]) if event.event == "error": print(event.data["error"]["message"]) if event.event == "done": break ``` ### `/research` Answer a question with cited multi-source synthesis from the live web. Streams (SSE). ```typescript import Tabstack from '@tabstack/sdk' const client = new Tabstack() async function main() { // /research always streams Server-Sent Events. const stream = await client.agent.research({ query: 'Compare pricing and free tiers across the top 3 analytics platforms', }) try { for await (const event of stream) { // The final cited report arrives on the complete event. if (event.event === 'complete') { console.log(event.data.report) break } if (event.event === 'error') { console.error(event.data.error.message) break } } } catch (err) { console.error(err) } } main() ``` ```python from tabstack import Tabstack client = Tabstack() # /research always streams Server-Sent Events. stream = client.agent.research( query="Compare pricing and free tiers across the top 3 analytics platforms", ) for event in stream: if event.event == "complete": print(event.data["report"]) break if event.event == "error": print(event.data["error"]["message"]) break ``` ## When to Use Which - **Extract.** Use /extract/json when the page has a known structure and you want it as JSON, or /extract/markdown to read a page as clean Markdown. - **Generate.** Use /generate/json to transform and summarize page content into a new shape, not just lift what is already there. - **Research.** Use /research for a cited answer synthesized across multiple live sources. - **Automate.** Use /automate for an interactive multi-step browser task: navigation, clicks, and forms on pages you do not control. ## Effort, Caching, Geotargeting - `effort`: `min` (fastest, 1-5s), `standard` (default, 3-15s), `max` (full browser rendering for JS-heavy sites, 15-60s). Available on `/extract/json`, `/extract/markdown`, and `/generate/json`. - `nocache`: set to `true` to bypass the cache and force fresh retrieval. - `geo_target`: `{ country: "US" }` using ISO 3166-1 alpha-2 codes. Supported on `/extract/json`, `/extract/markdown`, `/generate/json`, and `/automate`. Not supported on `/research`. Every request field, enum, and default is listed in the OpenAPI description at https://tabstack.ai/openapi.json. ## Streaming Model `/automate` and `/research` always stream over Server-Sent Events. Iterate the stream and switch on `event.event`. - `/automate` termination sequence: `task:validated` -> `task:completed` -> `complete` -> `done`. Read the final answer from the `task:completed` event (`event.data.finalAnswer`). Stop on `done`. - `/research` emits progress events through its phases, then a single `complete` event carrying the cited report (`event.data.report`). There is no `done` event for research. Stop on `complete`. - Both surface a top-level `error` event (`event.data.error.message`). - Every event name and payload is enumerated in the OpenAPI description: `v1.AutomateEvent` and `v1.ResearchEvent` are discriminated unions keyed on `event`. ## Error Codes and Retries | Status | Error Type | | --- | --- | | 400 | BadRequest | | 401 | Authentication | | 403 | PermissionDenied | | 404 | NotFound | | 409 | Conflict | | 422 | UnprocessableEntity | | 429 | RateLimit | | 500+ | InternalServer | The SDK automatically retries twice with exponential backoff on `408`, `409`, `429`, and `500+`. Everything else throws a typed error you catch. ## MCP Point an MCP client at the hosted endpoint, no install: ```json { "mcpServers": { "tabstack": { "type": "http", "url": "https://tabstack.stlmcp.com", "headers": { "x-tabstack-api-key": "${TABSTACK_API_KEY}" } } } } ``` The hosted endpoint exposes the non-streaming endpoints. It does not expose the streaming endpoints `/automate` and `/research`. ## More - OpenAPI 3.0.3 description (every field, enum, and event): https://tabstack.ai/openapi.json - Full documentation and API reference: https://docs.tabstack.ai/ - TypeScript SDK quickstart: https://docs.tabstack.ai/sdks/typescript/quickstart/ - Python SDK quickstart: https://docs.tabstack.ai/sdks/python/quickstart/ - MCP setup: https://docs.tabstack.ai/getting-started/mcp/ - Pilo, the open-source browser engine, on GitHub: https://github.com/mozilla/pilo # Framework Adapters ## LangChain adapter URL: https://tabstack.ai/integrations/langchain Markdown: https://tabstack.ai/integrations/langchain.md Language: TypeScript, Python Package: @tabstack/langchain Give your LangChain agents reliable web access. Schema-enforced extraction, multi-source research, and browser automation as native tools, in TypeScript and Python. ## Why LangChain + Tabstack LangChain's built-in loaders (`WebBaseLoader`, `PlaywrightURLLoader`) are fine for prototypes and brittle in production: unpredictable parsing, Playwright binaries to maintain, and behavior that drifts across releases. The Tabstack adapters replace them with a hosted API exposed as native LangChain tools: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. It's an SDK call, not a loader coupled to your LangChain version. Two officially-maintained packages track the same tool surface: - **TypeScript**: [`@tabstack/langchain`](https://www.npmjs.com/package/@tabstack/langchain) for LangChain.js - **Python**: [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/) for LangChain (Python) ## Quickstart Install the adapter and set your key: ```bash # TypeScript npm install @tabstack/langchain @langchain/core zod # Python pip install langchain-tabstack export TABSTACK_API_KEY="your-key-here" ``` Drop the tool set into an agent. TypeScript: ```ts import { createAgent } from "langchain"; import { tabstackTools } from "@tabstack/langchain"; const agent = createAgent({ model: "openai:gpt-4o", tools: tabstackTools, systemPrompt: "You are a research assistant with web intelligence tools.", }); const result = await agent.invoke({ messages: [{ role: "user", content: "What are Vercel's pricing plans?" }], }); console.log(result.messages.at(-1)?.content); ``` The same set in Python: ```python from langchain.agents import create_agent from langchain_tabstack import TABSTACK_TOOLS agent = create_agent("openai:gpt-4o", tools=TABSTACK_TOOLS) result = agent.invoke( {"messages": [{"role": "user", "content": "What are Vercel's pricing plans?"}]} ) print(result["messages"][-1].content) ``` `tabstackTools` / `TABSTACK_TOOLS` read `TABSTACK_API_KEY` from the environment. Every tool is also exported individually, so you can hand a single one to a chain without an agent. ## The tools | 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` | Synthesized 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. | ## Common use cases - Give a research agent cited, multi-source answers instead of a single fragile scrape. - Extract typed records (pricing, listings, docs) from any URL straight into your chain state. - Reshape a live page into derived JSON with `generate_structured_data`. - Run natural-language browser tasks from inside an agent step. ## Next steps - [TypeScript adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [Python adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-langchain-python) - [Get an API key](https://console.tabstack.ai/signup) --- ## Vercel AI SDK adapter URL: https://tabstack.ai/integrations/vercel-ai-sdk Markdown: https://tabstack.ai/integrations/vercel-ai-sdk.md Language: TypeScript Package: @tabstack/ai Give your Vercel AI SDK apps reliable web access. Schema-enforced extraction, research, generation, and browser automation as drop-in AI SDK tools. ## Why Vercel AI SDK + Tabstack Wiring web access into an AI SDK app usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai) replaces that with a hosted API exposed as typed `tool()` definitions: define the shape with Zod or JSON Schema and get that shape back, with JS-heavy pages rendered server-side. No Playwright, no binaries. One key covers extraction, research, generation, and automation. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/ai ai zod export TABSTACK_API_KEY="your-key-here" ``` Pass the tool set straight to `generateText` (or `streamText`): ```ts import { openai } from "@ai-sdk/openai"; import { generateText, stepCountIs } from "ai"; import { tabstackTools } from "@tabstack/ai"; const { text } = await generateText({ model: openai("gpt-4o"), tools: tabstackTools, stopWhen: stepCountIs(5), // let the model call a tool, then use the result prompt: "What are Vercel's pricing plans and how do they compare?", }); console.log(text); ``` `tabstackTools` reads `TABSTACK_API_KEY` from the environment and is a named object keyed by tool name. Pass all of them, or pick a subset: ```ts import { streamText, stepCountIs } from "ai"; import { tabstackTools } from "@tabstack/ai"; const result = streamText({ model: openai("gpt-4o"), tools: { research_question: tabstackTools.research_question, extract_page_content: tabstackTools.extract_page_content, }, stopWhen: stepCountIs(5), prompt: "Summarize the latest on quantum error correction, with sources.", }); for await (const chunk of result.textStream) process.stdout.write(chunk); ``` ## The tools | 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` | Synthesized 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. | ## Common use cases - Add live web research with citations to a chat app in a few lines. - Stream a model answer that pulls typed data from a URL mid-response. - Gate expensive automation behind a `stepCountIs` budget. - Ship only the tools a given surface needs by passing a subset. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/ai on npm](https://www.npmjs.com/package/@tabstack/ai) - [Get an API key](https://console.tabstack.ai/signup) --- ## OpenAI Agents adapter URL: https://tabstack.ai/integrations/openai-agents Markdown: https://tabstack.ai/integrations/openai-agents.md Language: TypeScript Package: @tabstack/openai-agents Give your OpenAI Agents SDK agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as native Agents SDK tools. ## Why the OpenAI Agents SDK + Tabstack Wiring web access into an agent usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/openai-agents`](https://www.npmjs.com/package/@tabstack/openai-agents) replaces that with a hosted API exposed as native Agents SDK tools: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. Tool names stay in lockstep with the `@tabstack/langchain`, `@tabstack/ai`, `@tabstack/eve`, and Python `langchain-tabstack` packages. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/openai-agents @openai/agents zod export TABSTACK_API_KEY="your-key-here" ``` `@openai/agents` (v0.13 or later) and `zod` (v4) are peer dependencies. `tabstackTools` is an array of ready-to-use tools, so hand it to an `Agent` and run it: ```ts import { Agent, run } from "@openai/agents"; import { tabstackTools } from "@tabstack/openai-agents"; const agent = new Agent({ name: "Research assistant", instructions: "You are a research assistant with web intelligence tools. Use research_question for open " + "questions that need multiple sources, extract_page_content to read a specific URL as " + "markdown, and the extract tools to pull structured fields from a page. Always cite sources.", tools: tabstackTools, }); const result = await run(agent, "What are Vercel's pricing plans, with sources?"); console.log(result.finalOutput); ``` The tools resolve `TABSTACK_API_KEY` lazily on first call, so importing the package never requires a key. For a custom key, base URL, or a shared client, build the tools explicitly: ```ts import { createTabstackOpenAIAgentsTools } from "@tabstack/openai-agents"; const tools = createTabstackOpenAIAgentsTools({ apiKey: process.env.MY_KEY }); // or pass an SDK client you already have: createTabstackOpenAIAgentsTools({ client }) ``` ## The tools | Tool name | Export | What it does | | --- | --- | --- | | `extract_structured_data` | `extractStructuredDataTool` | Pull specific fields from a URL into a JSON shape you define. | | `extract_page_content` | `extractPageContentTool` | Fetch a page as clean markdown. | | `research_question` | `researchQuestionTool` | Synthesized answer with cited sources across multiple pages. | | `generate_structured_data` | `generateStructuredDataTool` | Fetch a page, then AI-transform it into derived or reshaped JSON. | | `automate_browser_task` | `automateBrowserTaskTool` | Run a multi-step, natural-language browser task. | ## Strict mode and schemas The Agents SDK forces **strict** JSON Schema mode whenever a tool's `parameters` is a Zod schema, and passing `strict: false` alongside a Zod schema throws. Strict mode cannot represent two constructs the shared core schemas rely on: `.optional()` fields, since strict mode requires every property to appear in `required`, and `automate_browser_task`'s open `data` object, which needs a schema-valued `additionalProperties` that strict mode forbids. So the adapter converts each core Zod schema to a JSON Schema and registers the tools with `strict: false`. The model still sees full field descriptions, and every call is validated against the core Zod schema inside `execute` before the request runs, so malformed model output fails fast with a clear error. ## Good to know - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form input, so it never blocks. It returns the final answer plus the data it extracted and the pages it visited. - Failed calls throw `TabstackToolError`, a normalized message plus an HTTP `status` for API errors. The Agents SDK surfaces tool errors to the model as a tool result, so a failing call does not abort the run by default. - **Requires Zod 4.** The SDK depends on it, and the adapter uses Zod 4's native `z.toJSONSchema` to advertise each tool's parameters. ## Common use cases - Give a research agent cited, multi-source answers instead of a single fetch. - Pull structured fields off a page without writing parsing code. - Let an agent drive a multi-step browser task in natural language. - Keep tool names consistent across your TypeScript and Python agents. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/openai-agents on npm](https://www.npmjs.com/package/@tabstack/openai-agents) - [OpenAI Agents SDK for TypeScript](https://openai.github.io/openai-agents-js/) - [Get an API key](https://console.tabstack.ai/signup) --- ## Claude Agent adapter URL: https://tabstack.ai/integrations/claude-agent Markdown: https://tabstack.ai/integrations/claude-agent.md Language: TypeScript Package: @tabstack/claude-agent Give your Claude Agent SDK agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as an in-process MCP server. ## Why the Claude Agent SDK + Tabstack Wiring web access into an agent usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/claude-agent`](https://www.npmjs.com/package/@tabstack/claude-agent) replaces that with a hosted API exposed as an in-process MCP server: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. One call builds a ready-to-use server for `query()`, with no separate process to run. ## Quickstart Install the adapter and set your keys: ```bash npm install @tabstack/claude-agent @anthropic-ai/claude-agent-sdk zod export TABSTACK_API_KEY="your-tabstack-key" export ANTHROPIC_API_KEY="your-anthropic-key" ``` `@anthropic-ai/claude-agent-sdk` and `zod` are peer dependencies. The Claude Agent SDK requires **Zod 4**, so this package's `zod` peer is `^4.0.0`. `tabstackServer` is a ready-built in-process MCP server, and `tabstackAllowedTools` pre-approves every Tabstack tool. Wire both into a `query()` call: ```ts import { query } from "@anthropic-ai/claude-agent-sdk"; import { tabstackServer, tabstackAllowedTools, tabstackMcpServerName } from "@tabstack/claude-agent"; for await (const message of query({ prompt: "What are Vercel's current pricing plans, with sources?", options: { mcpServers: { [tabstackMcpServerName]: tabstackServer }, allowedTools: tabstackAllowedTools, }, })) { if (message.type === "result" && message.subtype === "success") { console.log(message.result); } } ``` The server resolves `TABSTACK_API_KEY` lazily on first tool call, so importing the package never requires a key. For a custom key, base URL, or a shared client, build the server explicitly: ```ts import { createTabstackClaudeAgentServer } from "@tabstack/claude-agent"; const server = createTabstackClaudeAgentServer({ apiKey: process.env.MY_KEY }); // or pass an SDK client you already have: createTabstackClaudeAgentServer({ client }) ``` Assembling the server yourself? `createTabstackClaudeAgentTools(config)` returns the raw tool array to pass to your own `createSdkMcpServer({ name, version, tools })`. ## The tools | Tool name | 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` | Synthesized 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. | Claude sees each tool under its fully qualified MCP name, `mcp__tabstack__`. The names stay in lockstep with the `@tabstack/langchain`, `@tabstack/ai`, `@tabstack/eve`, and Python `langchain-tabstack` packages. ## Good to know - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form input, so it never blocks. It returns the final answer plus the data it extracted and the pages it visited. - Failed calls normalize to a `TabstackToolError` and return as an MCP error result (`isError: true`), so Claude reads a useful message and can retry or explain rather than seeing a raw exception. - Each tool's input schema is the core Zod schema's raw shape, so the Agent SDK validates the model's arguments before your handler runs. ## Common use cases - Give a Claude agent cited, multi-source research without running a browser. - Pull structured fields off a page into a shape you define. - Add web access to an existing `query()` call with two imports. - Keep tool names consistent across your TypeScript and Python agents. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/claude-agent on npm](https://www.npmjs.com/package/@tabstack/claude-agent) - [Claude Agent SDK docs](https://code.claude.com/docs/en/agent-sdk/overview) - [Get an API key](https://console.tabstack.ai/signup) --- ## Mastra adapter URL: https://tabstack.ai/integrations/mastra Markdown: https://tabstack.ai/integrations/mastra.md Language: TypeScript Package: @tabstack/mastra Give your Mastra agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as native Mastra tools. ## Why Mastra + Tabstack Wiring web access into an agent usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/mastra`](https://www.npmjs.com/package/@tabstack/mastra) replaces that with a hosted API exposed as typed `createTool` definitions: define the shape with Zod and get that shape back, with JS-heavy pages rendered server-side and one key for extraction, research, generation, and automation. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/mastra @mastra/core zod export TABSTACK_API_KEY="your-key-here" ``` `@mastra/core` (v1 or later) and `zod` are peer dependencies, so your app's single instance of each is shared. `tabstackTools` is a named object keyed by tool name, ready to spread into an `Agent`: ```ts 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); ``` Want a subset? Every tool is exported individually: ```ts 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, }, }); ``` For a custom key, base URL, or a shared client, build the tools explicitly: ```ts import { createTabstackMastraTools } from "@tabstack/mastra"; const tools = createTabstackMastraTools({ apiKey: process.env.MY_KEY }); // or pass an SDK client you already have: createTabstackMastraTools({ client }) ``` ## The tools | 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` | Synthesized 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 the inputs in, but the shapes are worth knowing: `extract_structured_data` takes `url` and `json_schema_json` (a JSON-encoded JSON Schema string), `extract_page_content` takes `url`, `research_question` takes `query`, `generate_structured_data` takes `url`, `instructions`, and `json_schema_json`, and `automate_browser_task` takes `task` plus optional `url`, `guardrails`, `data`, `country`, `max_iterations`, and `max_validation_attempts`. ## Optional inputs The model can pass these for finer control, and they are sent to Tabstack only when present: - `extract_structured_data`, `extract_page_content`, `generate_structured_data`: `effort` (`"min"`, `"standard"`, or `"max"`, where `"max"` suits JS-heavy pages), `nocache` to bypass the cache, and `country` as an ISO 3166-1 alpha-2 code for geotargeted fetches. - `research_question`: `mode` (`"fast"` or `"balanced"`) and `nocache`. ## Good to know - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form input, so it never blocks. It returns the final answer plus the data it extracted and the pages it visited. - Failed calls throw `TabstackToolError`, a normalized message plus an HTTP `status` for API errors. Mastra surfaces this in the tool result. - Tool names, descriptions, and inputs match the `@tabstack/langchain` and Python `langchain-tabstack` packages, so behavior is consistent across frameworks and languages. ## Common use cases - Give a Mastra agent cited, multi-source answers from the live web. - Pull structured fields off a page into a Zod-defined shape. - Geotarget a fetch by country to see region-specific pricing or availability. - Keep tool names consistent across your TypeScript and Python agents. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/mastra on npm](https://www.npmjs.com/package/@tabstack/mastra) - [Mastra docs](https://mastra.ai) - [Get an API key](https://console.tabstack.ai/signup) --- ## LlamaIndex adapter URL: https://tabstack.ai/integrations/llamaindex Markdown: https://tabstack.ai/integrations/llamaindex.md Language: TypeScript Package: @tabstack/llamaindex Give your LlamaIndex.TS agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as native LlamaIndex tools. ## Why LlamaIndex + Tabstack Wiring web access into an agent usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/llamaindex`](https://www.npmjs.com/package/@tabstack/llamaindex) replaces that with a hosted API exposed as native LlamaIndex tools: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/llamaindex llamaindex zod export TABSTACK_API_KEY="your-key-here" ``` `llamaindex` (v0.12 or later) and `zod` are peer dependencies. The tools come pre-built as an array, so pass them straight to an agent: ```ts 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); ``` The tools resolve `TABSTACK_API_KEY` lazily on first call, so importing the package never requires a key. For a custom key, base URL, or a shared client, build the tools explicitly: ```ts import { createTabstackLlamaindexTools } from "@tabstack/llamaindex"; const tools = createTabstackLlamaindexTools({ apiKey: process.env.MY_KEY }); // or pass an SDK client you already have: createTabstackLlamaindexTools({ client }) ``` ## The 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` | Synthesized 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 individual tools for a subset, or use the `tabstackTools` array for all of them. The names stay in lockstep with the `@tabstack/langchain`, `@tabstack/ai`, `@tabstack/eve`, and Python `langchain-tabstack` packages. ## Good to know - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form input, so it never blocks. It returns the final answer plus the data it extracted and the pages it visited. - Failed calls throw `TabstackToolError`, a normalized message plus an HTTP `status` for API errors. LlamaIndex surfaces this in the tool result. - Inputs are validated against the core Zod schema, since LlamaIndex passes the schema straight through as the tool's `parameters`, so malformed model output is caught before the call runs. - **Zod 3 and Zod 4 both work.** LlamaIndex's `tool()` is Zod-native and accepts either, and your app's own Zod version is used. ## Common use cases - Replace a brittle web loader with a hosted API that returns the shape you asked for. - Give a LlamaIndex agent cited, multi-source research over live pages. - Read a specific URL as clean markdown for indexing or summarisation. - Keep tool names consistent across your TypeScript and Python agents. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/llamaindex on npm](https://www.npmjs.com/package/@tabstack/llamaindex) - [LlamaIndex.TS docs](https://developers.llamaindex.ai/typescript) - [Get an API key](https://console.tabstack.ai/signup) --- ## Pi Agent adapter URL: https://tabstack.ai/integrations/pi-agent Markdown: https://tabstack.ai/integrations/pi-agent.md Language: TypeScript Package: @tabstack/pi-agent Give the Pi coding agent reliable web access. Schema-enforced extraction, research, generation, and browser automation as native Pi tools. ## Why Pi + Tabstack Wiring web access into a coding agent usually means hand-rolled fetches, HTML parsing, and prompt gymnastics to coax structured data out of messy text. [`@tabstack/pi-agent`](https://www.npmjs.com/package/@tabstack/pi-agent) replaces that with a hosted API registered as native Pi tools through a Pi extension: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/pi-agent @earendil-works/pi-coding-agent typebox zod export TABSTACK_API_KEY="your-key-here" ``` `@earendil-works/pi-coding-agent` (v0.82 or later), `typebox` (v1), and `zod` (v4) are peer dependencies. A Pi extension is a `.ts` file with a default export that Pi calls with its extension API. Re-export this package's default extension and Pi registers all five tools: ```ts // tabstack.ts export { default } from "@tabstack/pi-agent"; ``` Then load it with the `pi` CLI: ```bash pi -e ./tabstack.ts ``` Pi also auto-discovers extensions dropped into `~/.pi/agent/extensions/*.ts` (global, every project) or `.pi/extensions/*.ts` (project-local, checked into the repo), with no `-e` flag needed. The tools resolve `TABSTACK_API_KEY` lazily on first call, so loading the extension never requires a key. ## Registering a subset You often do not want every tool. A coding agent that should read and research but never drive a browser can register just those two, in the order given: ```ts // tabstack.ts import { createTabstackPiExtension } from "@tabstack/pi-agent"; // Read-and-research only, no browser automation. export default createTabstackPiExtension({ tools: ["extract_page_content", "research_question"], }); ``` For full control, register individual tools in a hand-written extension. Every tool is exported as a Pi `ToolDefinition`, and `createTabstackPiTools(config)` returns them keyed by name for a custom client: ```ts import { extractPageContentTool, researchQuestionTool } from "@tabstack/pi-agent"; export default function (pi) { pi.registerTool(extractPageContentTool); pi.registerTool(researchQuestionTool); // ...and your own tools alongside them. } ``` ## The tools | Tool name | Label | What it does | | --- | --- | --- | | `extract_structured_data` | Extract Structured Data | Pull specific fields from a URL into a JSON shape you define. | | `extract_page_content` | Extract Page Content | Fetch a page as clean markdown. | | `research_question` | Research Question | Synthesized answer with cited sources across multiple pages. | | `generate_structured_data` | Generate Structured Data | Fetch a page, then AI-transform it into derived or reshaped JSON. | | `automate_browser_task` | Automate Browser Task | Run a multi-step, natural-language browser task. | The names stay in lockstep with the `@tabstack/langchain`, `@tabstack/openai-agents`, `@tabstack/eve`, and Python `langchain-tabstack` packages. ## Good to know - **Cancellation works.** Pi's abort signal is threaded into the Tabstack request, so cancelling a tool call, for example a long `research_question` or `automate_browser_task`, aborts the request and stops billing rather than just the agent loop. - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form input, so it never blocks. It returns the final answer plus the data it extracted and the pages it visited. - Failed calls throw `TabstackToolError`, a normalized message plus an HTTP `status` for API errors. Pi surfaces the failure to the model as a tool result rather than aborting the session. - **Requires Zod 4.** The adapter uses Zod 4's native `z.toJSONSchema` to derive each tool's Typebox parameters. ## Common use cases - Give a coding agent cited research without leaving the terminal. - Read a page as clean markdown mid-task, with no browser to install. - Register a read-only subset so the agent can research but never act. - Check the extension into `.pi/extensions/` so the whole team gets the tools. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/pi-agent on npm](https://www.npmjs.com/package/@tabstack/pi-agent) - [Pi extensions](https://pi.dev/docs/latest/extensions) - [Get an API key](https://console.tabstack.ai/signup) --- ## eve adapter URL: https://tabstack.ai/integrations/eve Markdown: https://tabstack.ai/integrations/eve.md Language: TypeScript Package: @tabstack/eve Give your eve agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as native, path-derived eve tools. ## Why eve + Tabstack eve derives each tool's name from its filename: add a file under `agent/tools/` and the model can call it. [`@tabstack/eve`](https://www.npmjs.com/package/@tabstack/eve) ships pre-built `defineTool` definitions you re-export into that directory: schema-enforced output, server-side rendering of JS-heavy pages, and one key for extraction, research, generation, and automation. Tool names stay in lockstep with the `@tabstack/langchain`, `@tabstack/ai`, and Python `langchain-tabstack` packages. ## Quickstart Install the adapter and set your key: ```bash npm install @tabstack/eve eve zod export TABSTACK_API_KEY="your-key-here" ``` Re-export a Tabstack tool as the **default export** of a file named after the tool: ```ts // agent/tools/research_question.ts export { researchQuestionTool as default } from "@tabstack/eve"; ``` ```ts // agent/tools/extract_page_content.ts export { extractPageContentTool as default } from "@tabstack/eve"; ``` That's it. The filename becomes the tool name, and the tools resolve `TABSTACK_API_KEY` lazily on first call. For a custom key, base URL, or a shared client, build them explicitly: ```ts // agent/tools/research_question.ts import { createTabstackEveTools } from "@tabstack/eve"; const tools = createTabstackEveTools({ apiKey: process.env.MY_KEY }); export default tools.research_question; ``` ## The tools | File name | What it does | | --- | --- | | `extract_structured_data.ts` | Pull specific fields from a URL into a JSON shape you define. | | `extract_page_content.ts` | Fetch a page as clean markdown. | | `research_question.ts` | Synthesized answer with cited sources across multiple pages. | | `generate_structured_data.ts` | Fetch a page, then AI-transform it into derived or reshaped JSON. | | `automate_browser_task.ts` | Run a multi-step, natural-language browser task. | The filename must match the tool name, because eve's identity is path-derived. ## Common use cases - Add web research to an eve agent by dropping one file into `agent/tools/`. - Expose only the capabilities a given agent needs, one file each. - Share a single configured Tabstack client across every tool. - Keep tool names consistent across your TypeScript and Python agents. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-integrations-typescript) - [@tabstack/eve on npm](https://www.npmjs.com/package/@tabstack/eve) - [Get an API key](https://console.tabstack.ai/signup) --- ## Hermes Agent adapter URL: https://tabstack.ai/integrations/hermes-agent Markdown: https://tabstack.ai/integrations/hermes-agent.md Language: Python Package: tabstack-hermes Give your Hermes agents reliable web access. Schema-enforced extraction, research, generation, and browser automation as a native Hermes toolset. ## Why Hermes + Tabstack Hermes ships with web search and extraction backends already. [`tabstack-hermes`](https://pypi.org/project/tabstack-hermes/) adds the parts they do not cover: schema-enforced extraction where you define the JSON shape and get that shape back, JS-heavy pages rendered server-side with no browser to install or patch on the box Hermes lives on, research that returns a synthesized answer plus its sources, and multi-step browser automation driven in natural language. Five tools land in a single `tabstack` toolset, and the plugin also registers a `tabstack` web provider so Hermes' built-in `web_extract` can fetch through Tabstack without the model learning a new tool. ## Quickstart Requires Python 3.11 or newer, the same floor as `hermes-agent`. ```bash pip install tabstack-hermes hermes plugins enable tabstack hermes env set TABSTACK_API_KEY ``` Plugins are opt-in: pip puts the plugin on Hermes' discovery path, and `hermes plugins enable tabstack` lets it load. To install from git instead of PyPI: ```bash hermes plugins install Mozilla-Ocho/tabstack-hermes/tabstack_hermes --enable ``` The `/tabstack_hermes` suffix is the subdirectory holding the plugin, where `plugin.yaml` sits next to the code. Either path lands at `~/.hermes/plugins/tabstack/`. Confirm it loaded: ```bash hermes plugins list # tabstack, enabled, 5 tools hermes tools # the tabstack toolset ``` ## The tools | Tool name | 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` | Synthesized 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. | All five land in the `tabstack` toolset, so they enable and disable as a unit in `hermes tools`. Names, descriptions, and inputs match the `langchain-tabstack` package and the TypeScript adapters, so a Tabstack tool behaves the same whichever framework calls it. Tools return a JSON string; `extract_page_content` returns markdown directly. Optional inputs are passed only when the model provides them, so omitting them keeps Tabstack's defaults: `effort` (`"min"` | `"standard"` | `"max"`, use `"max"` for JS-heavy pages), `nocache`, and `country` on the extract and generate tools; `mode` (`"fast"` | `"balanced"`) and `nocache` on `research_question`; `data`, `country`, `max_iterations`, and `max_validation_attempts` on `automate_browser_task`. ## Tabstack as the web extract backend Point Hermes' own `web_extract` tool at Tabstack and the model keeps calling the tool it already knows: ```yaml # ~/.hermes/config.yaml web: extract_backend: "tabstack" ``` Extract only. Tabstack has no ranked search endpoint, so `supports_search` is `False` and `web_search` keeps using whichever backend you already have. For synthesis across sources, reach for the `research_question` tool instead. Behavior worth knowing: - URLs come back in the order they went in, because `web_extract` re-interleaves them with the ones it rejected as unsafe. - A batch fans out 5 URLs at a time with a 60s ceiling per URL. One failing URL returns an `error` entry for that URL and does not fail the batch. - `format="html"` is ignored: Tabstack returns markdown. ## Configuration | Variable | Purpose | | --- | --- | | `TABSTACK_API_KEY` | Required. Get one at [console.tabstack.ai](https://console.tabstack.ai/signup). | | `TABSTACK_BASE_URL` | Optional. Point the SDK at a different API base URL. | Keys are read through Hermes' config layer first (`~/.hermes/.env` via `hermes env set`), then the process environment, so credentials work in gateway sessions, delegated children, and subprocess agent runs where the variable was never exported. Without a key the plugin still loads and the tools still appear in `hermes tools`, but a `check_fn` keeps them out of dispatch until a key is set. The SDK is imported and the client built on the first tool call, so a session that never calls Tabstack pays no cold-start cost. Handlers never raise. A failure returns JSON the model can act on, with the HTTP status when the API supplied one: ```json {"error": "Extract failed for https://example.com", "status": 429} ``` ## Common use cases - Give a Hermes agent cited research in one call instead of a search-then-read loop. - Extract a fixed JSON shape from a page and hand it straight to the next step. - Route Hermes' existing `web_extract` through Tabstack without touching prompts. - Drive a multi-step web task (navigate, fill, extract) with no browser on the host. ## Next steps - [Adapter on GitHub](https://github.com/Mozilla-Ocho/tabstack-hermes) - [tabstack-hermes on PyPI](https://pypi.org/project/tabstack-hermes/) - [Hermes plugin docs](https://hermes-agent.nousresearch.com/docs/user-guide/features/plugins) - [Get an API key](https://console.tabstack.ai/signup) # Blog ## Give a LlamaIndex.TS agent live web access URL: https://tabstack.ai/blog/llamaindex-ts-web-access-tabstack Date: 2026-08-06 A hands-on walkthrough: add five live web tools to a LlamaIndex.TS agent alongside whatever you have indexed, read pages, pull typed fields, and research questions across sources on demand. 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) - `llamaindex` 0.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](https://console.tabstack.ai) ```bash npm install @tabstack/llamaindex llamaindex zod @llamaindex/workflow @llamaindex/openai ``` ```bash export 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 ```ts 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: ```ts 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 string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `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: ```ts 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`](https://www.npmjs.com/package/@tabstack/langchain), [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai), [`@tabstack/eve`](https://www.npmjs.com/package/@tabstack/eve), and the Python [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/), so prompts carry over if this workload moves. The package is on npm as [`@tabstack/llamaindex`](https://www.npmjs.com/package/@tabstack/llamaindex), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and query your first live page alongside your index. Full API in the [docs](https://docs.tabstack.ai). --- ## Add web tools to an OpenAI Agents SDK agent with Tabstack URL: https://tabstack.ai/blog/openai-agents-sdk-web-tools-tabstack Date: 2026-08-05 A hands-on walkthrough: hand Tabstack's tools to an OpenAI Agents SDK agent, and understand why the adapter registers them with strict mode off without losing input validation. The OpenAI Agents SDK has a clean model. You build an `Agent`, you give it tools, you call `run`. This tutorial gives it the tools nearly every agent eventually needs: read a page, pull typed fields from it, research across sources, and drive a browser when the data sits behind an interaction. It also spends a section on strict mode, because that is the one place where this adapter had to make a real decision. Why are these tools not strict, when the SDK pushes you so hard towards it? The answer is more interesting than it sounds, and knowing it will save you from filing a bug that is not a bug. ## What you need - A Node project (the package ships dual ESM and CJS builds) - `@openai/agents` v0.13 or later - Zod 4 - A Tabstack API key from [console.tabstack.ai](https://console.tabstack.ai) ```bash npm install @tabstack/openai-agents @openai/agents zod ``` ```bash export TABSTACK_API_KEY="your-key-here" ``` Zod 4 is required, not preferred. The Agents SDK depends on it, and the adapter uses Zod 4's native `z.toJSONSchema` to advertise each tool's parameters. If you are still on Zod 3, that upgrade comes first. ## Step 1: an agent with web access ```ts import { Agent, run } from "@openai/agents"; import { tabstackTools } from "@tabstack/openai-agents"; const agent = new Agent({ name: "Research assistant", instructions: "You are a research assistant with web intelligence tools. Use research_question for open " + "questions that need multiple sources, extract_page_content to read a specific URL as " + "markdown, and the extract tools to pull structured fields from a page. Always cite sources.", tools: tabstackTools, }); const result = await run(agent, "What are Vercel's pricing plans, with sources?"); console.log(result.finalOutput); ``` `tabstackTools` is an array of ready-to-use tools, which is exactly what `Agent` wants for `tools`. No mapping step, no wrapper. The tools resolve `TABSTACK_API_KEY` lazily on first call, so importing the package never requires a key to be set. Your test suite and your build steps stay clean. ## Step 2: the five tools | Tool name | Export | What it does | | --- | --- | --- | | `extract_structured_data` | `extractStructuredDataTool` | Pull specific fields from a URL into a JSON shape you define | | `extract_page_content` | `extractPageContentTool` | Fetch a page as clean markdown | | `research_question` | `researchQuestionTool` | Synthesised answer with cited sources across multiple pages | | `generate_structured_data` | `generateStructuredDataTool` | Fetch a page, then AI-transform it into derived or reshaped JSON | | `automate_browser_task` | `automateBrowserTaskTool` | Run a multi-step, natural language browser task | Import them individually when you want a subset. I think building your own array is worth the thirty seconds it costs you. Every tool you pass is another option the model weighs on every step, and another way for it to choose wrong. An agent that only reads and researches should be handed two tools, not five. The inputs, which the model fills in: - `extract_structured_data`: `url`, `json_schema_json`, a JSON encoded JSON Schema string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `guardrails`, `data`, `country`, `max_iterations`, `max_validation_attempts` Extract versus generate is the distinction that trips people up. 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 summary nobody wrote down. If the field exists in the HTML, extract it. If you are asking for judgement, generate it. ## Step 3: why these tools are not strict Open the tool definitions and you will find they are registered with `strict: false`. That is deliberate, and it is worth understanding rather than treating as a wart. The Agents SDK forces strict JSON Schema mode whenever a tool's `parameters` is a Zod schema. Passing `strict: false` alongside a Zod schema throws, so it is not a knob you can simply turn. And strict mode cannot represent two constructs the shared core schemas rely on: Optional fields. Strict mode requires every property to appear in `required`, which rules out `.optional()`. All those production flags in the next section are optional by design, because sending them when you do not mean them would override Tabstack's defaults. Open objects. `automate_browser_task` accepts a `data` object for form-filling context, whose keys you cannot know ahead of time. Strict mode forbids the schema-valued `additionalProperties` that an open record needs. So the adapter converts each core Zod schema to a JSON Schema and registers the tools with `strict: false`. Two things you do not lose in the trade. The model still sees full field descriptions, so tool selection and argument quality are unaffected. And every call is validated against the core Zod schema inside `execute` before the request goes out, so malformed model output still fails fast with a clear error rather than reaching the API. The shared core schemas are never modified to accommodate this. That is what keeps behaviour identical across every framework and both languages. ## Step 4: the flags that matter in production The model can pass these itself, and they are only sent to Tabstack when present, so omitting them 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 almost always a client-rendered app and `effort: "max"` is the fix. That one argument replaces installing Playwright, shipping a browser binary, and keeping both patched. Nothing runs on your side. The tidiest way to apply these is through instructions rather than 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. The Agents SDK catches tool errors and surfaces them to the model as a tool result, so a failing call does not abort the run by default. The model sees the failure, and can retry with different arguments or explain the problem. That is usually what you want. A dead link in step two of a five-step research task should not kill the run. 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 waiting on a person. It returns the final answer plus 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: ```ts import { createTabstackOpenAIAgentsTools } from "@tabstack/openai-agents"; const tools = createTabstackOpenAIAgentsTools({ apiKey: process.env.MY_KEY }); ``` Same array shape, same tool names, so it drops into `tools` exactly like the default export. ## Where to take it next Narrow the toolset per agent. If you are using handoffs, a specialist agent should carry only the tools its speciality needs. A pricing researcher wants `extract_structured_data`. A general assistant wants `research_question`. Very few agents want all five. Show the citations. `research_question` returns sources alongside its answer, and surfacing them is what separates an assistant people trust from one they quietly verify. The tool names are identical in [`@tabstack/langchain`](https://www.npmjs.com/package/@tabstack/langchain), [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai), [`@tabstack/eve`](https://www.npmjs.com/package/@tabstack/eve), and the Python [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/), so your instructions carry over if this workload moves. The package is on npm as [`@tabstack/openai-agents`](https://www.npmjs.com/package/@tabstack/openai-agents), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and hand your agent its first typed extraction. Full API in the [docs](https://docs.tabstack.ai). --- ## Give a Claude Agent SDK app web access URL: https://tabstack.ai/blog/claude-agent-sdk-web-access-tabstack Date: 2026-08-04 A hands-on walkthrough: wire Tabstack's ready-built in-process MCP server into a query() call, understand the mcp__tabstack__ namespacing, and give Claude web access with no separate server to run. Every other integration in this series hands your framework an array or an object of tools. This one is different, because the Claude Agent SDK expects tools to arrive as an MCP server. That sounds like more work, and I assumed it would be. It is actually less, because the package ships a server already built. You import it, name it in your `query()` options, and Claude has web access. There is no separate process to run and no MCP config file to maintain. ## What you need - A Node project (the package ships dual ESM and CJS builds) - A Tabstack API key from [console.tabstack.ai](https://console.tabstack.ai) - An Anthropic API key ```bash npm install @tabstack/claude-agent @anthropic-ai/claude-agent-sdk zod ``` ```bash export TABSTACK_API_KEY="your-tabstack-key" export ANTHROPIC_API_KEY="your-anthropic-key" ``` One version constraint to note. The Claude Agent SDK depends on Zod 4, so this package's `zod` peer dependency is `^4.0.0`. Unlike the eve and LlamaIndex adapters, Zod 3 is not an option here. If your app is still on Zod 3, that upgrade is a prerequisite rather than a nice-to-have. ## Step 1: wire in the server ```ts import { query } from "@anthropic-ai/claude-agent-sdk"; import { tabstackServer, tabstackAllowedTools, tabstackMcpServerName } from "@tabstack/claude-agent"; for await (const message of query({ prompt: "What are Vercel's current pricing plans, with sources?", options: { mcpServers: { [tabstackMcpServerName]: tabstackServer }, allowedTools: tabstackAllowedTools, }, })) { if (message.type === "result" && message.subtype === "success") { console.log(message.result); } } ``` Three imports, and Claude can read the web. Note the `for await`. `query()` returns a lazy async generator rather than a promise, so nothing runs until you iterate it. Writing `await query({ ... })` typechecks fine and does absolutely nothing, which is a quiet half hour to lose. `tabstackServer` is a ready-built in-process MCP server holding all five tools. In-process is the important word. It runs inside your Node process, so there is no subprocess to spawn, no port to bind, and no server lifecycle to manage. If your only previous experience of MCP was editing a JSON config and restarting something, this is a pleasant surprise. `tabstackAllowedTools` pre-approves every Tabstack tool. The Claude Agent SDK gates tool use by permission, so without this the tools exist but Claude cannot call them. It is the line people forget, and the symptom is an agent that talks about wanting to look something up and then does not. `tabstackMcpServerName` is the string `"tabstack"`. Use the export rather than typing the literal, because the name becomes part of how Claude addresses each tool. The server resolves `TABSTACK_API_KEY` lazily on first tool call, so importing the package never requires a key to be set. ## Step 2: understand what Claude actually sees Claude does not see `research_question`. It sees `mcp__tabstack__research_question`. MCP tools are namespaced by their server, so every tool arrives fully qualified as `mcp__tabstack__`. This matters in two places. When you read a transcript and want to know which tool ran, that prefix is what you are scanning for. And when you write instructions referring to a tool by name, the unqualified name is what Claude maps onto, but the qualified name is what appears in permission prompts and logs. Underneath the prefix, the names are the same everywhere: | Tool name | 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 | Those names are shared with the LangChain, Vercel AI SDK, eve, Mastra, and Python packages, so a prompt written for one carries over. The 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 summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it. ## Step 3: the inputs the model fills in You will read these in transcripts: - `extract_structured_data`: `url`, `json_schema_json`, a JSON encoded JSON Schema string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `guardrails`, `data`, `country`, `max_iterations`, `max_validation_attempts` Each tool's input schema is the core Zod schema's raw shape, which means the Claude Agent SDK validates Claude's arguments before the handler runs. Malformed tool calls fail immediately with a clear message rather than travelling to the API and coming back rejected. There are optional inputs too, and each is accepted by a different subset of the tools: - `effort`, as `"min"`, `"standard"`, or `"max"`, on the three fetch-based tools - `nocache` to bypass the cache, on the three fetch-based tools and `research_question` - `country`, an ISO 3166-1 alpha-2 code, for geotargeted fetches, on the three fetch-based tools and `automate_browser_task` - `mode`, as `"fast"` or `"balanced"`, on `research_question` only The first three are forwarded only when Claude supplies them. `mode` is the exception: it defaults to `"balanced"` and is always sent. If a page comes back thin or empty, it is nearly always a client-rendered app and `effort: "max"` is the fix. Full rendering happens server side, so there is no Playwright in your project and no browser binary in your image. Put it in your system prompt and let Claude apply it: > If a page returns little or no content, retry once with effort set to max. Always cite the sources you used. ## Step 4: custom keys, and assembling it yourself For a key per tenant, a custom base URL, or reuse of an SDK client you already have: ```ts import { query } from "@anthropic-ai/claude-agent-sdk"; import { createTabstackClaudeAgentServer, tabstackAllowedTools, tabstackMcpServerName, } from "@tabstack/claude-agent"; const server = createTabstackClaudeAgentServer({ apiKey: process.env.MY_KEY }); for await (const message of query({ prompt: "...", options: { mcpServers: { [tabstackMcpServerName]: server }, allowedTools: tabstackAllowedTools, }, })) { if (message.type === "result" && message.subtype === "success") { console.log(message.result); } } ``` If you would rather assemble the server yourself, perhaps because you want Tabstack's tools alongside your own in a single server, `createTabstackClaudeAgentTools(config)` returns the raw tool array to pass into your own `createSdkMcpServer({ name, version, tools })`. That is also the route to take when you want a subset. Build the array, keep the tools you need, and hand the rest of your own tools in beside them. ## Step 5: failures This adapter handles errors differently from the others, and the difference is deliberate. When a tool call fails, the handler normalises it to a `TabstackToolError` with a clean message, plus an HTTP `status` for API errors, then returns it as an MCP error result with `isError: true`. What Claude sees is the message; the `status` stays on the error object for your own logging rather than travelling into the transcript. Either way Claude reads a useful message rather than a raw exception, which means it can retry with different arguments or explain the problem to the user. A dead link does not end the run. That is the right default for an agent loop. You are not catching these in your own code in the common case, because the model is the one that needs to react. 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 a run waiting on someone. 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. ## Where to take it next Trim `allowedTools`. `tabstackAllowedTools` approves all five, which is the fastest way to get started and, I would argue, rarely what you want in production. Narrowing it to the two or three your agent actually needs reduces both cost and the chance of Claude choosing a tool you did not intend. Combine it with your own tools. Because this is an in-process MCP server, adding Tabstack does not preclude anything else. Your own `createSdkMcpServer` output and `tabstackServer` sit side by side under different names in `mcpServers`. The package is on npm as [`@tabstack/claude-agent`](https://www.npmjs.com/package/@tabstack/claude-agent), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and hand your Claude agent its first live page. Full API in the [docs](https://docs.tabstack.ai). --- ## Build a web-connected Mastra agent with Tabstack URL: https://tabstack.ai/blog/mastra-web-agent-tabstack Date: 2026-08-03 A hands-on walkthrough: spread Tabstack's tools into a Mastra Agent for schema-enforced extraction, research, and browser automation, one import, plus the production knobs and trimming decisions that follow. 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](https://console.tabstack.ai) ```bash npm install @tabstack/mastra @mastra/core zod ``` ```bash export 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 ```ts 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 string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `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: ```ts 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: ```ts 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: ```ts 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`](https://www.npmjs.com/package/@tabstack/langchain), [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai), and the Python [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/). Your instructions carry over, because the model sees the same tools. The package is on npm as [`@tabstack/mastra`](https://www.npmjs.com/package/@tabstack/mastra), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and hand your Mastra agent its first live page. Full API in the [docs](https://docs.tabstack.ai). --- ## Give an eve agent web access with Tabstack URL: https://tabstack.ai/blog/eve-agent-web-access-tabstack Date: 2026-07-31 A hands-on walkthrough: eve reads its tools from a directory, so wiring in Tabstack is five one-line files, schema-enforced extraction, research, and browser automation, with no array to assemble. eve does something unusual with tools. It does not ask you to register them, assemble them into an array, or pass them into a constructor. It reads the filenames in `agent/tools/`. Add a file, the model can call it. Delete the file, it cannot. I like this more than I expected to. It makes this the shortest integration tutorial in the series: giving your agent the ability to read and research the web is going to take five one-line files. ## What you need - An existing eve project (the package ships dual ESM and CJS builds) - eve v0.24 or later - A Tabstack API key from [console.tabstack.ai](https://console.tabstack.ai) ```bash npm install @tabstack/eve eve zod ``` ```bash export TABSTACK_API_KEY="your-key-here" ``` `eve` and `zod` are peer dependencies, so your app's single instance of each is what gets used. Zod 3 and Zod 4 both work. On Zod 4 the adapter uses the built-in `z.toJSONSchema`, and on Zod 3 it falls back to `zod-to-json-schema`. You do not have to care which, but it is worth knowing nothing will break when you upgrade. ## Step 1: add a tool Create one file under `agent/tools/`, named after the tool, and re-export the Tabstack tool as its default export. ```ts // agent/tools/research_question.ts export { researchQuestionTool as default } from "@tabstack/eve"; ``` That is the whole integration. Your agent can now answer open questions using multiple web sources, with citations. The filename is load-bearing. eve derives tool identity from the path, so `research_question.ts` is what makes the tool `research_question`. It has to match exactly, and not only because eve requires it. That name is identical in the LangChain, Vercel AI SDK, Mastra, and Python packages, so a prompt written against one works against all of them. Rename the file to `web_search.ts` and you have quietly opted out of that. ## Step 2: add the rest Same pattern, one file each: ```ts // agent/tools/extract_page_content.ts export { extractPageContentTool as default } from "@tabstack/eve"; ``` ```ts // agent/tools/extract_structured_data.ts export { extractStructuredDataTool as default } from "@tabstack/eve"; ``` ```ts // agent/tools/generate_structured_data.ts export { generateStructuredDataTool as default } from "@tabstack/eve"; ``` ```ts // agent/tools/automate_browser_task.ts export { automateBrowserTaskTool as default } from "@tabstack/eve"; ``` Here is what each one buys you: | File name | Export | What it does | | --- | --- | --- | | `extract_structured_data.ts` | `extractStructuredDataTool` | Pull specific fields from a URL into a JSON shape you define | | `extract_page_content.ts` | `extractPageContentTool` | Fetch a page as clean markdown | | `research_question.ts` | `researchQuestionTool` | Synthesised answer with cited sources across multiple pages | | `generate_structured_data.ts` | `generateStructuredDataTool` | Fetch a page, then AI-transform it into derived JSON | | `automate_browser_task.ts` | `automateBrowserTaskTool` | Run a multi-step, natural language browser task | The tools resolve `TABSTACK_API_KEY` lazily on first call, so importing the package never requires a key to be set. Your build steps and tests stay happy without secrets. ## Step 3: choose your toolset by choosing your files This is where eve's convention pays off. In every other framework, narrowing the toolset means editing the array or object you pass to the agent. Here you just do not create the file. So ask yourself what your agent actually needs before you create all five. If it only reads and researches, create two files and stop. The model never sees `automate_browser_task`, so it can never decide to try it. Your toolset is visible in a directory listing, which is a more honest description of your agent's capabilities than most codebases manage. That restraint is worth exercising. Every tool you expose is another option the model weighs on every step, and another way for it to choose wrong. Smaller models feel this most. One tradeoff worth naming. Unlike the other Tabstack adapters, this package has no aggregate `tabstackTools` export. Given the file convention there is nowhere sensible to put one, so I think it is the right call, but if you are moving code over from the LangChain or Vercel AI SDK packages it is the difference you will hit first. ## Step 4: know what the model is filling in You never write these arguments yourself, but you will read them in traces when something goes sideways: - `extract_structured_data`: `url`, `json_schema_json`, a JSON encoded JSON Schema string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `guardrails`, `data`, `country`, `max_iterations`, `max_validation_attempts` The distinction people get wrong 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 summary nobody wrote down. If the field exists in the HTML, extract it. If you are asking for judgement, generate it. There are also optional inputs the model can pass for finer control, sent to Tabstack only when present: - `effort`, as `"min"`, `"standard"`, or `"max"`, on the three fetch-based tools. `"max"` does full server-side browser rendering. - `nocache` to bypass the cache - `country`, an ISO 3166-1 alpha-2 code, for geotargeted fetches - `mode`, as `"fast"` or `"balanced"`, on `research_question` If a page comes back thin or empty, it is almost always a client-rendered app and `effort: "max"` is the fix. That single argument is the entire replacement for running a browser. No Playwright, no binaries in your image, nothing to keep patched. You can steer this from your agent's instructions rather than in code: > If a page returns little or no content, retry once with effort set to max. Prefer research_question for open questions, and always cite the sources you used. ## Step 5: use a custom key or client The default tools read `TABSTACK_API_KEY` from the environment. When you need a key per tenant, a custom base URL, or you want to reuse an SDK client you already have, build the tools explicitly and re-export the one you want: ```ts // agent/tools/research_question.ts import { createTabstackEveTools } from "@tabstack/eve"; const tools = createTabstackEveTools({ apiKey: process.env.MY_KEY }); export default tools.research_question; ``` The file convention still applies. The filename decides the tool name, whatever you did inside. ## Step 6: failures When a tool call fails it throws `TabstackToolError`, carrying a normalised message plus an HTTP `status` for API errors, and eve surfaces it in the tool result. The model sees a readable failure rather than a stack trace, which means it can retry or explain rather than falling over. Inputs are also validated against the core Zod schema before the SDK call runs. When a model produces malformed arguments, it fails fast with a clear message instead of sending a broken request and waiting for the API to reject it. One behaviour to know up front: `automate_browser_task` runs non-interactively. It never pauses for human-in-the-loop form input, so it cannot block your agent waiting for someone to fill in a field. It returns the final answer along with 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. ## Where to take it next Start with two files. `research_question.ts` and `extract_page_content.ts` cover most of what a general assistant needs, and you can add the structured tools when a real use case asks for them. When you add `automate_browser_task`, put a guardrails instruction in your agent's system prompt. It is a plain English constraint on what a run may do, and on anything pointed at a live site it is the difference between a read-only browse and an agent cheerfully clicking a button you did not want clicked. The package is on npm as [`@tabstack/eve`](https://www.npmjs.com/package/@tabstack/eve), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and hand your eve agent its first live page. Full API in the [docs](https://docs.tabstack.ai). --- ## Stream a research assistant with the Vercel AI SDK and Tabstack URL: https://tabstack.ai/blog/stream-research-assistant-vercel-ai-sdk-tabstack Date: 2026-07-30 A hands-on walkthrough: give the model Tabstack's web tools with the Vercel AI SDK, then stream a cited answer token by token so users see progress forming instead of staring at a spinner. Web research is slow. Reading four pages and synthesising an answer takes seconds, sometimes tens of seconds, and there is no clever trick that makes it instant. What you can do is stop making people stare at a spinner while it happens. This tutorial builds a research assistant on the Vercel AI SDK that streams its answer as it forms, using Tabstack for the web access. The whole thing is about thirty lines. ## What you need - A Node project (the package ships dual ESM and CJS builds) - A Tabstack API key from [console.tabstack.ai](https://console.tabstack.ai) - An OpenAI key, or any other provider the AI SDK supports ```bash npm install @tabstack/ai ai zod @ai-sdk/openai ``` ```bash export TABSTACK_API_KEY="your-key-here" export OPENAI_API_KEY="your-openai-key" ``` `ai` and `zod` are peer dependencies, so your app keeps one instance of each. The package works with AI SDK v5 and later, and is v6 ready. ## Step 1: give the model tools and get out of the way The AI SDK expects `tools` to be an object keyed by tool name. That is exactly what `tabstackTools` is, so it passes straight through. ```ts import { openai } from "@ai-sdk/openai"; import { generateText, stepCountIs } from "ai"; import { tabstackTools } from "@tabstack/ai"; const { text } = await generateText({ model: openai("gpt-4o"), tools: tabstackTools, stopWhen: stepCountIs(5), prompt: "What are Vercel's pricing plans and how do they compare?", }); console.log(text); ``` That is a working web-connected agent. `tabstackTools` reads `TABSTACK_API_KEY` from the environment, so there is no client to construct. The one line people leave out is `stopWhen`. Without it the SDK makes a single call, the model asks for a tool, and you get back a response containing a tool call and no text. `stepCountIs(5)` tells it to keep going: call the tool, take the result, and carry on until it has an actual answer. Five is a reasonable ceiling for research work. Raise it if your agent legitimately needs to chain several lookups, lower it if you want a hard cost bound. ## Step 2: stream it `generateText` waits for everything. For anything user-facing, swap to `streamText` and start showing output immediately. ```ts import { openai } from "@ai-sdk/openai"; import { streamText, stepCountIs } from "ai"; import { tabstackTools } from "@tabstack/ai"; const result = streamText({ model: openai("gpt-4o"), tools: { research_question: tabstackTools.research_question, extract_page_content: tabstackTools.extract_page_content, }, stopWhen: stepCountIs(5), prompt: "Summarize the latest on quantum error correction, with sources.", }); for await (const chunk of result.textStream) { process.stdout.write(chunk); } ``` Note what happened to `tools`. Because it is a plain object keyed by tool name, narrowing the toolset is just picking keys. Here the assistant only researches and reads, so it gets two tools instead of five. That is worth doing deliberately. Every tool you pass is another option the model weighs on every step, and another way for it to pick wrong. On smaller and cheaper models, cutting the toolset to what the job needs is one of the highest-leverage changes you can make. One honest note about streaming with tools, and it is the thing I would fix first in any demo. The text stream goes quiet while a tool runs, because the model is waiting on a network call it cannot narrate. Users read that silence as a hang. Surface tool activity in your UI, even if it is just "searching the web", so the pause has an explanation. ## Step 3: know your five tools | 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` | A 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 inputs, but knowing the shapes helps when you are debugging why it chose badly: - `extract_structured_data`: `url`, `json_schema_json`, a JSON encoded JSON Schema string - `extract_page_content`: `url` - `research_question`: `query` - `generate_structured_data`: `url`, `instructions`, `json_schema_json` - `automate_browser_task`: `task`, plus optional `url`, `guardrails`, `data`, `country`, `max_iterations`, `max_validation_attempts` The distinction that matters most 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 summary nobody wrote. If the field exists in the HTML, extract it. If you are asking for judgement, generate it. ## Step 4: the flags that matter in production The model can pass these itself, and they are only sent to Tabstack when present, so defaults stay intact when they are omitted. On `extract_structured_data`, `extract_page_content`, and `generate_structured_data`: - `effort`: `"min"`, `"standard"`, or `"max"`. Reach for `"max"` on JavaScript-heavy pages, where it does full server-side browser rendering. - `nocache`: bypass the cache when you need a fresh read. - `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 suspiciously empty, it is nearly always a client-rendered app and `effort: "max"` is the fix. That is the whole workaround. No Playwright, no browser binary in your container image, no keeping either patched. You can steer these from the system prompt rather than in code, which is often the cleaner move: ```ts const result = streamText({ model: openai("gpt-4o"), tools: tabstackTools, stopWhen: stepCountIs(5), system: "You research questions using web tools. " + "If a page returns little or no content, retry once with effort set to max. " + "Prefer research_question for open questions and cite every source you use.", prompt: userQuestion, }); ``` ## Step 5: failures When a tool call fails it throws `TabstackToolError`, carrying a readable message and, for API failures, an HTTP `status`. Inside an agent loop the AI SDK surfaces this in the tool result, so the model can see the failure and react. If you are calling a tool's `execute` directly, catch it yourself: ```ts import { TabstackToolError, tabstackTools } from "@tabstack/ai"; try { await tabstackTools.extract_page_content.execute( { url: "https://example.com" }, { toolCallId: "1", messages: [] }, ); } 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. ## Step 6: configuration The defaults read `TABSTACK_API_KEY` from the environment, which is what you want in most apps. When you need a key per tenant, a custom base URL, or you already have an SDK client in scope: ```ts import { createTabstackAiTools } from "@tabstack/ai"; const tools = createTabstackAiTools({ apiKey: process.env.MY_KEY }); ``` The client is created lazily, so importing the package never requires a key to be set. Your module imports stay safe in tests and in build steps that do not have secrets. ## Where to take it next Render the citations. `research_question` returns sources alongside the answer, and showing them is what separates an assistant people trust from one they double-check. Narrow the toolset per route. A pricing page reader needs `extract_structured_data` and nothing else. A general assistant needs the lot. There is no rule that every surface in your app gets the same five. If part of this workload moves to Python or another framework, the tool names and inputs are identical in [`@tabstack/langchain`](https://www.npmjs.com/package/@tabstack/langchain) and [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/). Your prompts carry over, because the model sees the same tools. The package is on npm as [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and stream your first cited answer. Full API in the [docs](https://docs.tabstack.ai). --- ## Build a competitor brief agent with LangChain.js and Tabstack URL: https://tabstack.ai/blog/competitor-brief-langchain-js-tabstack Date: 2026-07-29 A hands-on TypeScript walkthrough: pull pricing off a page, research positioning across sources, generate the judgement calls, then assemble a LangChain.js agent that produces a competitor brief with sources attached. The Python tutorial built a research agent. This one builds something with a shape you can hand to a colleague: a competitor brief. Point it at a company, get back their pricing, their positioning, and what people are saying about them, with sources. Same five tools as the Python package, same names, same inputs. If you have read the Python version, the only thing that changes here is the syntax, which I still find slightly surprising every time I move between them. ## What you need - A Node project (the package ships dual ESM and CJS builds) - A Tabstack API key from [console.tabstack.ai](https://console.tabstack.ai) - An OpenAI key, or any other model LangChain.js supports ```bash npm install @tabstack/langchain @langchain/core zod langchain @langchain/openai ``` ```bash export TABSTACK_API_KEY="your-key-here" export OPENAI_API_KEY="your-openai-key" ``` `@langchain/core` and `zod` are peer dependencies rather than dependencies. That is intentional. Your app keeps a single instance of each, which avoids the genuinely miserable class of bug where two copies of Zod disagree about whether your schema is a schema. ## Step 1: pull structured data off a page Start with the piece the whole brief hangs on. You know the pricing page URL, and you know what you want from it, so describe the shape and ask for it. ```ts import { extractStructuredDataTool } from "@tabstack/langchain"; const pricing = await extractStructuredDataTool.invoke({ url: "https://example.com/pricing", json_schema_json: JSON.stringify({ type: "object", properties: { plans: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Plan name" }, price: { type: "number", description: "Monthly price in USD" }, features: { type: "array", items: { type: "string" }, description: "Included features", }, }, }, }, }, }), }); for (const plan of pricing.plans) { console.log(`${plan.name}: $${plan.price}`); } ``` Two details worth pausing on. The schema goes in as `json_schema_json`, a JSON encoded string rather than an object. That looks fussy 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 get right far more often than nested objects. If you write one thing carefully in this whole tutorial, make it the field descriptions. The `description` on each field is not decoration. It is the instruction that tells the extractor which number on the page you meant when you said `price`. A pricing page has annual figures, discounted figures, and per-seat figures on it. The description is how you disambiguate. Vague or missing descriptions are the most common reason extraction results disappoint. Unlike the Python package, the TypeScript tools return plain objects. No `JSON.parse` step. ## Step 2: research what is not on any single page Pricing lives on a pricing page. Positioning and reputation do not live anywhere in particular, which is what `research_question` is for. ```ts import { researchQuestionTool } from "@tabstack/langchain"; const { answer, sources } = await researchQuestionTool.invoke({ query: "How do developers describe Example Corp's developer experience?", }); console.log(answer); for (const source of sources) { console.log(`- ${source.title}: ${source.url}`); } ``` `sources` comes back as `{ title, url }` objects, which drops straight into a citation list. This is the part that makes a brief defensible rather than just confident. When someone asks where a claim came from, you have the page. While iterating, pass `mode: "fast"`. Move to `"balanced"` when you care more about quality than latency. ## Step 3: generate what the page never said Extraction pulls out what exists. Sometimes you want something derived, like a category or a one-line summary that nobody wrote down. That is `generate_structured_data`: fetch the page, then transform it. ```ts import { generateStructuredDataTool } from "@tabstack/langchain"; const positioning = await generateStructuredDataTool.invoke({ url: "https://example.com", instructions: "Identify the primary audience this homepage is written for, " + "the main problem it claims to solve, and its strongest differentiator.", json_schema_json: JSON.stringify({ type: "object", properties: { audience: { type: "string" }, problem: { type: "string" }, differentiator: { type: "string" }, }, }), }); ``` The line between the two tools is simple. If the field is written on the page, 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 4: assemble the agent Three tool calls, three pieces of a brief. You could orchestrate that yourself with three awaits, and for a fixed report you probably should. But the moment the shape of the question varies, let the model choose. ```ts import { createAgent } from "langchain"; import { tabstackTools } from "@tabstack/langchain"; const agent = createAgent({ model: "openai:gpt-4o", tools: tabstackTools, systemPrompt: "You produce competitor briefs. " + "Use extract_structured_data for facts on a known page such as pricing, " + "extract_page_content when you need to read a page in full, " + "generate_structured_data when you need a judgement the page does not state outright, " + "and research_question for anything spread across multiple sources. " + "Always cite the sources you used.", }); const result = await agent.invoke({ messages: [ { role: "user", content: "Build me a brief on Example Corp. Their pricing is at https://example.com/pricing.", }, ], }); console.log(result.messages.at(-1)?.content); ``` `tabstackTools` is all five, 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 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. `tabstackTools` is an array here, so filter it or import individual tools and build your own array if the agent needs two. Fewer tools means fewer chances to pick the wrong one, which matters more the smaller your model is. One inconsistency to watch, because it caught me out. The aggregate export shape is not the same across the Tabstack adapters. Here and in the OpenAI Agents and LlamaIndex packages it is an array. In `@tabstack/ai` and `@tabstack/mastra` it is an object keyed by tool name. That is not sloppiness, it is each package matching what its framework expects, but if you are wiring up two of these in one codebase, check before you spread. Notice what the system prompt is doing. It is not describing the tools, since each package ships descriptions written for the model to read. It is describing the job. That division keeps your prompts short and means tool description improvements reach you on upgrade rather than requiring a prompt rewrite. ## Step 5: when the data is behind an interaction Some things are not on a page until you do something. A search box, a region selector, a "load more" button. That is `automate_browser_task`: ```ts import { automateBrowserTaskTool } from "@tabstack/langchain"; const result = await automateBrowserTaskTool.invoke({ task: "Find the top 3 trending repositories and their star counts", url: "https://github.com/trending", guardrails: "browse and extract only, do not submit forms", }); console.log(result.answer, result.pagesVisited, result.durationMs); ``` You get back `answer`, `success`, `iterations`, `actions`, `durationMs`, `extracted`, and `pagesVisited`. The last few are what make failures diagnosable. When the answer is wrong, `actions` and `pagesVisited` usually tell you why in about ten seconds. Set `guardrails`. It is a plain English constraint on what the run is allowed to do, and on anything pointed at a live site it is the difference between a read-only browse and an agent cheerfully clicking a button you did not want clicked. One thing to know up front: this tool runs non-interactively. It never pauses for human input, so it cannot block your agent loop waiting for someone to fill in a form. 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: flags and failures Four optional inputs cover nearly everything. They are only sent when you pass them, so omitting them keeps Tabstack's defaults. ```ts // Render a JS-heavy SPA, fresh, as if from the UK: await extractPageContentTool.invoke({ url: "https://example.com/app", effort: "max", nocache: true, country: "GB", }); ``` `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. That single keyword replaces installing Playwright, shipping a browser binary, and keeping both patched. `nocache` matters when you are watching something that moves, like prices. `country` matters when the page varies by region, which for pricing pages is most of the time. Every tool throws one error type: ```ts import { TabstackToolError, extractPageContentTool } from "@tabstack/langchain"; try { await extractPageContentTool.invoke({ url: "https://example.com" }); } catch (err) { if (err instanceof TabstackToolError) { console.error(`Tabstack failed (${err.status ?? "no status"}): ${err.message}`); } } ``` One `catch` instead of separate handling for transport, parsing, and API errors. `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. ## Configuration, if you need it The default tools read `TABSTACK_API_KEY` from the environment. If you need a different key per tenant, a custom base URL, or you already have an SDK client you want reused: ```ts import { createTabstackLangchainTools } from "@tabstack/langchain"; const tools = createTabstackLangchainTools({ apiKey: process.env.MY_KEY }); ``` ## Where to take it next Turn the brief into a scheduled job. `nocache: true` plus a diff against last week's run is a change monitor, and it is maybe twenty more lines. If you are moving this to a streaming UI, the same five tools exist in `@tabstack/ai` for the Vercel AI SDK, under identical names. Your system prompt carries over unchanged. The package is on npm as [`@tabstack/langchain`](https://www.npmjs.com/package/@tabstack/langchain), and the full API reference is at [docs.tabstack.ai](https://docs.tabstack.ai). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and generate your first competitor brief. Full API in the [docs](https://docs.tabstack.ai). --- ## Build a research agent that cites its sources with LangChain and Tabstack URL: https://tabstack.ai/blog/research-agent-langchain-tabstack Date: 2026-07-28 A hands-on Python walkthrough: call a Tabstack tool on its own, extract typed fields from a page, then hand all five tools to a LangChain agent that answers open questions with citations, in roughly forty lines with no browser to install. 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](https://console.tabstack.ai) - An OpenAI key, or any other chat model LangChain supports ```bash pip install langchain-tabstack langchain langchain-openai ``` ```bash export 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`: ```python 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: ```bash python research.py ``` You 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. ```python 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. ```python 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. ```python 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: ```python 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: ```python 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](https://docs.tabstack.ai), and the package is on PyPI as [`langchain-tabstack`](https://pypi.org/project/langchain-tabstack/). Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and give your agent its first cited answer. Full API in the [docs](https://docs.tabstack.ai). --- ## Turn any page into clean markdown or typed JSON, cheaply URL: https://tabstack.ai/blog/extract-most-used-endpoint Date: 2026-07-20 Research and browser agents get the demos. But most of the time the answer is just "fetch this page and give me the clean version," which is a cheaper, deterministic call, not an agent. Turn any URL into clean markdown, or into the exact typed fields you asked for. Here's why it's the right default for most web-data work, and when it isn't. If I had to bet on which Tabstack endpoint you will call most often, I would put my money on `/extract` without thinking twice. It is not the one people get excited about in the demo. Research and automate get the "wow." Extract is the one that ends up in production, running thousands of times a day, doing the unglamorous work that everything else is built on top of. Here is the thing about extract: it is deterministic, it is cheap, and it does exactly one job per call. Those are not limitations. They are the reason it is the right default for most web data work. Before you reach for an agent, ask whether the answer is just "fetch this page and give me the clean version." A lot of the time, it is. Extract has two methods. They solve two different problems, so let me take them one at a time. ## extract.markdown: the page, minus the noise Every web page is wrapped in stuff you do not want. Navigation, ads, cookie banners, sidebars, footers, the same boilerplate on every page of the site. When all you need is the actual content, `extract.markdown` fetches the URL, strips the cruft, and hands you clean markdown. ```typescript import Tabstack from '@tabstack/sdk'; const client = new Tabstack(); const result = await client.extract.markdown({ url: 'https://example.com/blog/article', }); console.log(result.content); ``` By default, the page's metadata (title, author, description, and so on) comes embedded as YAML frontmatter at the top of `content`. That is handy if you are feeding a static site generator that already expects frontmatter. For most programmatic use, though, you want the metadata as a structured object, not a string you have to parse. Add `metadata: true`: ```typescript const result = await client.extract.markdown({ url: 'https://example.com/blog/article', metadata: true, }); console.log(result.content); // pure markdown, no frontmatter console.log(result.metadata?.title); // "Example Article Title" ``` Now `content` is clean markdown and `metadata` is a typed object with `title`, `author`, `publisher`, `image`, `site_name`, `type`, and friends. No YAML parsing, which means no broken parse the day a title contains a colon or a quote. Where does this earn its keep? Mostly two places. The first is anything that feeds an LLM: RAG pipelines, summarization jobs, content classification. Models work better on clean markdown than on a soup of HTML, and you are not paying for tokens you will throw away. The second is content systems: read-it-later apps, aggregators, archives, anything that wants a stable, storable version of a page instead of the live HTML. A couple of parameters worth knowing: - `effort` controls how hard the renderer works, with values `min`, `standard` (the default), and `max`. Static pages are fine on `standard`. Pages that build their content with JavaScript may need `max`. Pages that are already plain HTML can drop to `min`. This is a direct cost and speed lever, so it is worth tuning per source rather than leaving on default everywhere. - `nocache` defaults to `false`. Extract caches for a short window, which is a win for blog posts and docs. For breaking news or anything that changes minute to minute, set `nocache: true` to force a fresh fetch. - `geo_target` takes a country code (`{ country: 'US' }`) when the page serves different content by region. ## extract.json: the shape you asked for, every time `extract.markdown` gives you the whole page. Often you do not want the whole page. You want three fields out of it, typed, ready to drop into your code. That is `extract.json`. You pass a URL and a JSON schema. You get back data in exactly that shape. ```typescript const result = await client.extract.json({ url: 'https://news.ycombinator.com', json_schema: { type: 'object', properties: { stories: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, points: { type: 'number' }, }, }, }, }, }, }); // { stories: [ { title: "...", points: 342 }, ... ] } ``` The response matches your schema. Not "close to" your schema. You get strings where you asked for strings and numbers where you asked for numbers, instead of everything arriving as text that you then coerce by hand. There is no HTML parsing on your side, no brittle selectors that break the next time the site ships a redesign. Two behaviors worth internalizing: First, a missing field does not throw. A single field the extractor cannot fill, on one page in a batch of a thousand, will not blow up the whole run, which is exactly what you want in production. What comes back for that field is not guaranteed to be `null`, though: depending on the type, you may get `null` or a best-effort value the extractor inferred. So validate the values you get back, do not just check for `null`, and mark a field `required` when you want the extractor to work harder to find it. Second, your schema descriptions are doing real work. Adding a `description` to a property gives the extractor context about what to look for. Start with a simple schema, run it, and add descriptions to the fields that come back wrong. It is the highest-leverage tuning you can do. ```typescript json_schema: { type: 'object', properties: { price: { type: 'number', description: 'The current sale price in USD, not the original or crossed-out price.', }, }, } ``` That one description is the difference between extracting the right number and the struck-through one. ## Where extract is the wrong tool This is the honest part, and it is the same boundary I drew for generate. Extract pulls what is on the page. It does not form opinions. If the field you want is "is this pricing aggressive" or "summarize this in two sentences," that is interpretation, and it belongs to `/generate/json`, which puts a model in the loop on purpose. The reason to keep that line clean is cost and determinism. Extract is the cheaper, repeatable workhorse. Reach for it whenever the data is literally on the page, which is more often than you would guess. Save the AI endpoints for the jobs that actually need judgment. If you are deciding between extract and research: extract is for when you have the URL. Research is for when you have a question and do not yet know which page holds the answer. Different problems, different endpoints. ## Getting started ```bash npm install @tabstack/sdk # or pip install tabstack ``` The SDK reads `TABSTACK_API_KEY` from your environment, so a first call is two lines: ```python from tabstack import Tabstack client = Tabstack() result = client.extract.markdown(url="https://example.com/blog/article") print(result.content) ``` Start with `extract.markdown` to confirm your key works and see what a clean page looks like. Then move to `extract.json` the moment you know which fields you actually want. Most of the web data problems you think need a pipeline are one extract call and a schema. Ready to build? [Sign up](https://console.tabstack.ai/signup) for 10,000 free credits, no card required, and make your first `extract` call. Full API in the [docs](https://docs.tabstack.ai). --- ## Tabstack on Stripe Projects URL: https://tabstack.ai/blog/stripe-projects-integration Date: 2026-07-15 AI agents cannot manually navigate web dashboards to provision API keys. Tabstack is now integrated with Stripe Projects, allowing developers and autonomous software to provision browsing infrastructure, manage account lifecycles, and handle usage-based billing entirely through the Stripe CLI with a single command. At Tabstack, we build the advanced web-browsing, automation, and research toolchains that power autonomous AI agents. Yet, every agent project eventually hits a setup wall: your agent needs an API key. That means leaving whatever you were building, going to another dashboard, creating an account, adding billing, copying a credential, and wiring it into your environment. For a human developer, this is friction. For an AI agent trying to bootstrap its own tooling, it is a hard stop. Stripe Projects resolves this exact architectural bottleneck. ## Stripe Projects: Commercial Routing for Machines [Stripe Projects](https://stripe.com/docs/projects) is a new workflow in the Stripe CLI. Currently in developer preview, it lets developers provision third-party services directly from within a Stripe project. Once provisioned, credentials inject automatically into the project environment, and billing flows natively through Stripe. Think of it as a marketplace where an AI agent can walk in, select the required tools, and exit with functional credentials and an established billing relationship. When you add a service through Stripe Projects, Stripe Projects coordinates account creation or linking with Tabstack, provisions the resource, and drops the credentials directly into your `.env` file. Your agent moves from zero to a working API key with just a few terminal commands. ## Terminal-First Workspace Setup Starting today, Tabstack is participating in the Stripe Projects developer preview. As a co-design partner on the underlying Agentic Provisioning Protocol, we worked closely with Stripe to ensure that provisioning and credential handoff work reliably and deterministically across providers. With Stripe Projects, you can initialize your app workspace and attach the Tabstack toolchain directly from the terminal: ```shell # Initialize the project environment stripe projects init my-app # Provision the Tabstack resource stripe projects add tabstack/api ``` You will then be prompted to select a Tabstack plan for your account (you can also view our full tier breakdown on the [Tabstack pricing page](https://tabstack.ai/pricing)). The Stripe CLI triggers an interactive menu directly in your terminal to make your selection: ```shell Plan Price Description trial Free 10,000 credits included. individual Usage based $0.35 per 1,000 credits. team $99/month 500,000 credits included, then $0.30 per 1,000. pro $499/month 3,000,000 credits included, then $0.25 per 1,000. ``` Once you select a plan and confirm, the CLI provisions your Tabstack account while injecting `TABSTACK_API_KEY` into your project environment. On paid plans, billing details are set up once and shared securely with your SaaS stack through a Shared Payment Token, eliminating separate invoice management. Resources continue to live under your own Tabstack account with standard access and dashboard configurations intact. ## Unlocking the Toolchain Once provisioning is complete, your Tabstack API key gives you or your agent full access to all Tabstack features: * **/extract/markdown:** Converts any URL into clean Markdown, built for an LLM context window. * **/extract/json:** Converts web pages into structured JSON matching a defined schema. * **/generate/json:** Generates structured data from web content using natural language payloads. * **/automate:** Navigates, clicks, fills forms, and completes multi-step web workflows. * **/research:** Runs a multi-pass agentic research loop and outputs a synthesized, cited report. Initialize the SDK once the credential populates your environment variables: ```ts import { Tabstack } from '@tabstack/sdk'; const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY }); // Target the markdown extraction backend directly const result = await client.extract.markdown({ url: 'https://example.com/docs' }); console.log(result.content); ``` ## Lifecycle Management Beyond provisioning, you can manage your entire Tabstack resource lifecycle directly from the terminal, making it simple to upgrade tiers, rotate credentials, or log in to the Tabstack dashboard without leaving your workflow: ```shell # Rotate your API key stripe projects rotate tabstack/api # Open the Tabstack dashboard for your account stripe projects open tabstack # Upgrade to a higher plan stripe projects upgrade tabstack/api # Remove the service and deprovision the account stripe projects remove tabstack/api ``` ## Designing for Autonomy This integration is about more than just developer convenience; it is about building infrastructure native to machines. Manual credentials break autonomous systems. When an AI agent relies on one-off dashboard navigation or hardcoded keys, it stalls. Eliminating this manual setup allows the agent to provision its own browsing infrastructure dynamically as dependencies scale. When an automated deployment script triggers the workspace setup, a functional key drops in thirty seconds later with billing wired instantly. This unlocks entirely new operational patterns: automated project initialization, self-healing runtimes that swap out broken tools, and dynamic infrastructure scaling based on task complexity. Tabstack handles the browsing. Stripe handles the billing. The agent handles the execution. ## Next Steps This integration is in developer preview, and we want your feedback. To get started, install the [Stripe CLI here](https://stripe.com/docs/stripe-cli), initialize your app workspace, and attach Tabstack to your environment: ```shell stripe projects init my-app stripe projects add tabstack/api ``` Review the full API documentation at [docs.tabstack.ai](https://docs.tabstack.ai). --- ## Introducing Schema Source: Programmatic Blueprinting for Web Data Extraction URL: https://tabstack.ai/blog/introducing-schema-source-web-data-extraction Date: 2026-06-26 Building schemas for AI web scraping is tedious. We built Schema Source, an independent API that instantly reverse-engineers any webpage to generate a production-ready JSON schema. Learn how to combine it with Tabstack to automate programmatic blueprinting, extraction, and data transformations with zero manual config. We built **Tabstack** to replace traditional parsing configurations with a declarative model. Instead of telling a scraper *how* to navigate a page, you define *what* data you want using a JSON schema, and our `/extract/json` and `/generate/json` endpoints handle the execution. Schema-first extraction bypasses structural web shifts, but introduces a predictable bottleneck: **manual schema design**. Manually building a valid, production-grade JSON schema for a complex web page is tedious. It requires inspecting the DOM, identifying data hierarchies, mapping out types, and writing detailed string descriptions so the underlying LLMs understand the context of each field. To automate this setup phase, we built [**Schema Source**](https://schema.tabstack.ai?utm_source=tabstack.ai&utm_medium=blog&utm_campaign=blog_post&utm_content=schema_intro). ## **What is Schema Source?** **Schema Source** is an independent API that reverse-engineers a webpage's visual and semantic layout to instantly generate a structured data schema. Pass a URL via a raw `GET` request to instantly retrieve a structured JSON blueprint. For example, if you point it at a content-heavy community page like `https://reddit.com/r/nba`, Schema Source analyzes the page components and automatically outputs an optimized JSON schema mapping the posts, metrics, and metadata. You can then pass this schema directly into Tabstack's extraction pipelines to begin pulling live data immediately. ## **How Schema Source Fits into the Tabstack Workflow** In a schema-first architecture, **object descriptions act as system prompts**. If your schema has a field called score, an LLM needs to know if that means a sporting event's box score, a user upvote count, or a sentiment rating. Schema Source handles this contextual work by generating explicit metadata alongside the data types. ### **Step 1: Bootstrapping the Schema via Schema Source** To generate a structural baseline for a target page, append your target URL directly to the Schema Source endpoint: ```ts async function generateSchema(url) { const res = await fetch( `https://schema.tabstack.ai/get/${encodeURIComponent(url)}?format=json`, ); const { schema } = await res.json(); return schema; } ``` The API analyzes the target page and returns a clean, production-ready JSON schema tailored to its core data elements: ```json { "type":"object", "properties":{ "posts":{ "type":"array", "description":"List of popular posts on Reddit's front page", "items":{ "type":"object", "properties":{ "title":{ "type":"string", "description":"The title of the Reddit post" }, "author":{ "type":"string", "description":"The username of the post's author" }, "score":{ "type":"number", "description":"The score (upvotes) of the post" }, "comment_count":{ "type":"number", "description":"The number of comments on the post" }, "permalink":{ "type":"string", "description":"The direct link to the Reddit post discussion page" }, "published_at":{ "type":"string", "description":"The timestamp when the post was published" } }, "required":[ "title", "author", "score", "comment_count", "permalink", "published_at" ] }, "maxItems":10 } }, "required":[ "posts" ] } ``` ### **Step 2: Running a Direct Extraction with Tabstack** Deploying the schema into a production pipeline requires zero manual layout configuration. Pass the generated schema directly into the official `@tabstack/sdk` to execute a literal data extraction. ```ts import Tabstack from '@tabstack/sdk'; const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY }); const url = 'https://reddit.com/r/nba'; const json_schema = await generateSchema(url); async function extractLiteralData() { const response = await client.extract.json({ url, json_schema, }); console.log('Extracted Data:', JSON.stringify(response.data, null, 2)); } ``` ### **Step 3: Layering AI Transformation with generate.json** If your pipeline requires transforming, filtering, or analyzing data rather than just mirroring what is literally on the page, you can use the exact same schema with Tabstack's `/generate/json` endpoint. While `/extract/json` returns literal data from the DOM, generate allows you to pass custom prompt instructions (like summarization or synthesis) while guaranteeing the engine wraps those modified results perfectly inside your schema structure: ```ts async function extractAndTransformData() { const response = await client.generate.json({ url, json_schema, instructions: "Filter out any threads that aren't about live game updates. For the remaining posts, rewrite the title to be a strictly objective 5-word summary.", }); console.log('Transformed Data:', JSON.stringify(response.data, null, 2)); } ``` ## **Decoupling Configuration from Execution** We decoupled Schema Source to isolate structural data discovery from production data ingestion pipelines. This architecture eliminates the need to manually draft schemas or guess layout hierarchies during the initial development cycle. Teams can use Schema Source programmatically to seed baseline data definitions, then rely entirely on Tabstack’s infrastructure to fetch, parse, and validate those payloads at scale. Try generating your first schema at [schema.tabstack.ai](https://schema.tabstack.ai?utm_source=tabstack.ai&utm_medium=blog&utm_campaign=blog_post&utm_content=schema_cta). --- ## Hardening AI Web Agents: How We're Securing Tabstack Against Indirect Prompt Injection URL: https://tabstack.ai/blog/securing-ai-agents-indirect-prompt-injection Date: 2026-06-12 Following a disclosure from Brave, we patched an indirect prompt injection vulnerability in our automation endpoint. This report details the original exploit behavior and the new isolation boundaries added to the Mozilla Pilo engine. At Mozilla, we believe that building a useful AI ecosystem requires radical transparency, especially when it comes to security. Recently, security researchers at Brave reached out to us regarding an Indirect Prompt Injection (IPI) vulnerability they identified in Tabstack's `/v1/automate` endpoint, which they have since detailed in their public blog post on the flaw. Because Tabstack is built to act as an autonomous web agent that can browse, click, and interact with the live web on behalf of a user, the implications of IPI are a critical design challenge. The vulnerability has been patched, and the fix was independently verified by the Brave team before their public write-up. We want to share a transparent look at the exploit, how our model handled it, and the architecture we've implemented to harden our automation engine against this entire class of attacks. ## The Vulnerability: Bypassing the Scope of the Task The attack discovered by Brave highlights the unique risks associated with "agentic" AI tools. During a controlled test, researchers passed a standard, routine prompt to the `/v1/automate` endpoint: "Summarize this page." However, the target page contained hidden, malicious instructions (rendered in white-on-white text, invisible to a human but fully readable in the page's text layer ingested by the AI). The injected text instructed the model to ignore its previous task, grab the user's full conversation history, paste it into an external web form, and hit submit. Because Large Language Models mix user intent and third-party data into a single, flat text window, Tabstack didn't view this as a security conflict. Instead, it confidently followed the instructions step-by-step: 1. Navigated away from the target page to an external domain. 2. Copied the user's conversation context. 3. Submitted the form, actively exfiltrating the data to the researcher's server. When we analyzed the agent's internal reasoning traces, the challenge became even clearer. The model wasn't "tricked" or confused; it was executing what it genuinely believed to be a legitimate workflow continuation. ## How We Fixed It: Moving Beyond a Flat Context Prompt injection is a dynamic, shifting threat vector across the entire AI industry. While it is functionally impossible to guarantee any LLM is 100% immune to prompt injection, we can severely limit an agent's ability to act on malicious inputs. That was our north star: assume the model will occasionally be fooled by injected text, and make sure that being fooled cannot translate into exfiltrated data. Following the disclosure, our engineering team confirmed the gap and shipped a series of changes to our underlying browser automation engine, Mozilla Pilo. Rather than trying to detect malicious prompts (a losing game of pattern-matching), we focused on shrinking the agent's "blast radius" with structural guardrails that hold regardless of what the page says. ### A Structural Action Firewall for Forms The core of the fix is a new action firewall that sits between the agent's decisions and the browser's execution of them. Critically, it does not work by scanning text for "suspicious" instructions. Instead, it classifies every form interaction using DOM field metadata and reference provenance: where a value came from, what kind of field it is targeting, and whether a human ever approved it. - The agent is free to fill operational controls it legitimately needs to do its job (search boxes, date pickers, range sliders, comboboxes). - It is structurally blocked from auto-filling freeform or sensitive fields, and from submitting any form that contains agent-filled data that was never approved, before the action reaches the browser. - Even operational submissions are now restricted to the same host as the current page. An attacker page can label its collector field as a "search box," but it cannot make the agent submit that data to an attacker-controlled domain. Unknown page hosts fail closed. When a block fires in non-interactive mode, the CLI prints a remediation footer explaining how to proceed. That footer is user-facing only: the model never sees it, so injected page content cannot instruct the agent to talk the user into disabling the firewall. ### External Content Isolation We also closed the more fundamental gap that made the original exploit possible: the agent treated web-page text and user instructions as one undifferentiated stream. Web-sourced content returned by the agent's tools (page extracts, fetched markdown, search results, even the completion validator's own feedback) is now wrapped in explicit `` tags at the point the data enters the conversation, with an inline warning on every block and a matching directive in the system prompt. This mirrors the trust-framing we already applied to raw page snapshots, so untrusted page text is consistently delineated from the user's actual task. This wrapping is structural, not learned: the boundary is enforced by how we construct the context, not by hoping the model "decides" to distrust the right paragraph. The same change also clips stale external content out of conversation history after a turn, which incidentally shut down a cost-amplification angle of the same attack. ### Caller-Controlled Trust Boundaries, Now Per Request Some legitimate workflows genuinely need the agent to enter data into third-party forms. Rather than weaken the firewall globally, we exposed explicit, opt-in trust controls: - **`trustedHostnames`:** A caller-supplied allowlist that bypasses the fill and submit gates, but only when the current page host and every form-action target (including submitter `formaction` overrides) are all on the list. - **`unsafeMode`:** A deliberate, fully-documented global opt-out, with prominent data-risk warnings on every surface. - A caller-provided start URL is treated as consent to interact with that specific site, but planner-chosen or agent-navigated URLs are not, since those are influenced by the model and the page. Originally configured via local environment variables and core configuration files, we have exposed these parameters directly to our automation engine's request layer. This allows the Tabstack API to pass `trustedHostnames` and `unsafeMode` dynamically inside the request body, establishing strict trust boundaries per request rather than per deployment. ### Defense in Depth Alongside the firewall and content isolation, we tightened how the agent reports completion. A new data-grounding rule requires every value the agent returns to trace back to a page snapshot, tool result, or task input (no filling in answers from the model's own training), backed by a pre-completion verification checklist. We also wired the completion validator's conversation history and outcome into the agent loop, and hardened the agent against getting stuck repeating injected actions across changing element references. ## The Responsible Disclosure Timeline We are incredibly grateful to the Brave research team for their clean, professional coordination on this bug. Their outreach allowed us to identify, patch, and verify a critical gap before it could ever affect users in production. - **May 13, 2026:** Brave reports the IPI vulnerability to the Mozilla Tabstack team. - **May 14, 2026:** Mozilla engineers review the traces, confirm the vulnerability, and begin engineering defenses. - **June 1, 2026:** Mozilla deploys the updated Pilo browser engine changes, and the Brave team independently verifies the fix. Securing autonomous web agents is one of the most complex engineering challenges in AI right now. By treating third-party web content as inherently untrusted, creating structural boundaries, and cutting off the pathways for unauthorized data exfiltration, we are building a safer foundation for agentic workflows. We will continue to harden Tabstack out in the open, and we welcome the global security community's continued scrutiny. --- ## Upgrading Pilo to Support Human-in-the-Loop Browser Automation URL: https://tabstack.ai/blog/pilo-interactive-mode Date: 2026-04-16 What happens when an AI web agent needs context it does not have? With Pilo Interactive Mode, it no longer has to guess or fail. This new beta feature lets Pilo pause its reasoning loop, ask the user or a parent agent for the missing data, and seamlessly complete the task. When we open-sourced Pilo, our goal was to provide a robust execution engine for AI web agents capable of navigating the chaos of the modern web. By leveraging the accessibility tree and intelligent reasoning loops, Pilo handles complex browser orchestration reliably. But autonomous execution has a hard limit: missing context. Today, we are excited to introduce **Interactive Mode (Beta)** for Pilo. This new feature allows the Pilo agent to pause its execution loop and ask the caller, whether that is a human user or a parent agent, for the specific information it needs to complete a task. ## The Problem of Missing Context In traditional web automation, agents are expected to operate entirely on their own once given a prompt. But what happens when an agent encounters a step that requires specific, localized knowledge? Imagine giving an agent the prompt: *"Sign up for the mozilla newsletter."* The agent can successfully generate a plan, navigate to the Mozilla website, locate the newsletter signup page, and identify the required input fields. But when it tries to submit the form, it hits a roadblock: it doesn't know your email address or your subscription preferences. Previously, this would result in a failed task or a hallucinated input. The agent was trapped in a silo, unable to request the missing piece of the puzzle. ## Enter Interactive Mode Interactive Mode transforms Pilo from a purely autonomous executor into a collaborative agent. Instead of failing when it lacks information, Pilo can now dynamically halt its loop and prompt the caller for guidance. For our first iteration of Interactive Mode, we are focusing specifically on form completion. When Pilo encounters a form requiring personal data, credentials, or preferences it doesn't possess, it will output a request for input. Once the caller provides the missing information, Pilo directly injects it into the form and resumes the agentic loop to complete the task. ## How It Works Across the Stack Interactive Mode integrates deeply into Pilo's core execution engine rather than relying on the LLM to decide when to ask for help. When Pilo encounters a form, it uses a "fill gate" mechanism to inspect the required fields. If it detects that user input is needed, Pilo bypasses the LLM entirely to pause task execution and request the data. Because Pilo can be run in several different environments, we designed the interactive feedback loop to adapt to how you are using it: * **Pilo Core:** When you are building directly on top of the Pilo library, the core engine expects a simple callback function. When the execution loop pauses, it triggers your callback with the required fields, waiting for your application logic to return the necessary data before resuming. * **Pilo CLI:** For developers testing locally, the Pilo CLI handles this automatically. When the core requests information, the CLI pauses the terminal output and interactively prompts the user for the missing fields right in the console. * **Pilo Server:** For remote execution, the Pilo Server emits an `interactive:form_data:request` event containing the full field data. It utilizes a dedicated WebSocket endpoint (`/pilo/run`), enabling real-time, bidirectional communication to exchange user data while the task is suspended. Furthermore, Pilo captures form validation states directly within its ARIA tree snapshots. If a user submits an invalid email, the agent detects the field error in the snapshot and will automatically trigger a re-prompt for the corrected information. ## Seeing it in Action with Tabstack The Tabstack **/automate** endpoint is built directly on top of the Pilo Server. This means you can test the WebSocket-driven Interactive Mode today using the Tabstack SDK. Here is what it looks like to catch those interactive requests and supply the missing context: ```python from tabstack import Tabstack # Initialize the Tabstack client client = Tabstack( api_key="YOUR_TABSTACK_API_KEY", ) # Start a task with Interactive Mode enabled stream = client.agent.automate( task="signup for the mozilla newsletter", interactive=True, ) # Listen for events in the stream for event in stream: print(event) # Catch the specific event where the agent asks for form data if event.event == "interactive:form_data:request": request_id = event.data.get("requestId") fields = event.data.get("fields", []) print(f"\n--- Interactive input requested (requestId: {request_id}) ---") field_values = [] # Dynamically prompt the user for the missing fields for field in fields: ref = field.get("ref", "") label = field.get("label", "") required = field.get("required", False) prompt = f" {label}{'*' if required else ''}: " value = input(prompt) field_values.append({"ref": ref, "value": value}) # Submit the user's input back to the agent to resume the task response = client.agent.automate_input( request_id, fields=field_values, ) print(f"\n--- Input response: {response} ---\n") ``` By decoupling the reasoning engine from the data source, Interactive Mode allows you to build more adaptable, resilient pipelines. Whether you are typing in the terminal or wiring Pilo into a complex backend where a master orchestrator agent automatically supplies the missing data, the engine easily adapts. ## Just the First Step It is important to note that Interactive Mode is currently in **Beta**. This initial release is highly optimized for form-filling scenarios, but this is only the first step in our vision for collaborative agents. As we continue to test and build out this functionality, Pilo's interactive capabilities will become much more advanced. Future iterations will support complex scenarios like resolving ambiguous instructions mid-task, requesting visual confirmations before executing destructive actions, and handling multi-step interactive workflows. We built Pilo to be the most reliable open-source foundation for web agents, and giving those agents the ability to ask questions is a massive step forward in making them truly useful. Check out the updated documentation on the [Mozilla Pilo GitHub repository](https://github.com/mozilla/pilo) or try out the SDK at [Tabstack.ai](https://tabstack.ai) today. --- ## Tabstack's New Pricing Plans URL: https://tabstack.ai/blog/updated-pricing-plans Date: 2026-04-06 Tabstack's pricing is evolving to give you more control and flexibility. Enjoy clearer, consistent billing with our new free 10k credit Trial, Pay As You Go Individual plan, or predictable monthly subscriptions. Build without interruption and scale at your own pace! ## Unlocking Flexibility Since day one, our goal at Tabstack has been to build tools that make your workflow smoother and more efficient. We have loved watching our community grow and build amazing things using our platform. But as your projects have evolved, we realized our pricing model needed to evolve alongside them. Until now, Tabstack has operated exclusively on prepaid credit packs. While this worked for many, we heard your feedback loud and clear: you want **clearer, consistent billing**, and you want choices that adapt to the way your team actually works. Today, we are thrilled to announce a complete overhaul of our pricing structure, designed to give you ultimate control, predictability, and flexibility. ## What Is New? We are moving away from the strict advanced credit model and introducing a tiered system that grows with you, taking you from your very first API call to enterprise deployments. ### The New Trial Tier We want everyone to experience the power of Tabstack without any friction. Every new user will now start with a **Trial Tier that includes 10,000 free credits**. This allows you to test the waters, build a proof of concept, and see exactly how Tabstack fits into your stack before spending a dime. ### The Individual Plan (Pay As You Go) Got a project with unpredictable volume? The Individual plan is for you. Once you graduate from the Trial Tier, you can opt to simply pay for what you use. You will be billed monthly based strictly on your exact usage. There are no upfront commitments, and you are never paying for credits you do not end up using. ### Predictable Monthly Subscriptions For teams with consistent or high volume needs, we are introducing two new subscription plans. These plans offer a set monthly price, a generous default credit allowance, and a lower cost per credit ratio. | Plan Tier | Monthly Price | Included Monthly Credits | Ideal For | | :---- | :---- | :---- | :---- | | **Team** | $99 / month | 500,000 credits | Growing startups and mid-sized teams with consistent workloads. | | **Pro** | $499 / month | 3,000,000 credits | High-volume applications and established companies scaling fast. | ## Overages and Spend Protection We know that traffic can spike unexpectedly. If you are on the Team or Pro plan and exceed your monthly credit allowance, your workflow will not break. You will seamlessly transition to overage billing so your services keep running smoothly. However, we also want to protect you from unexpected bills. You now have the power to configure your own custom usage limits directly from your dashboard. You can set up alerts to notify you when you reach a certain threshold, or set your own hard caps to pause operations automatically. Additionally, to provide an extra layer of security against massive, surprise bills, we have implemented a platform-level hard cap on all accounts. While you cannot edit this system cap directly, you can easily request an increase from our support team as your project scales. ## Important Details for Existing Users If you are already building with Tabstack, we want to make sure your transition is as smooth as possible. We are providing a 90 day grace period for all existing accounts. During the next 90 days, you will continue to receive your legacy monthly allowance of 50,000 credits, and you can still purchase credit packs just like before. You can choose to upgrade to the Individual plan or one of the new subscription plans at any time during this window. When you upgrade, any existing credit balance you have will be carried over to the new system, and **those credits will not expire**. If you have not transitioned after 90 days, your account will be updated automatically, and your remaining credit balance will still be safely preserved without an expiration date. ## Updated Credit Costs Along with the new plans, we are updating the credit cost per operation to better reflect the scale and processing power of our tools. If you are an existing user in the 90 day grace period, you will continue to enjoy the old pricing. All new users, existing users who choose to upgrade early, and all users after the 90 day window will be subject to the new pricing structure. | Operation | Old Cost (Credits/Action) | New Cost (Credits/Action) | | :---- | :---- | :---- | | **Extract Markdown** | 9 | 10 | | **Extract JSON** | 50 | 100 | | **Generate** | 57 | 250 | | **Automate** | 75 | 500 | | **Research Fast** | 25 | 500 | | **Research Balanced** | 50 | 1,000 | ## Why We Made the Switch (And Why It Is Good For You) We did not just change our pricing to add more options; we redesigned it to solve real pain points we noticed in how users were interacting with the platform. Here is how the new system works in your favor: * **Clearer, Consistent Billing:** You can now budget effectively. Whether you choose the predictability of the Team and Pro tiers or the exactness of the Individual plan, your finance team will thank you. * **Goodbye, "Top-Up Anxiety":** Under the old credit pack system, running out of credits meant your operations paused until someone manually logged in and bought more. Now, your workflow remains entirely uninterrupted. * **Scale at Your Own Pace:** Start for free, scale to the Individual plan as you gain traction, and lock in savings with a subscription tier once your volume is established. You are never forced into a box that does not fit your current size. * **No Bill Shock:** With customizable alerts and platform safety limits, you get the peace of mind that your budget is protected even if your application goes viral overnight. * **Lower Barrier to Entry:** The 10k credit Trial Tier allows developers and creators to experiment freely without needing to pull out a company credit card right away. ## Ready to Explore the New Plans? These new plans are live today! Head over to your dashboard to view your current usage, explore the new tiers, and select the plan that makes the most sense for your next big project. As always, if you have any questions, our support team is ready to help you find the perfect fit. Happy building! --- ## Not Every Page Needs a Browser URL: https://tabstack.ai/blog/fetch-effort-parameter Date: 2026-03-17 We rearchitected how Tabstack fetches pages to be faster by default, and added a new effort parameter that puts you in control. Start lightweight, escalate to full browser rendering only when you need it, and build adaptive pipelines that retry intelligently. ## The Problem with One-Size-Fits-All When you point an extraction API at a URL, you typically have no say in how the page gets fetched. A static documentation page and a JavaScript-heavy single-page application go through the same pipeline, with the same overhead and the same latency. You end up paying a time penalty on simple pages because the system has to assume every page might be complex. The reality is that most pages on the web are straightforward. A news article, a product listing on a server-rendered site, a wiki page: none of these need a full headless browser to extract. But a React dashboard behind client-side rendering absolutely does. You know your target sites. We should let you tell us. ## What Changed We made two changes to how Tabstack handles page fetching. First, we rearchitected the default fetch path to be significantly faster and more reliable. The `"standard"` effort level that every request uses by default is now smarter about how it retrieves content, with better fallback mechanisms and reduced overhead. Second, we added the `effort` parameter to the `/extract/json`, `/extract/markdown`, and `/generate/json` endpoints. This gives you explicit control over how hard Tabstack works to fetch a page before extraction begins. There are three levels: - **`"min"`**: a lightweight fetch with no fallback. This is the fastest option usually under 1 second, ideal for static or server-rendered pages where you know the content is available without JavaScript execution. - **`"standard"`**: the default. A balanced approach with enhanced reliability and fallback mechanisms, typically completing in less than 10 seconds. This will handle most pages, including ones with some JavaScript. Start here for any new integration. - **`"max"`**: full browser rendering. This spins up a headless browser, executes JavaScript, waits for dynamic content to load, and then extracts. It takes 15 to 60 seconds but is necessary for SPAs, pages behind client-side hydration, or sites that load content dynamically. Try this when the other levels are not getting the content you want. ## Using Effort Adding the parameter is straightforward. Here is a basic example that extracts markdown from a JavaScript-heavy page: ```python import os from tabstack import Tabstack client = Tabstack(api_key=os.environ.get("TABSTACK_API_KEY")) # Full browser rendering for a JS-heavy page result = client.extract.markdown( url="https://example.com/spa-dashboard", effort="max" ) print(result.content) ``` For a static docs site where speed matters, drop to `"min"`: ```python result = client.extract.markdown( url="https://docs.example.com/api-reference", effort="min" ) ``` ## Adaptive Effort: Start Fast, Escalate When Needed The most powerful pattern with the effort parameter is adaptive escalation. Instead of guessing the right level upfront, you start with the fastest option and only escalate when the result is insufficient. This gives you the best possible latency on easy pages while still handling complex ones reliably. ```python import os from tabstack import Tabstack client = Tabstack(api_key=os.environ.get("TABSTACK_API_KEY")) def extract_with_adaptive_effort(url, min_length=100): """Extract markdown, escalating effort until we get a good result.""" efforts = ["min", "standard", "max"] for effort in efforts: result = client.extract.markdown(url=url, effort=effort) if result.content and len(result.content) >= min_length: return result return result # Return whatever we got at max effort result = extract_with_adaptive_effort("https://example.com/products") print(result.content) ``` This pattern works well in production pipelines where you process a mix of sites. Simple pages resolve in a couple of seconds at `"min"`, and only the pages that genuinely need browser rendering incur the additional latency. Your average response time drops significantly while your success rate stays high. ## When to Use Each Level Here is a quick guide for choosing the right effort level: - **Use `"min"` when** you are scraping well-known static sites, server-rendered pages, or any URL where you have verified that lightweight fetching returns complete content. Great for high-volume pipelines where speed is critical. - **Use `"standard"` when** you are integrating with a new site or processing a mix of URLs. It handles most of the web reliably without the overhead of full rendering. - **Use `"max"` when** content is missing from lower effort levels, the target is a known SPA or JavaScript-heavy application, or the page loads data asynchronously after initial render. ## Get Started The effort parameter is available now on all extract and generate endpoints. Update to the latest SDK version to use it, or pass `effort` directly in your request body if you are calling the API without an SDK. - **API Reference:** [docs.tabstack.ai](https://docs.tabstack.ai) - **Tabstack Dashboard:** [tabstack.ai](https://tabstack.ai) If you are building pipelines that process diverse URLs, try the adaptive effort pattern. Start fast, escalate smart, and let your code decide how hard to try. --- ## Scaling Web Automation with Pilo and Agentic AI URL: https://tabstack.ai/blog/introducing-pilo-browser-automation Date: 2026-02-24 Building reliable AI web agents is incredibly complex. To solve this, we are open sourcing Pilo. Pilo is a robust execution engine that uses the accessibility tree, smart context compression, and agentic reasoning loops to reliably navigate the modern web. Run it entirely on your own hardware. ## The Chaos of the Modern Web Building reliable web automation is notoriously difficult. Modern websites are dynamic, heavily reliant on JavaScript, and constantly changing. When you add Large Language Models to the mix to create autonomous web agents, the complexity multiplies. You suddenly have to manage browser orchestration, keep token costs down while maintaining full page context, handle flaky networks, and build complex reasoning loops just to get an AI to reliably extract data or click a button. You end up managing massive amounts of infrastructure just to handle a basic reasoning loop. We didn't just stumble upon these problems; we ran into them headfirst while building Tabstack. We needed a reliable engine to power our own /automate endpoint, but nothing off the shelf could handle the chaos of the modern web without constant breakage. So we built our own solution. We call it Pilo. It became the bedrock of our platform, and we realized that developers everywhere need this same robust, transparent foundation. Today, we are very excited to share Pilo as an open source project, enabling anyone to run this powerful web automation engine completely on their own infrastructure. You can find the full source code and documentation on [Github here](https://github.com/mozilla/pilo). ## Giving AI the Steering Wheel At its core, Pilo moves away from rigid scripts and embraces an agentic loop built around Playwright's accessibility tree. Instead of telling a program exactly where to click, you give Pilo a natural language goal. Pilo then takes over and makes decisions at every step based on what it actually sees on the screen. The system operates in a continuous, intelligent loop: - **Observe:** Pilo captures the current page state using the browser's accessibility tree. This provides a semantic, stable structure of the page rather than a chaotic mess of raw HTML tags. - **Decide:** Pilo passes this state to an LLM provider of your choice. The LLM evaluates the context and selects a specific tool to use next. - **Act:** Pilo executes the chosen action, whether that is navigating to a URL, clicking a button, filling out a form, or extracting structured data. Pilo handles all the gritty details under the hood. It features layered retries for flaky navigation, automatic context truncation to prevent context window bloat during long tasks, and a robust error recovery philosophy. If an action fails, Pilo does not simply crash. It feeds the error back to the LLM so the agent can adapt and try a new approach. ![Pilo Agent Loop](/images/blog/browser-ref2.png "By utilizing the accessibility tree, Pilo turns a chaotic webpage into a structured roadmap of labeled elements, making complex browser automation faster and more reliable.") ## Anatomy of an Autonomous Action To understand how Pilo handles the complexities of the web, let's look under the hood at a task like "find the best pizza restaurants in Seattle". **1. The Planning Phase:** Before launching a browser, Pilo forces the LLM to pause and strategize. It calls a create_plan tool to generate a step-by-step path and, crucially, defines strict success criteria. For a search-heavy task like this, the planner might smartly set the starting URL to about:blank, signaling the agent to immediately trigger its search workflow rather than wasting time navigating to a specific homepage. **2. Layered Navigation:** Navigating to a URL is the single most failure-prone operation in browser automation. Pilo wraps navigation in a layered defense system. It starts with timeout escalation, doubling the allowed wait time on each failure. If the network is truly flaky and throws a DNS error, Pilo will automatically kill and restart the entire browser instance to clear any bad state before retrying. **3. Compressing the Matrix:** Once the page loads, Pilo needs to show it to the LLM. Passing raw HTML is too expensive and noisy, so Pilo captures the accessibility tree, a semantic map of the page's structure. It pipes this tree through a compression engine that maps verbose tags like listitem to li, shortens reference IDs, and deduplicates repetitive text. This process slashes token usage by 60 to 80 percent while keeping every interactive element accessible. **4. The Decision Loop:** The LLM reviews the compressed snapshot and selects a tool, such as click. Pilo resolves the reference ID (e.g., e45) to a live Playwright locator and executes the action. If the page shifted and e45 is gone, Pilo catches the InvalidRefException and feeds it back to the LLM as a recoverable error. The agent sees the failure, realizes the page state has changed, and naturally requests a fresh snapshot to try again. **5. Quality Control:** When the agent thinks it's finished and calls done, Pilo doesn't just take its word for it. It triggers an isolated validation step where a second LLM acts as a grader. It compares the final results against the success criteria defined in step one. If the agent found only two restaurants when the plan demanded five, the validator marks it as "partial" quality and sends the agent back to work with specific feedback on what's missing. ## Seeing is Believing: Pilo in Your Browser While Pilo is a powerful backend engine, we wanted to make its capabilities completely tangible. Because the event system decouples the core logic from where it runs, Pilo is built to work seamlessly in different contexts, including a browser extension. You can install the extension, give it a natural language prompt, and literally watch the agentic loop drive your browser in real time. It is one thing to read about accessibility trees and LLM decision making; it is entirely another to sit back and watch an AI navigate, click, and extract data across the live web right in front of your eyes. It provides a perfect sandbox to test tasks and understand exactly how Pilo thinks and operates. ## The Engine Inside Tabstack We built Pilo to be completely standalone. You can run it locally, plug in your own API keys, and use it to power your own applications without ever signing up for Tabstack. Our goal is to provide developers with a reliable, open source foundation for web agents. However, building and scaling this kind of infrastructure in production is incredibly resource intensive. You have to manage compute pools, handle persistent browser sessions, and scale concurrent tasks. This exact challenge is why we built Tabstack in the first place. Pilo is the core engine that underlies our /automate endpoint. If you want to leverage the power and resilience of Pilo but do not want to manage the underlying infrastructure, browser orchestration, or scaling logistics, Tabstack provides all of this out of the box. --- ## Tabstack Research: Verified Answers from the Open Web URL: https://tabstack.ai/blog/tabstack-research-verified-answers Date: 2026-02-03 Tabstack Research moves the autonomous reasoning loop into the infrastructure layer. We handle the discovery, extraction, and verification required to bridge the synthesis gap. Access high-fidelity, structured data with inline citations instead of raw HTML noise or model hallucinations. Web browsing is the hidden tax of AI development. When you connect an agent to the open web, you are forced to stop building AI and start managing the overhead of browser orchestration. You end up debugging JS-rendering, rotating proxies, and writing brittle selectors just to turn the chaotic web into clean inputs. This is why we built Tabstack: to turn browsing into a reliable infrastructure layer. But even with a stable browser fleet, developers face a secondary bottleneck: **The Synthesis Gap**. Fetching raw data from a single URL is an infrastructure problem. Answering a complex question like "Compare Slack vs. Teams retention policies" is a state management and reasoning problem. To answer that, an agent must spawn a fleet of parallel searches, navigate dozens of unvetted URLs, filter out 90% marketing noise, and reconcile conflicting data. Processing this at scale creates a direct conflict between latency and context window density. If you feed your model every raw byte, you burn your token budget on noise. If you truncate too early, you lose the ground truth. Today we are launching **Tabstack Research**. It is a high-level research primitive that moves the autonomous reasoning loop into the infrastructure layer. You give us a goal. We execute the coordinated workflow of discovery, extraction, and verification to return a synthesized report backed by source citations. ## The Drudgery of Scale Most developers begin by wrapping a basic scraper in a loop, but they quickly hit an infrastructure wall. Scaling a research task does not just increase your bandwidth requirements. It multiplies your **orchestration complexity**. When an agent tackles a non-trivial query, it rarely performs a single lookup. It spawns a fan-out of parallel sub-queries. In this scenario, "ten blue links" quickly explode into 100 unvetted URLs. Processing this volume in production requires you to solve three high-stakes problems: 1. **The Concurrency Bottleneck:** Running 100 parallel headless sessions to handle modern, JS-heavy sites requires massive compute overhead. Managing the memory leaks, zombie processes, and proxy rotation needed to avoid rate limits turns into a full-time DevOps job. 2. **The Token Tax:** Consumption is not comprehension. Shoveling 50,000 tokens of raw HTML boilerplate into your model is a recipe for high latency and "lost in the middle" reasoning errors. Without a pre-processing layer, you are paying to process navigation menus and footer links instead of actual data. 3. **The Resolution Logic:** You are forced to build custom logic to reconcile conflicting data points across different domains. You end up spending more time on data cleaning and deduplication than on your core agent logic. The result is a fragile pipeline where engineering cycles are wasted managing a browser fleet instead of refining the product. ## An Adaptive Research Loop We built **Tabstack Research** to move this orchestration logic out of your application and into our specialized browsing layer. When you send us a request, we initiate a multi-phase agentic loop designed to mimic the recursive nature of human research at machine speed. By offloading the "discovery and verification" cycle to our infrastructure, you avoid the complexity of building custom state machines to handle search branching and error correction. Behind the scenes, the system executes a coordinated workflow that treats research as an iterative process rather than a linear fetch: - **Planning and Decomposition:** The system mimics a researcher's initial "mental map" by breaking your goal into targeted sub-questions. It identifies that a true comparison requires hitting distinct data silos: official documentation, enterprise pricing tables, and compliance whitepapers. - **Parallel Execution:** We visit these sites in parallel using our core browsing infrastructure. We perform real-time content extraction, filtering out the DOM noise and marketing fluff that typically bloat a context window. - **Gap Evaluation:** This is the recursive heart of the system. The system evaluates the collected data against the original intent. If it identifies a missing variable or a conflicting date, it detects the gap and triggers a new iteration to hunt down that specific information. - **Verification and Termination**: The loop concludes when the system determines the claims are sufficiently verified against the source text or when it reaches the iteration limit for the selected mode. For example, while the system stops early if it has all the required information, **Balanced Mode** allows for up to three rounds of recursive discovery to ensure the final output is grounded in retrieved evidence rather than model weights. ## Anatomy of a Request To see how this works in production, consider a complex research task: **"Compare enterprise data retention policies for Slack vs. Microsoft Teams."** ```python import os from tabstack import Tabstack # Initialize the client tabs = Tabstack(api_key=os.getenv('TABSTACK_API_KEY')) # Ask a research question result = await tabs.agent.research( query="Compare enterprise data retention policies for Slack vs. Microsoft Teams.", mode="balanced", ) print(result) ``` If you pass this query to a standard LLM or a basic search-based agent, you will likely get generic advice or a surface-level summary of the marketing pages. Here is how the **Tabstack Research** loop executes it: ### The Planning Phase The system deconstructs the high-level prompt into a series of technical primitives. It identifies that "retention" is not a single value. It generates sub-queries for "Teams Purview chat retention," "Slack Enterprise Grid file storage limits," and "M365 E5 compliance overrides." ### The Discovery Phase It executes these searches in parallel. It prioritizes official documentation like `learn.microsoft.com` and `slack.com`. Critically, our browsing layer ignores the SEO-optimized "Top 10" blog posts from 2023 that no longer reflect the 2026 pricing and policy landscape. ### The Recursive Pivot During extraction, the system hits a common stumbling block. Initial results for Teams mention a 30-day default retention. However, our evaluation step detects a critical gap: **Files shared in Teams are actually stored in SharePoint and OneDrive, which often have conflicting retention policies.** A standard agent would miss this nuance. Tabstack Research detects the ambiguity and triggers a targeted, second-pass search to clarify the "Conflict Resolution" logic between these interdependent Microsoft services. The engine identifies that while messages appear in Teams, the actual preservation happens in a hidden "SubstrateHolds" folder within the "Exchange Recoverable Items" folder. This is a nuance that dictates how legal holds are actually applied and accessed. ### The Verification Phase The final report is compiled with specific, time-sensitive data points that are verified against the source text to mitigate the risk of model hallucinations: - **Service Interdependence:** It clarifies that while Teams manages the message metadata, SharePoint manages the physical file retention. - **Price Adjustments:** It catches upcoming M365 price increases scheduled for July 1, 2026 (e.g., E3 moving from $36 to $39). - **Infrastructure Transitions:** It identifies technical shifts scheduled for late 2025 and 2026, such as Slack implementing a two-year rolling retention policy for audit logs and Teams migrating private channel messages to group mailboxes. - **Retention Granularity:** It identifies that Slack offers channel-level control, whereas Microsoft favors a centralized "longest-period-wins" logic. Every single claim is returned with an inline citation and a direct link to the specific documentation used for the extraction. ## Verifiable Claims The output is not a block of prose. It is a structured synthesis designed for downstream application consumption. We prioritize high-fidelity data over general summaries. ### A Note on AI-Generated Research LLMs can be remarkably confident even when they are incorrect. While Tabstack Research is built to minimize errors through recursive browsing and cross-referencing, no automated system is perfect. This is why we provide thorough, inline citations for every claim. We believe the value of an AI agent is not in replacing human judgment, but in providing the clear, sourced evidence required for you to make an informed decision. ### Grounded Attribution In an era of black box AI, we have moved the burden of proof from the model to the evidence. Every claim in a Tabstack Research report is backed by an inline citation. If the system reports that Slack Enterprise Grid requires a custom quote for the Discovery API, it provides the specific URL and the text fragment used to verify that fact. This allows your application to provide "click to verify" functionality for your end users. ### Conflict Resolution The system does not just aggregate data. It reconciles it. The loop often encounters conflicting information. For example, one source may claim Slack retention is limited to 90 days while another cites indefinite storage for paid tiers. The system resolves this by verifying the context, such as Free tier visibility limits versus Enterprise Grid storage policies. The result is a reconciled data point that prioritizes the most authoritative source. ### Structured Data Primitives Because we strip the DOM noise and marketing boilerplate during the execution phase, the final synthesis is high-density. You receive clean and actionable data points: - **Logic Mapping:** You learn that Slack offers granular channel level control while Microsoft favors a centralized "longest period wins" conflict logic. - **Temporal Accuracy:** You get specific price points like Slack's \~$15 to $45 per user per month (custom pricing) versus Microsoft 365's $36 for E3 and $60 for E5 list price, which accounts for upcoming 2026 shifts. - **Infrastructure Insights:** You receive technical specifics that impact compliance. This includes the fact that Teams video clips and embedded images are retained, but code snippets and voice memos are often excluded from standard retention policies. By the time the data reaches your application, the heavy lifting of discovery and extraction is complete. You are left with a verified asset that is ready to be stored in a database or presented in a UI. ## Continuum of Execution: Choosing Your Speed Research is not a one-size-fits-all operation. Different use cases require different levels of recursion and computational depth. We offer two primary modes to help you manage the balance between latency and comprehensiveness. - **Fast Mode:** This is optimized for instant answers and populating UI tooltips. It uses lightweight fetches and high-priority search excerpts to return an answer in **10 to 30 seconds**. It is ideal for "what is" questions where the answer is likely available in the primary search result or a single documentation page. - **Balanced Mode:** This triggers our full agentic loop. The system visits, renders, and analyzes pages deeply. It performs the multi-pass gap evaluation we described earlier. Typically delivering a comprehensive report in **1 to 2 minutes**, this mode is designed for complex comparisons and multi-source verification. ## Built for Trust In an era of opaque AI, we have taken a different path. As part of the **Mozilla** ecosystem, Tabstack is built on a foundation of privacy and responsibility. This is not just a marketing claim. It is a technical constraint on how we handle your data. - **Zero Training:** We do not train AI models on your research queries or the data we collect for you. Your intellectual property remains yours. - **Ephemeral by Design:** Your research data is treated as ephemeral. It is used to execute the specific task and is then discarded from our active processing memory. - **Full Verifiability:** By providing full citations and source metadata, we move the burden of proof from the model to the evidence. This allows you to build applications where the AI can be audited in real time. ## What You Can Finally Ask When you stop worrying about how to get the data, you can finally focus on asking the right questions. By moving the orchestration of web browsing into the infrastructure layer, the scope of what your agents can accomplish expands significantly. You can build agents that handle: - **Competitive Intelligence:** Compare enterprise pricing and data retention policies for Slack versus Teams. - **Regulatory Compliance:** Extract mandatory data residency requirements for healthcare SaaS in the European Union. - **Due Diligence:** Identify strategic supply chain risks in the latest 10-K filings from NVIDIA. - **Strategic Analysis:** Summarize the major commercial partnerships announced by Salesforce in the last year. We take care of the hard parts. We handle the browsing, the parsing, and the noise reduction so you can focus on the reasoning that makes your agent unique. Ready to build? [Sign up](https://console.tabstack.ai/signup) to get started and receive 50,000 free credits per month or explore the technical implementation in our [documentation](https://docs.tabstack.ai/api/research-v-1). --- ## Tabstack: Browsing Infrastructure for AI Agents URL: https://tabstack.ai/blog/intro-browsing-infrastructure-ai-agents Date: 2026-01-14 Building browser infrastructure is a hidden tax on AI development. Tabstack is the developer API that turns the chaotic web into clean inputs for your agents. From rendering SPAs to complex automation, we handle the orchestration so you can focus on reasoning. Reliable, scalable, and built by Mozilla. ## **The Web Access Challenge for Agentic AI** Every AI engineer eventually hits the same infrastructure wall. You build an agent that reasons perfectly in a sandbox, but the moment you connect it to the open web to perform a complex task, you are forced to stop building AI and start managing the massive overhead of browser orchestration. What starts as a simple script to fetch a URL quickly spirals into a full-time job. You find yourself managing headless browsers just to handle client-side rendering. You spend hours writing brittle selectors to parse raw HTML, only to watch them break the moment a site updates its layout. Instead of getting clean data, you are stuck debugging a chaotic mess of tags and attributes. This is the hidden tax of autonomous systems. The web is messy, unpredictable, and designed for human eyes rather than software agents. When teams try to bring browsing infrastructure to production themselves, they end up spending more cycles handling session reliability and patching browser fleets than they do improving their agent's intelligence. **Suddenly, you are not building AI anymore. You are maintaining a fragile, expensive web layer.** We built Tabstack to turn that chaotic infrastructure into a solved problem. ## **Web Browsing as Infrastructure** Today, we are publicly launching **Tabstack**, the web execution layer for AI systems. Tabstack is a developer-focused API that enables AI agents to extract, generate, and automate web content. It acts as a single abstraction layer that collapses the complexity of web automation. Tabstack gives your agents the ability to navigate pages, interact with real user flows, and turn the chaotic web into clean, actionable inputs. **We treat browsing as a distinct infrastructure problem.** Developers should not have to manually assemble network layers, proxies, and rendering engines just to reliably interact with a website. Whether you need to render complex single-page applications or navigate multi-step user workflows, Tabstack handles the heavy lifting of orchestration and reliability. We provide the stability needed to operate at scale so you can focus on building the intelligence that drives your agents. ## **A Continuum of Execution** Under the hood, Tabstack dynamically routes every request to the most efficient extraction method. It does not just blindly fire up a heavy browser for every request. Instead, it intelligently selects the lightest viable method by starting with raw HTTP fetches and escalating to full browser sessions only when edge cases or complex interactions demand it. This adaptive approach delivers: - **Speed:** Process simple pages instantly via lightweight fetches. - **Reliability:** Handle complex single-page apps (SPAs) with a robust automation engine that manages scrolling, rendering, and wait times. - **Resilience:** Maintain high success rates with **intelligent routing and retry logic**, without your team needing to manage the underlying infrastructure. ## **Turning the Web into Data** Tabstack provides a high-level interface for both understanding and acting on web content. By filtering out the noise before it reaches your model, **Tabstack prevents you from burning your budget on parsing raw HTML.** We ensure every token in your context window is spent on reasoning, not rendering. Whether you are feeding a RAG (Retrieval-Augmented Generation) pipeline or building an autonomous agent, we have an endpoint for the job: - **/markdown:** Instantly convert any URL's page content into clean Markdown, perfect for feeding into LLM context windows. - **/json:** Transform unstructured web pages into structured JSON objects using a schema you provide. This is ideal for turning product listings or news articles into database-ready records. - **/automate:** Instruct Tabstack to navigate, click, type, and handle complex interactions to complete multi-step workflows. This allows your agent to execute tasks just like a human user. ## **Giving Your Agent Eyes** We designed our API to be dead simple. Just import the SDK and tell Tabstack what to do. Here is how you can automate a complex task, like researching the top posts on Hacker News, in just a few lines of Python: ```python import os from tabstack import Tabstack # Initialize the client tabs = Tabstack(api_key=os.getenv('TABSTACK_API_KEY')) # Run an autonomous task result = await tabs.agent.automate( url="https://news.ycombinator.com", task="Go through 5 pages of the top posts. For each post, determine the website it is from and group all the posts by website. Return the list of the 10 websites with the most posts." ) print(result) ``` In the background, Tabstack's engine navigates pagination, interprets the content, aggregates the results, and returns the final answer. Your agent stays focused on reasoning while we handle the browsing. ## **Built for Production** Tabstack is designed for teams building AI systems that need to work reliably beyond a prototype. You can start quickly with a simple API and SDKs, then scale without re-architecting your stack or taking on new operational burden. There is no need to manage browser fleets, network layers, or constantly shifting edge cases. We are seeing developers use Tabstack to build incredible things that go far beyond simple data retrieval: - **Autonomous Research:** Agents that "read" the news or documentation and synthesize answers rather than hallucinating from outdated training sets. - **Action-Oriented Agents:** Systems that can book reservations, check flight statuses, or fill out forms on your behalf. - **Live Market Analysis:** Tools that navigate e-commerce sites to track pricing and availability in real-time. ## **Privacy You Can Trust (Backed by Mozilla)** We know that giving an external service access to your web traffic requires trust. **Tabstack is built by Mozilla.** In an era where data is often harvested without consent, we are building this capability in a way that reflects Mozilla's values. - **No Training on Your Data:** Mozilla does **not** train AI systems on the data that is collected by Tabstack. - **Ephemeral by Default:** We practice strict data minimization. Internally, we use the returned data solely to execute the requested task. Customer data is treated as ephemeral. - **Secure by Design:** We utilize end-to-end TLS and scoped API keys to support the secure handling of sensitive information, giving you control and peace of mind. We are building a tool for developers to access the web responsibly, rather than a mass data harvester for model training. ## **Get Started** The era of static, isolated AI is over. It is time to let your agents browse. Tabstack is now in public early access. We want to see what you can build when **the entire web is an API.** - **Website:** [https://tabstack.ai](https://tabstack.ai) - **Documentation:** [https://docs.tabstack.ai](https://docs.tabstack.ai) - **Twitter/X:** [@tabstack](https://x.com/tabstack) Let's see what you build.