Skip to content
Get started

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.


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)
}
}

/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:

EventWhendata shape (highlights)
startOnce, at the beginning{ message, timestamp }
planning:start / planning:endPlanning phase boundaries{ message, timestamp, ... }
iteration:startEach search iteration begins{ iteration, maxIterations, queries, message, timestamp }
iteration:endEach search iteration ends{ iteration, isLast, stopReason?, message, timestamp }
searching:start / searching:endFetching and reading sources{ message, timestamp, ... }
writing:start / writing:endSynthesizing the final report{ message, timestamp, ... }
completeOnce, at the end{ report, metadata, message, timestamp }
errorTask-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.

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)
}
}

mode controls the depth-vs-speed tradeoff.

ModeSpeedSources consultedUse when
'fast'FasterFewerDefault. Time-sensitive queries where a quick answer is sufficient.
'balanced'More thoroughMoreHigh-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 research
const 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.

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}`))

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')
}

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')
}

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)
}
}

SituationUse
You know the exact URL and want specific fields from itclient.extract.json()
You have a question that requires synthesizing multiple sourcesclient.agent.research()
You want clean markdown from one pageclient.extract.markdown()
You need to answer a question about a topic, not a specific pageclient.agent.research()
You want AI to transform content from a known URLclient.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.


ParameterTypeDefaultDescription
querystringrequiredThe research question
mode'fast' | 'balanced''fast'Controls depth vs. speed. 'balanced' requires a paid plan.
nocachebooleanfalseForce fresh results, bypass cache
fetch_timeoutnumberTimeout in seconds for fetching individual web pages

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/except around the call.
  • Task-level failures arrive as error events inside the stream. event.data.error is an object with message, name, and optional stack; event.data.activity tells 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
}
}