Streaming Patterns
How to handle Server-Sent Event streams from /automate and /research across TypeScript, Python, and different runtime environments.
The /automate and /research endpoints always stream via Server-Sent Events (SSE). This guide covers how to handle that stream correctly across TypeScript and Python, and in different runtime environments.
Why these endpoints stream
Section titled “Why these endpoints stream”Automation and research tasks take time: seconds to minutes. SSE lets you show real-time progress to users, handle partial results, and detect failures early rather than waiting for a timeout.
There is no non-streaming mode for /automate or /research. Every call streams.
TypeScript: Node.js
Section titled “TypeScript: Node.js”The TypeScript SDK returns an async iterable stream. Iterate with for await. The SDK models the stream as a discriminated union, so switching on event.event narrows event.data to the correct type — no casts needed:
import Tabstack from '@tabstack/sdk'
const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY })
const stream = await client.agent.research({ query: 'What are the current pricing models for cloud browser APIs?', mode: 'balanced'})
for await (const event of stream) { switch (event.event) { case 'iteration:start': process.stdout.write( `\r[iteration ${event.data.iteration}/${event.data.maxIterations}]`, ) break case 'complete': console.log('\n\n' + event.data.report) break case 'error': console.error('\nError:', event.data.error?.message ?? 'unknown error') break }}/research emits a rich set of progress events — planning:*, iteration:*, searching:*, writing:*, and (on balanced mode) additional prefetching:*, analyzing:*, following:*, evaluating:*, outlining:*, judging:* events. Switch on the ones you want to display; ignore the rest. The error event’s data.error is an object with message, name, and optional stack.
TypeScript: Edge Runtimes (Vercel Edge, Cloudflare Workers)
Section titled “TypeScript: Edge Runtimes (Vercel Edge, Cloudflare Workers)”Edge runtimes support the SDK natively. The SDK uses fetch under the hood, which is available globally in edge environments:
import Tabstack from '@tabstack/sdk'
export const runtime = 'edge'
export async function GET() { const client = new Tabstack({ apiKey: process.env.TABSTACK_API_KEY })
const stream = await client.agent.research({ query: 'What are the latest LLM benchmarks?', mode: 'fast' })
// Forward the SSE stream to the browser const encoder = new TextEncoder() const readable = new ReadableStream({ async start(controller) { for await (const event of stream) { const chunk = `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n` controller.enqueue(encoder.encode(chunk))
if (event.event === 'complete' || event.event === 'error') { controller.close() break } } } })
return new Response(readable, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', } })}TypeScript: Bun
Section titled “TypeScript: Bun”Bun supports the SDK natively. Same pattern as Node.js:
import Tabstack from '@tabstack/sdk'
const client = new Tabstack({ apiKey: Bun.env.TABSTACK_API_KEY })
const stream = await client.agent.automate({ task: 'Extract the top 10 repositories from GitHub trending', url: 'https://github.com/trending', guardrails: 'Browse and extract only.'})
for await (const event of stream) { if (event.event === 'complete') { console.log(JSON.stringify(event.data, null, 2)) }}Python: Synchronous
Section titled “Python: Synchronous”The primary Python pattern iterates the stream directly. The SDK handles SSE framing (event: and data: lines) internally — you don’t need to parse raw lines. event.data is a typed model; access fields as attributes, not with .get():
import osfrom tabstack import Tabstack
client = Tabstack(api_key=os.environ.get("TABSTACK_API_KEY"))
for event in client.agent.research( query="What are the current pricing models for cloud browser APIs?", mode="balanced",): if event.event == "complete": print(event.data.report) elif event.event == "error": message = getattr(event.data.error, "message", None) or "unknown error" print("Error:", message)The SDK also exposes
client.agent.with_streaming_response.research(...)as a context manager for cases where you need the rawhttpx.Response(custom timeout handling, manual line iteration). The direct iterator above is the recommended pattern for nearly all consumers.
Python: Async
Section titled “Python: Async”import osimport asynciofrom tabstack import AsyncTabstack
async def run_research(): async with AsyncTabstack(api_key=os.environ.get("TABSTACK_API_KEY")) as client: stream = await client.agent.research( query="What are the latest GPU benchmarks?", mode="fast", ) async for event in stream: if event.event == "complete": return event.data.report if event.event == "error": message = getattr(event.data.error, "message", None) or "unknown error" raise RuntimeError(message)
result = asyncio.run(run_research())Always handle the error event
Section titled “Always handle the error event”Across all languages: always handle error events. An SSE error is a task-level failure that arrives in the stream, not an HTTP error. If you only handle complete, failures will silently produce no output.
// Minimal correct handler (for /research)for await (const event of stream) { if (event.event === 'complete') { return event.data.report } if (event.event === 'error') { throw new Error(event.data.error?.message ?? 'unknown error') }}