
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/agentsv0.13 or later- Zod 4
- A Tabstack API key from console.tabstack.ai
npm install @tabstack/openai-agents @openai/agents zodexport 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
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 stringextract_page_content:urlresearch_question:querygenerate_structured_data:url,instructions,json_schema_jsonautomate_browser_task:task, plus optionalurl,guardrails,data,country,max_iterations,max_validation_attempts
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:
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, @tabstack/ai, @tabstack/eve, and the Python langchain-tabstack, so your instructions carry over if this workload moves.
The package is on npm as @tabstack/openai-agents, and the full API reference is at docs.tabstack.ai.
Ready to build? Sign up for 10,000 free credits, no card required, and hand your agent its first typed extraction. Full API in the docs.