Autonomous Research
Run multi-source research from a single API call. /research handles source selection, synthesis, and citations. No orchestration code required.
Run multi-source research from a single API call. /research handles source selection, synthesis, and citations. No orchestration code required.
Quickstart
Section titled “Quickstart”import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
const stream = await client.agent.research({ query: 'What are the main approaches to browser automation for AI agents?', mode: 'fast',})
for await (const event of stream) { if (event.event === 'complete') { console.log(event.data.report)
const cited = event.data.metadata.citedPages ?? [] console.log(`\nCited ${cited.length} sources:`) for (const page of cited) { console.log(`- ${page.title ?? '(untitled)'}: ${page.url}`) } }
if (event.event === 'error') { throw new Error(event.data.error.message) }}from tabstack import Tabstack
client = Tabstack()
# Primary Python pattern: iterate the stream directly. The SDK handles# SSE framing internally.for event in client.agent.research( query="What are the main approaches to browser automation for AI agents?", mode="fast",): if event.event == "complete": print(event.data.report)
cited = event.data.metadata.cited_pages or [] print(f"\nCited {len(cited)} sources:") for page in cited: print(f"- {page.title or '(untitled)'}: {page.url}") elif event.event == "error": raise RuntimeError(event.data.error.message)Understanding the stream
Section titled “Understanding the stream”/research always streams via Server-Sent Events. Every call returns a stream. There is no non-streaming mode.
The SDK models the stream as a discriminated union: each event has an event field (a string literal) and a data payload whose shape depends on the event name. Switch on event.event and the SDK narrows event.data to the correct type automatically.
The events you’ll usually care about:
| Event | When | data shape (highlights) |
|---|---|---|
start | Once, at the beginning | { message, timestamp } |
planning:start / planning:end | Planning phase boundaries | { message, timestamp, ... } |
iteration:start | Each search iteration begins | { iteration, maxIterations, queries, message, timestamp } |
iteration:end | Each search iteration ends | { iteration, isLast, stopReason?, message, timestamp } |
searching:start / searching:end | Fetching and reading sources | { message, timestamp, ... } |
writing:start / writing:end | Synthesizing the final report | { message, timestamp, ... } |
complete | Once, at the end | { report, metadata, message, timestamp } |
error | Task-level failure | { error: { message, name, stack? }, activity?, iteration?, message, timestamp } |
Balanced mode emits a richer set of progress events — prefetching:*, analyzing:*, following:*, evaluating:*, outlining:*, and judging:* — around the iteration loop. The API reference lists every variant.
The error event is a task-level failure delivered inside the stream, not an HTTP error. Handle it explicitly: if you only listen for complete, failures will produce no output.
Handling events
Section titled “Handling events”Switch on event.event and the SDK narrows event.data for each case. A minimal progress reporter looks like this (setup as in Quickstart):
for await (const event of stream) { switch (event.event) { case 'start': console.log(event.data.message) break case 'iteration:start': console.log(`iteration ${event.data.iteration}/${event.data.maxIterations}`) break case 'complete': console.log('\n' + event.data.report) break case 'error': throw new Error(event.data.error.message) }}for event in stream: match event.event: case "start": print(event.data.message) case "iteration:start": print(f"iteration {event.data.iteration}/{event.data.max_iterations}") case "complete": print("\n" + event.data.report) case "error": raise RuntimeError(event.data.error.message)The mode parameter
Section titled “The mode parameter”mode controls the depth-vs-speed tradeoff.
| Mode | Speed | Sources consulted | Use when |
|---|---|---|---|
'fast' | Faster | Fewer | Default. Time-sensitive queries where a quick answer is sufficient. |
'balanced' | More thorough | More | High-stakes research where breadth matters. Requires a paid plan and emits additional progress events (prefetching:*, analyzing:*, following:*, evaluating:*, outlining:*, judging:*). |
Default is 'fast'. Omitting mode produces the same result as setting mode: 'fast'.
import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
// Quick answer for time-sensitive use cases (default mode)const fastStream = await client.agent.research({ query: 'What are the current funding rounds in AI infrastructure?', mode: 'fast',})
for await (const event of fastStream) { if (event.event === 'complete') { console.log(event.data.report) } if (event.event === 'error') { throw new Error(event.data.error.message) }}
// Thorough answer for high-stakes researchconst balancedStream = await client.agent.research({ query: 'What are the main regulatory approaches to AI in the EU and US?', mode: 'balanced',})// Balanced mode uses the same iteration pattern, plus emits the richer progress events listed above.from tabstack import Tabstack
client = Tabstack()
# Quick answer for time-sensitive use cases (default mode)fast_stream = client.agent.research( query="What are the current funding rounds in AI infrastructure?", mode="fast",)
for event in fast_stream: if event.event == "complete": print(event.data.report) elif event.event == "error": raise RuntimeError(event.data.error.message)
# Thorough answer for high-stakes researchbalanced_stream = client.agent.research( query="What are the main regulatory approaches to AI in the EU and US?", mode="balanced",)# Balanced mode uses the same iteration pattern, plus emits the richer progress events listed above.Working with citations
Section titled “Working with citations”The complete event’s data.metadata.citedPages (TypeScript) / data.metadata.cited_pages (Python) lists every source the agent actually cited in its report. Each entry has guaranteed id, url, claims (the specific statements drawn from that page), and sourceQueries / source_queries (the search queries that surfaced it). Fields like title, summary, relevance, and reliability are optional — present when the research pipeline populates them.
import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
async function research(query: string) { const stream = await client.agent.research({ query, mode: 'fast' })
for await (const event of stream) { if (event.event === 'error') { throw new Error(event.data.error.message) }
if (event.event === 'complete') { return { report: event.data.report, sources: event.data.metadata.citedPages ?? [], } } }
throw new Error('Stream ended without a complete event')}
const result = await research('What are the main approaches to browser automation for AI agents?')
console.log(result.report)console.log(`\nCited ${result.sources.length} sources:`)result.sources.forEach((s, i) => console.log(`${i + 1}. ${s.title ?? '(untitled)'}\n ${s.url}`))from tabstack import Tabstack
client = Tabstack()
def research(query: str): for event in client.agent.research(query=query, mode="fast"): if event.event == "error": raise RuntimeError(event.data.error.message)
if event.event == "complete": return { "report": event.data.report, "sources": event.data.metadata.cited_pages or [], }
raise RuntimeError("Stream ended without a complete event")
result = research("What are the main approaches to browser automation for AI agents?")
print(result["report"])print(f"\nCited {len(result['sources'])} sources:")for i, s in enumerate(result["sources"], 1): print(f"{i}. {s.title or '(untitled)'}\n {s.url}")Use cases
Section titled “Use cases”Competitive intelligence
Section titled “Competitive intelligence”Research a competitor’s current pricing and limits without manually visiting their documentation:
import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
async function getPricingIntel(competitor: string) { const stream = await client.agent.research({ query: `What are ${competitor}'s current pricing plans, rate limits, and free tier details?`, mode: 'fast', nocache: true, // pricing changes frequently; skip cache })
for await (const event of stream) { if (event.event === 'error') { throw new Error(event.data.error.message) }
if (event.event === 'complete') { return { summary: event.data.report, sources: event.data.metadata.citedPages ?? [], retrievedAt: new Date().toISOString(), } } }
throw new Error('No result returned')}from datetime import datetime, timezonefrom tabstack import Tabstack
client = Tabstack()
def get_pricing_intel(competitor: str): for event in client.agent.research( query=f"What are {competitor}'s current pricing plans, rate limits, and free tier details?", mode="fast", nocache=True, # pricing changes frequently; skip cache ): if event.event == "error": raise RuntimeError(event.data.error.message)
if event.event == "complete": return { "summary": event.data.report, "sources": event.data.metadata.cited_pages or [], "retrieved_at": datetime.now(timezone.utc).isoformat(), }
raise RuntimeError("No result returned")Prospect research
Section titled “Prospect research”Pull together recent activity on a company before an outreach or sales call:
import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
async function getCompanyBriefing(company: string) { const stream = await client.agent.research({ query: `What has ${company} announced or shipped in the last 90 days? Include funding, product launches, and hiring signals.`, mode: 'fast', })
for await (const event of stream) { if (event.event === 'error') { throw new Error(event.data.error.message) }
if (event.event === 'complete') { return { briefing: event.data.report, sources: event.data.metadata.citedPages ?? [], } } }
throw new Error('No result returned')}from tabstack import Tabstack
client = Tabstack()
def get_company_briefing(company: str): for event in client.agent.research( query=f"What has {company} announced or shipped in the last 90 days? Include funding, product launches, and hiring signals.", mode="fast", ): if event.event == "error": raise RuntimeError(event.data.error.message)
if event.event == "complete": return { "briefing": event.data.report, "sources": event.data.metadata.cited_pages or [], }
raise RuntimeError("No result returned")Market landscape questions
Section titled “Market landscape questions”Answer open-ended questions about a space where the answer spans many sources. This example also shows a simple progress indicator using the iteration events:
import Tabstack from '@tabstack/sdk'
const client = new Tabstack()
const stream = await client.agent.research({ query: 'What are the main approaches to browser automation for AI agents, and how do they differ?', mode: 'fast',})
for await (const event of stream) { if (event.event === 'iteration:start') { process.stdout.write(`\rIteration ${event.data.iteration}/${event.data.maxIterations}...`) }
if (event.event === 'complete') { console.log('\n\n' + event.data.report) }
if (event.event === 'error') { throw new Error(event.data.error.message) }}from tabstack import Tabstack
client = Tabstack()
for event in client.agent.research( query="What are the main approaches to browser automation for AI agents, and how do they differ?", mode="fast",): if event.event == "iteration:start": print( f"\rIteration {event.data.iteration}/{event.data.max_iterations}...", end="", flush=True, ) elif event.event == "complete": print("\n\n" + event.data.report) elif event.event == "error": raise RuntimeError(event.data.error.message)When to use /research vs /extract/json
Section titled “When to use /research vs /extract/json”| Situation | Use |
|---|---|
| You know the exact URL and want specific fields from it | client.extract.json() |
| You have a question that requires synthesizing multiple sources | client.agent.research() |
| You want clean markdown from one page | client.extract.markdown() |
| You need to answer a question about a topic, not a specific page | client.agent.research() |
| You want AI to transform content from a known URL | client.generate.json() |
The key distinction: /research is for questions where you don’t know which sources hold the answer. /extract/json is for structured extraction when you already have the URL.
Parameters
Section titled “Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
query | string | required | The research question |
mode | 'fast' | 'balanced' | 'fast' | Controls depth vs. speed. 'balanced' requires a paid plan. |
nocache | boolean | false | Force fresh results, bypass cache |
fetch_timeout | number | — | Timeout in seconds for fetching individual web pages |
Error handling
Section titled “Error handling”Two failure modes to distinguish:
- HTTP-level errors (bad API key, rate limit, permission denied) throw SDK exceptions before the stream opens. Catch them with
try/exceptaround the call. - Task-level failures arrive as
errorevents inside the stream.event.data.erroris an object withmessage,name, and optionalstack;event.data.activitytells you which phase failed.
In rare cases the error event may arrive without a populated error field — defensively fall back when that happens. The example below uses optional chaining (TS) / getattr (Python) so an unpopulated error doesn’t crash the handler.
import Tabstack, { RateLimitError, AuthenticationError } from '@tabstack/sdk'
const client = new Tabstack()
try { const stream = await client.agent.research({ query: 'What are the current pricing models for cloud browser APIs?', mode: 'fast', })
for await (const event of stream) { if (event.event === 'error') { // Task-level failure -- the agent could not complete the research. // The `error` field is typed as required but can arrive unpopulated; fall back defensively. const message = event.data.error?.message ?? 'unknown error' throw new Error( `Research failed during ${event.data.activity ?? 'unknown phase'}: ${message}`, ) }
if (event.event === 'complete') { console.log(event.data.report) } }} catch (err) { if (err instanceof RateLimitError) { console.error('Rate limit hit -- retry after a pause') } else if (err instanceof AuthenticationError) { console.error('Invalid API key -- check TABSTACK_API_KEY') } else { throw err }}from tabstack import Tabstackfrom tabstack import RateLimitError, AuthenticationError
client = Tabstack()
try: for event in client.agent.research( query="What are the current pricing models for cloud browser APIs?", mode="fast", ): if event.event == "error": # Task-level failure -- the agent could not complete the research. # The `error` field is typed as required but can arrive as None; tolerate it. activity = event.data.activity or "unknown phase" message = getattr(event.data.error, "message", None) or "unknown error" raise RuntimeError(f"Research failed during {activity}: {message}")
if event.event == "complete": print(event.data.report)
except RateLimitError: print("Rate limit hit -- retry after a pause")except AuthenticationError: print("Invalid API key -- check TABSTACK_API_KEY")