--- title: Autonomous Research | Tabstack description: 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 - [TypeScript](#tab-panel-113) - [Python](#tab-panel-114) ``` 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 `/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](/api-reference/agent/agent-research/index.md) 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 Switch on `event.event` and the SDK narrows `event.data` for each case. A minimal progress reporter looks like this (setup as in [Quickstart](#quickstart)): - [TypeScript](#tab-panel-115) - [Python](#tab-panel-116) ``` 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 `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'`. - [TypeScript](#tab-panel-117) - [Python](#tab-panel-118) ``` 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. ``` ``` 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 research balanced_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 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. - [TypeScript](#tab-panel-119) - [Python](#tab-panel-120) ``` 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 ### Competitive intelligence Research a competitor’s current pricing and limits without manually visiting their documentation: - [TypeScript](#tab-panel-121) - [Python](#tab-panel-122) ``` 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, timezone from 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 Pull together recent activity on a company before an outreach or sales call: - [TypeScript](#tab-panel-123) - [Python](#tab-panel-124) ``` 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 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: - [TypeScript](#tab-panel-125) - [Python](#tab-panel-126) ``` 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` | 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 | 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 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. - [TypeScript](#tab-panel-127) - [Python](#tab-panel-128) ``` 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 Tabstack from 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") ```