How to Use the Automate Endpoint
Automating complex, multi-step browser workflows has traditionally been brittle and time-consuming. The Tabstack API automate endpoint solves this by letting you describe your goal in natural language. An AI agent interprets your task, navigates websites, interacts with elements, and extracts data, all while streaming real-time progress updates.
This approach moves beyond simple, static extraction to handle dynamic, interactive tasks. Use it for:
- Complex web extraction requiring clicks, navigation, and stateful interaction.
- Automated form submission and multi-step workflows.
- Multi-page data collection, including pagination and “load more” buttons.
- Dynamic content extraction from SPAs (Single Page Applications).
- Price monitoring and competitive research that requires site interaction.
- Testing and validating web application workflows.
Key Capabilities:
- Natural Language Control: Execute tasks using plain-English instructions.
- AI-Powered Interaction: The agent can click, type, scroll, and navigate.
- Real-Time Streaming: Get live feedback as the agent works.
- Data & Context: Pass in JSON data for form-filling.
- Interactive Mode: Let the agent pause and request user input for forms requiring personal data.
- Safety Guardrails: Define “do-not-do” rules for safe execution.
The Tabstack SDKs are the recommended way to call automate. client.agent.automate(...) returns a stream of typed events that you iterate with for await ... of (TypeScript) or for ... in (Python), with no SSE framing to parse yourself. This guide is SDK-first. If you are not using an SDK, the curl and raw HTTP path is preserved in Using the raw HTTP API directly at the end.
Prerequisites
Section titled “Prerequisites”Before you can use automate, you’ll need:
- A Tabstack API key. Create one in the console and set it as the
TABSTACK_API_KEYenvironment variable. The Quickstart walks through the full setup. - An installed SDK (recommended):
@tabstack/sdkfor TypeScript ortabstackfor Python. See the TypeScript and Python quickstarts. For non-SDK use, any client that can read atext/event-streamresponse works (see the raw HTTP appendix). - A clear task: a specific, natural language description of your goal.
Both SDKs read TABSTACK_API_KEY from the environment automatically, so new Tabstack() (TypeScript) and Tabstack() (Python) take no arguments once the variable is set.
Quickstart
Section titled “Quickstart”The minimal automate call needs a task and, usually, a starting url. The SDK opens the stream and you iterate it; the final answer arrives on the task:completed event. Here is the same call in each SDK, with the raw curl equivalent alongside.
import Tabstack from "@tabstack/sdk";
const client = new Tabstack();
const stream = await client.agent.automate({ task: "Find the top 3 trending repositories and extract their names", url: "https://github.com/trending",});
for await (const event of stream) { if (event.event === "task:completed") { console.log(event.data.finalAnswer); }}from tabstack import Tabstack
client = Tabstack()
stream = client.agent.automate( task="Find the top 3 trending repositories and extract their names", url="https://github.com/trending",)
for event in stream: if event.event == "task:completed": print(event.data.final_answer)curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Find the top 3 trending repositories and extract their names", "url": "https://github.com/trending" }'The client constructor takes no arguments because the SDK reads TABSTACK_API_KEY from the environment. The for await / for ... in loop runs until the stream ends; there is no separate close call. With curl, the -N flag disables buffering so the Server-Sent Events print to your terminal as they arrive, and you parse the event: / data: lines yourself (see the raw HTTP appendix).
How the stream works
Section titled “How the stream works”Every automate run streams a sequence of typed events. Each event has a string event name and a data payload; switch on event.event and the SDK narrows data to the right shape. The lifecycle runs from task:started, through execution events, to task:completed, then complete and done. A minimal progress reporter (setup as in Quickstart):
for await (const event of stream) { switch (event.event) { case "task:started": console.log("Task started"); break; case "agent:action": console.log("Action:", event.data.action); break; case "agent:extracted": console.log("Extracted:", event.data.extractedData); break; case "task:completed": console.log("Done:", event.data.finalAnswer); break; case "error": console.error("Error:", event.data.error); break; }}for event in stream: match event.event: case "task:started": print("Task started") case "agent:action": print("Action:", event.data.action) case "agent:extracted": print("Extracted:", event.data.extracted_data) case "task:completed": print("Done:", event.data.final_answer) case "error": print("Error:", event.data.error)# curl gives you the raw SSE stream. Each message is an `event:` line# followed by a `data:` line of JSON. You match on the event name and# parse the data yourself; see the raw HTTP appendix for a full parser.For the complete event catalogue see Response structure and event types below, and for an end-to-end walkthrough of every event with runnable code, see Understanding Automate Event Flow.
Request parameters
Section titled “Request parameters”You configure the agent through the options you pass to automate(...) (or the JSON body, for curl). The TypeScript names are camelCase; the Python names are snake_case. The full option list is in the TypeScript and Python SDK references.
task (required)
Section titled “task (required)”- Type:
string - Description: The natural language description of your objective. Be as specific as possible.
Examples:
// Good for data extraction{ "task": "Find the top 5 products in the 'Electronics' category and extract their names, prices, and ratings."}
// Good for form filling{ "task": "Fill out the contact form with the provided information and submit it. Then, verify that the 'Thank You' message appears."}
// Good for multi-step workflows{ "task": "Go to the search bar, search for 'running shoes', filter by 'size 10' and 'brand: Nike', and extract the names of the first 3 results."}Tips for writing good tasks:
- Be specific: “Extract top 3” is better than “extract products.”
- Mention quantities: “first 5,” “all results on the page,” etc.
- Describe interactions: “Click the ‘Next’ button,” “filter by size,” “submit the form.”
- State the output: “extract their names and prices,” “return the final confirmation text.”
url (optional)
Section titled “url (optional)”-
Type:
string(URI format) -
Description: The starting URL for the task.
-
When to include: Use this when you know the exact starting page for the task.
-
When to omit: Omit this if your task is a general web search (e.g., “Find the weather in Boston”). The agent determines the starting point.
data (optional)
Section titled “data (optional)”- Type:
unknown(TypeScript, object-shaped in practice) /dict(Python) - Description: A JSON object providing context or data for the task, typically for form-filling. The agent maps the keys in this object (like
firstName) to the fields on the page.
{ "task": "Submit the registration form with my information", "url": "https://example.com/register", "data": { "firstName": "Alex", "lastName": "Johnson", "email": "alex@example.com" }}geo_target (optional)
Section titled “geo_target (optional)”- Type:
{ country: string }(TypeScript) /dict(Python). Both SDKs use the snake_case namegeo_target. This is the exception: the other automate params are camelCase (maxIterations,maxValidationAttempts). A camelCasegeoTargetis silently ignored. - Description: Geotargeting parameters for region-specific browsing, e.g.
{ country: "US" }. See Geotargeting.
guardrails (optional)
Section titled “guardrails (optional)”- Type:
string - Description: Safety constraints describing what the agent should NOT do. This is critical for preventing unintended actions.
Examples:
// Read-only extraction{ "task": "Extract product information", "guardrails": "Browse and extract only. Do not click buttons, submit forms, or add anything to a cart."}
// Domain restrictions{ "task": "Research company information", "guardrails": "Stay on the company's official website (example.com). Do not navigate to any external links, social media, or partner sites."}maxIterations (optional)
Section titled “maxIterations (optional)”- Type:
number - Default:
50 - Range:
1-100 - Python name:
max_iterations - Description: The maximum number of steps (page loads, clicks, extractions) the agent can take before terminating. This prevents infinite loops.
When to adjust:
- Lower (10-20): For simple, single-page tasks.
- Default (50): Sufficient for most tasks with moderate complexity.
- Higher (75-100): For complex, multi-page workflows or deep pagination.
maxValidationAttempts (optional)
Section titled “maxValidationAttempts (optional)”- Type:
number - Default:
3 - Range:
1-10 - Python name:
max_validation_attempts - Description: The maximum number of times the agent will try to validate that its final step was successful (e.g., re-checking for a “Thank You” message after form submission).
interactive (optional)
Section titled “interactive (optional)”- Type:
boolean - Default:
false - Description: Enable interactive mode to allow the agent to pause and request user input when it encounters forms requiring personal data (email, passwords, preferences, etc.). When enabled, the agent emits
interactive:form_data:requestevents and you respond withclient.agent.automateInput(...). See Interactive mode below and the Interactive Mode guide.
Response structure and event types
Section titled “Response structure and event types”You consume a stream of typed events. The tables below list the common events and their key data fields, grouped by category. Field names are camelCase in TypeScript and snake_case in Python (for example finalAnswer / final_answer, extractedData / extracted_data).
Task events (high-level task status)
Section titled “Task events (high-level task status)”| Event Type | Description | Key Data Fields |
|---|---|---|
start | Automation is starting | - |
task:setup | Task is being initialized | task |
task:started | Task execution began | task, plan, successCriteria, actionItems |
task:completed | Task finished successfully | finalAnswer, success |
task:aborted | Task was aborted | reason |
task:validated | Task result validated | completionQuality, observation |
task:validation_error | Validation failed | error |
Agent events (the AI’s reasoning and actions)
Section titled “Agent events (the AI’s reasoning and actions)”| Event Type | Description | Key Data Fields |
|---|---|---|
agent:processing | Agent is processing | operation, hasScreenshot |
agent:status | Status update | message |
agent:step | Iteration begins | currentIteration |
agent:action | Performing an action | action, ref, value |
agent:reasoned | Agent’s reasoning | reasoning |
agent:extracted | Data was extracted | extractedData |
agent:waiting | Waiting for page load/action | iterationId, seconds, timestamp |
Browser events (low-level browser state)
Section titled “Browser events (low-level browser state)”| Event Type | Description | Key Data Fields |
|---|---|---|
browser:navigated | Page navigation occurred | url, title |
browser:action_started | Browser action starting | action |
browser:action_completed | Browser action finished | action, result |
browser:screenshot_captured | Screenshot taken | screenshotId |
Interactive events (when interactive: true)
Section titled “Interactive events (when interactive: true)”| Event Type | Description | Key Data Fields |
|---|---|---|
interactive:form_data:request | Agent needs user input for a form | requestId, fields, formDescription |
interactive:form_data:error | Form validation failed after user input | requestId, fields, fieldErrors |
Stream control events
Section titled “Stream control events”| Event Type | Description |
|---|---|
complete | Final result event. data carries finalAnswer, stats, success, error?. |
done | Signals the stream is closing (always sent last). |
error | An unrecoverable error occurred. data carries the error detail. |
For the payload of each event with runnable handlers, see Understanding Automate Event Flow and the Automate Events reference.
Working with streaming responses
Section titled “Working with streaming responses”The Quickstart logs the final answer. In a real application you process events as they arrive to drive a UI, and capture the final result at the end.
Processing events in real time
Section titled “Processing events in real time”Switch on event.event and react to each phase. The SDK delivers typed data, so there is no buffering or JSON parsing to manage.
const stream = await client.agent.automate({ task: "Extract the top 5 products with their prices", url: "https://shop.example.com/products",});
for await (const event of stream) { switch (event.event) { case "agent:status": console.log("Status:", event.data.message); break; case "browser:navigated": console.log("Navigated to:", event.data.url); break; case "agent:extracted": console.log("Extracted:", event.data.extractedData); break; case "task:completed": console.log("Done:", event.data.finalAnswer); break; }}stream = client.agent.automate( task="Extract the top 5 products with their prices", url="https://shop.example.com/products",)
for event in stream: match event.event: case "agent:status": print("Status:", event.data.message) case "browser:navigated": print("Navigated to:", event.data.url) case "agent:extracted": print("Extracted:", event.data.extracted_data) case "task:completed": print("Done:", event.data.final_answer)curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Extract the top 5 products with their prices", "url": "https://shop.example.com/products" }'# Parse the streamed event:/data: lines yourself; see the raw HTTP appendix.Collecting the final result
Section titled “Collecting the final result”Often you want to process events as they arrive and also capture the final outcome. The complete event carries the canonical final state in event.data (finalAnswer, stats, success, and an optional error).
async function runAutomationTask(task: string, url: string) { const stream = await client.agent.automate({ task, url });
let finalAnswer: string | null = null; let stats: unknown = null;
for await (const event of stream) { if (event.event === "agent:extracted") { console.log("Progress:", event.data.extractedData); } if (event.event === "complete") { finalAnswer = event.data.finalAnswer; stats = event.data.stats; } }
return { finalAnswer, stats };}
const result = await runAutomationTask( "Find the top 3 articles and their publication dates", "https://news.example.com",);console.log(result.finalAnswer);def run_automation_task(task: str, url: str): stream = client.agent.automate(task=task, url=url)
final_answer = None stats = None
for event in stream: if event.event == "agent:extracted": print("Progress:", event.data.extracted_data) if event.event == "complete": final_answer = event.data.final_answer stats = event.data.stats
return {"final_answer": final_answer, "stats": stats}
result = run_automation_task( "Find the top 3 articles and their publication dates", "https://news.example.com",)print(result["final_answer"])curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Find the top 3 articles and their publication dates", "url": "https://news.example.com" }'# The final result arrives on the `complete` event near the end of the stream.Interactive mode
Section titled “Interactive mode”When you pass interactive: true and the agent hits a form needing user data, the stream pauses with an interactive:form_data:request event carrying a requestId and a fields array. Collect the values, then resume the task by submitting them with client.agent.automateInput(...). If validation fails, you receive an interactive:form_data:error event with fieldErrors, and you resubmit corrected values.
const stream = await client.agent.automate({ task: "Sign up for the newsletter", url: "https://example.com", interactive: true,});
for await (const event of stream) { if (event.event === "interactive:form_data:request") { const { requestId, fields } = event.data;
// Collect a value for each requested field (from a user, DB, or secrets store). const fieldValues = fields.map((field) => ({ ref: field.ref, value: lookupValue(field), // your logic }));
await client.agent.automateInput(requestId, { fields: fieldValues }); }}stream = client.agent.automate( task="Sign up for the newsletter", url="https://example.com", interactive=True,)
for event in stream: if event.event == "interactive:form_data:request": request_id = event.data.request_id
# Collect a value for each requested field. field_values = [ {"ref": field.ref, "value": lookup_value(field)} # your logic for field in event.data.fields ]
client.agent.automate_input(request_id, fields=field_values)# When the stream emits interactive:form_data:request, POST the values back# to resume the task, keyed by the requestId from the event:curl -X POST https://api.tabstack.ai/v1/automate/$REQUEST_ID/input \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fields": [{ "ref": "email", "value": "you@example.com" }] }'To cancel an interactive request instead of providing data, submit { cancelled: true } (TypeScript) / cancelled=True (Python). For a complete walkthrough, see the Interactive Mode guide.
Error handling
Section titled “Error handling”A robust implementation handles two kinds of failure:
- HTTP-level errors raised before the stream opens (a malformed body, a missing
task, an invalid token, a rate limit). With the SDK these surface as typed exception classes; wrap theautomate(...)call in try/catch. - In-stream
errorevents that arrive mid-task when the task runner itself fails (for example an unreachable URL). Handle these with acase "error":arm inside the loop.
See also: Error Reference for the canonical list of every status code, error message, and SDK exception across all endpoints, including the automate stream’s agent-level abort codes.
Common error status codes
Section titled “Common error status codes”| Status Code | Error | Description |
|---|---|---|
| 400 | task is required | Missing required task parameter. |
| 400 | invalid URL format | Malformed URL if provided. |
| 400 | maxIterations must be between 1 and 100 | Invalid iteration limit. |
| 401 | Unauthorized - Invalid token | Missing or invalid Bearer token. |
| 500 | failed to call automate server | Internal server error. |
| 503 | automate service not available | Service not configured or unavailable. |
import Tabstack, { BadRequestError, AuthenticationError, RateLimitError,} from "@tabstack/sdk";
const client = new Tabstack();
try { const stream = await client.agent.automate({ task: "Extract product information", url: "https://example.com/products", });
for await (const event of stream) { if (event.event === "error") { console.error("In-stream error:", event.data.error); break; } // ... handle the rest of the events }} catch (err) { if (err instanceof AuthenticationError) { console.error("Auth failed, check your API key"); } else if (err instanceof RateLimitError) { console.error("Rate limited, back off and retry"); } else if (err instanceof BadRequestError) { console.error("Bad request:", err.message); } else { throw err; }}from tabstack import ( Tabstack, BadRequestError, AuthenticationError, RateLimitError,)
client = Tabstack()
try: stream = client.agent.automate( task="Extract product information", url="https://example.com/products", ) for event in stream: if event.event == "error": print("In-stream error:", event.data.error) break # ... handle the rest of the eventsexcept AuthenticationError: print("Auth failed, check your API key")except RateLimitError: print("Rate limited, back off and retry")except BadRequestError as exc: print("Bad request:", exc)curl -sS -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Extract product information", "url": "https://example.com/products" }'# HTTP errors (401, 503, ...) arrive as the response status before the stream.# Task-level failures arrive as an `event: error` message inside the stream.If a task is terminated early, you may also see a task:aborted event before the stream closes. Agent-level aborts also surface inside the complete event with success: false and a structured error; see the Automate Events reference for the distinction.
Advanced usage patterns
Section titled “Advanced usage patterns”Form submission with data
Section titled “Form submission with data”Pass a data object and the agent maps its keys to the fields on the page. Use guardrails to keep the agent from straying.
const contactData = { name: "Alex Johnson", email: "alex@example.com", company: "Acme Inc", message: "Interested in learning more about your products",};
const stream = await client.agent.automate({ task: "Fill out and submit the contact form with the provided information", url: "https://company.com/contact", data: contactData, guardrails: "Only fill and submit the form, do not navigate away or click other links",});
for await (const event of stream) { if (event.event === "task:completed") { console.log("Result:", event.data.finalAnswer); }}contact_data = { "name": "Alex Johnson", "email": "alex@example.com", "company": "Acme Inc", "message": "Interested in learning more about your products",}
stream = client.agent.automate( task="Fill out and submit the contact form with the provided information", url="https://company.com/contact", data=contact_data, guardrails="Only fill and submit the form, do not navigate away or click other links",)
for event in stream: if event.event == "task:completed": print("Result:", event.data.final_answer)curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Fill out and submit the contact form with the provided information", "url": "https://company.com/contact", "data": { "name": "Alex Johnson", "email": "alex@example.com", "company": "Acme Inc", "message": "Interested in learning more about your products" }, "guardrails": "Only fill and submit the form, do not navigate away or click other links" }'Multi-page data collection
Section titled “Multi-page data collection”Raise maxIterations for tasks that page through results, and collect agent:extracted events as they arrive.
const extracted: unknown[] = [];
const stream = await client.agent.automate({ task: 'Search for "wireless headphones", go through the first 3 pages of results, and extract product names and prices from each page', url: "https://shop.example.com", maxIterations: 75, guardrails: "Browse and extract only, do not add items to cart or checkout",});
for await (const event of stream) { if (event.event === "agent:extracted") { extracted.push(event.data.extractedData); console.log("Pages collected:", extracted.length); }}
console.log(`Collected ${extracted.length} data points`);extracted = []
stream = client.agent.automate( task='Search for "wireless headphones", go through the first 3 pages of results, and extract product names and prices from each page', url="https://shop.example.com", max_iterations=75, guardrails="Browse and extract only, do not add items to cart or checkout",)
for event in stream: if event.event == "agent:extracted": extracted.append(event.data.extracted_data) print("Pages collected:", len(extracted))
print(f"Collected {len(extracted)} data points")curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Search for \"wireless headphones\", go through the first 3 pages of results, and extract product names and prices from each page", "url": "https://shop.example.com", "maxIterations": 75, "guardrails": "Browse and extract only, do not add items to cart or checkout" }'Your task description should explicitly mention pagination (“go through the first 3 pages”) to guide the agent.
Price monitoring
Section titled “Price monitoring”A focused single-page task: extract a price and stock status, then compare against a target. Keep maxIterations low for simple tasks.
async function monitorPrice(productUrl: string, targetPrice: number) { const stream = await client.agent.automate({ task: "Navigate to the product page and extract the current price. Also check if the product is in stock.", url: productUrl, maxIterations: 20, guardrails: "Browse and extract only, do not make purchases", });
let answer: string | null = null; for await (const event of stream) { if (event.event === "task:completed") { answer = event.data.finalAnswer; } } console.log("Result:", answer, "(target:", targetPrice, ")"); return answer;}
await monitorPrice("https://shop.example.com/product/12345", 299.99);def monitor_price(product_url: str, target_price: float): stream = client.agent.automate( task="Navigate to the product page and extract the current price. Also check if the product is in stock.", url=product_url, max_iterations=20, guardrails="Browse and extract only, do not make purchases", )
answer = None for event in stream: if event.event == "task:completed": answer = event.data.final_answer
print("Result:", answer, "(target:", target_price, ")") return answer
monitor_price("https://shop.example.com/product/12345", 299.99)curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Navigate to the product page and extract the current price. Also check if the product is in stock.", "url": "https://shop.example.com/product/12345", "maxIterations": 20, "guardrails": "Browse and extract only, do not make purchases" }'For a complete, scheduled price monitor built as an example app, see Price Monitor.
Best practices
Section titled “Best practices”1. Write clear, specific task descriptions
Section titled “1. Write clear, specific task descriptions”The quality of your task description is the single most important factor for success.
// Vague, may not get what you wantGet some products
// Better, more specificExtract the top 5 products with their names and prices
// Best, very detailedGo to the laptops category, filter by price (under $1000), sort by rating(highest first), and extract the top 5 results including product name,price, rating, and number of reviews2. Use guardrails for safety
Section titled “2. Use guardrails for safety”Always specify what the agent should NOT do, especially on e-commerce or sensitive sites.
{ "task": "Research competitor pricing", "guardrails": "Browse and view pages only. Do not submit any forms, create accounts, or make purchases. Stay on the main website, do not follow external links."}3. Set appropriate iteration limits
Section titled “3. Set appropriate iteration limits”Match maxIterations to your task’s complexity to prevent runaways.
- Simple (10-20): Single page extraction, basic form fill.
- Medium (30-50): Multi-page search, simple workflows.
- Complex (60-100): Deep pagination, multi-step form-filling with validation.
4. Provide context with the data parameter
Section titled “4. Provide context with the data parameter”When filling forms, structure your data object to match the conceptual groups on the page.
{ "task": "Complete the registration form", "data": { "personalInfo": { "firstName": "Alex", "lastName": "Johnson" }, "contact": { "email": "alex@example.com", "phone": "555-1234" }, "preferences": { "newsletter": true } }}5. Handle streaming events progressively
Section titled “5. Handle streaming events progressively”Don’t wait for the complete event. Process events as they arrive to build a real-time experience and collect data incrementally. Act on agent:extracted as it fires rather than buffering everything to the end.
6. Watch for errors and aborts
Section titled “6. Watch for errors and aborts”Add a case "error": arm for in-stream task-runner failures, and watch for task:aborted. Both are your signal that a task has failed and you should stop processing.
7. Test with simple tasks first
Section titled “7. Test with simple tasks first”Start with a simple task (like “Navigate to the products page”) to verify your connection. Then build up to more complex workflows, adding filters and interactions one at a time.
Using the raw HTTP API directly
Section titled “Using the raw HTTP API directly”If you are not using an SDK, you can call /v1/automate over plain HTTP. The SDK examples above are the recommended path; everything in this appendix is what the SDK does for you.
Endpoint details
Section titled “Endpoint details”- URL:
https://api.tabstack.ai/v1/automate - Method:
POST - Authentication:
Bearer <your-api-key>(required) - Content-Type:
application/json - Response Type:
text/event-stream(Server-Sent Events)
The SSE stream
Section titled “The SSE stream”You won’t get a single JSON response; you’ll get a series of events. Each message is an event: line and a data: line carrying a JSON payload. A successful run looks like:
event: task:starteddata: {"task": "Find the top 3 trending repositories", "url": "https://github.com/trending"}
event: agent:processingdata: {"status": "Creating task plan"}
event: browser:navigateddata: {"url": "https://github.com/trending"}
event: agent:extracteddata: {"extractedData": "[{\"name\": \"awesome-ai\"}]"}
event: task:completeddata: {"success": true, "finalAnswer": "Top 3 repos: ..."}
event: completedata: {"success": true, "finalAnswer": "Top 3 repos: ...", "stats": {}}
event: donedata: {}The stream begins with task:started, emits progress events as the agent works, and ends with task:completed, then complete (the canonical final result), then done.
Minimal request (curl)
Section titled “Minimal request (curl)”curl -X POST https://api.tabstack.ai/v1/automate \ -H "Authorization: Bearer $TABSTACK_API_KEY" \ -H "Content-Type: application/json" \ -N \ -d '{ "task": "Find the top 3 trending repositories and extract their names", "url": "https://github.com/trending" }'The -N flag disables curl’s buffering so events print as they arrive.
Parsing the stream (JavaScript with fetch)
Section titled “Parsing the stream (JavaScript with fetch)”SSE messages can arrive split across chunks, so buffer the bytes and only process complete lines (ending in \n).
async function processAutomationEvents() { const response = await fetch("https://api.tabstack.ai/v1/automate", { method: "POST", headers: { Authorization: `Bearer ${process.env.TABSTACK_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ task: "Extract the top 5 products with their prices", url: "https://shop.example.com/products", }), });
if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); }
const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = "";
while (true) { const { value, done } = await reader.read(); if (done) break;
buffer += decoder.decode(value, { stream: true });
let eolIndex; while ((eolIndex = buffer.indexOf("\n")) >= 0) { const line = buffer.substring(0, eolIndex).trim(); buffer = buffer.substring(eolIndex + 1);
if (line.startsWith("data: ")) { const data = line.slice(6); if (data === "[DONE]") break; try { const event = JSON.parse(data); console.log("Event:", event); } catch (e) { // Ignore incomplete JSON } } } }}
processAutomationEvents();Parsing the stream (Python with requests)
Section titled “Parsing the stream (Python with requests)”Python’s requests handles buffering with iter_lines(), so the code is simpler. raise_for_status() catches HTTP errors early.
import requestsimport osimport json
def process_automation_events(): response = requests.post( 'https://api.tabstack.ai/v1/automate', headers={ 'Authorization': f'Bearer {os.environ["TABSTACK_API_KEY"]}', 'Content-Type': 'application/json' }, json={ 'task': 'Extract the top 5 products with their prices', 'url': 'https://shop.example.com/products' }, stream=True )
response.raise_for_status()
for line in response.iter_lines(): if not line: continue
line = line.decode('utf-8') if line.startswith('data: '): data = line[6:] if data == '[DONE]': break try: event = json.loads(data) print('Event:', event) except json.JSONDecodeError: pass
process_automation_events()Collecting the final result (raw)
Section titled “Collecting the final result (raw)”Watch for the complete event and keep its payload. Storing all events as you go also helps with debugging.
import requestsimport osimport json
def run_automation_task(task, url): response = requests.post( 'https://api.tabstack.ai/v1/automate', headers={ 'Authorization': f'Bearer {os.environ["TABSTACK_API_KEY"]}', 'Content-Type': 'application/json' }, json={'task': task, 'url': url}, stream=True ) response.raise_for_status()
events = [] final_result = None
for line in response.iter_lines(): if not line: continue line = line.decode('utf-8') if line.startswith('data: '): data = line[6:] if data == '[DONE]': break try: event = json.loads(data) events.append(event) if 'success' in event and 'finalAnswer' in event: final_result = event['finalAnswer'] except json.JSONDecodeError: pass
return { 'success': final_result is not None, 'result': final_result, 'event_count': len(events), }Error handling (raw)
Section titled “Error handling (raw)”Handle errors at two levels: HTTP errors (caught before streaming via response.ok / raise_for_status()) and stream-level event: error messages emitted while the agent works.
import requests
try: response = requests.post( 'https://api.tabstack.ai/v1/automate', headers={'Authorization': f'Bearer {TABSTACK_API_KEY}', 'Content-Type': 'application/json'}, json={'task': 'Extract product information', 'url': 'https://example.com/products'}, stream=True, ) response.raise_for_status() # HTTP-level errors
for line in response.iter_lines(): line = line.decode('utf-8') if line else '' if 'event: error' in line or (line.startswith('data: ') and '"error"' in line): print('Task error in stream:', line)except requests.exceptions.HTTPError as http_err: code = http_err.response.status_code if code == 401: print('Authentication failed. Check your API key.') elif code == 503: print('Automate service is unavailable. Try again later.') raise