Skip to content
Get started
Build
Automation

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.

Before you can use automate, you’ll need:

  1. A Tabstack API key. Create one in the console and set it as the TABSTACK_API_KEY environment variable. The Quickstart walks through the full setup.
  2. An installed SDK (recommended): @tabstack/sdk for TypeScript or tabstack for Python. See the TypeScript and Python quickstarts. For non-SDK use, any client that can read a text/event-stream response works (see the raw HTTP appendix).
  3. 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.


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

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

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 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.


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.

  • 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.”
  • 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.

  • 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"
}
}
  • Type: { country: string } (TypeScript) / dict (Python). Both SDKs use the snake_case name geo_target. This is the exception: the other automate params are camelCase (maxIterations, maxValidationAttempts). A camelCase geoTarget is silently ignored.
  • Description: Geotargeting parameters for region-specific browsing, e.g. { country: "US" }. See Geotargeting.
  • 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."
}
  • 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.
  • 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).
  • 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:request events and you respond with client.agent.automateInput(...). See Interactive mode below and the Interactive Mode guide.

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

Event TypeDescriptionKey Data Fields
startAutomation is starting-
task:setupTask is being initializedtask
task:startedTask execution begantask, plan, successCriteria, actionItems
task:completedTask finished successfullyfinalAnswer, success
task:abortedTask was abortedreason
task:validatedTask result validatedcompletionQuality, observation
task:validation_errorValidation failederror

Agent events (the AI’s reasoning and actions)

Section titled “Agent events (the AI’s reasoning and actions)”
Event TypeDescriptionKey Data Fields
agent:processingAgent is processingoperation, hasScreenshot
agent:statusStatus updatemessage
agent:stepIteration beginscurrentIteration
agent:actionPerforming an actionaction, ref, value
agent:reasonedAgent’s reasoningreasoning
agent:extractedData was extractedextractedData
agent:waitingWaiting for page load/actioniterationId, seconds, timestamp
Event TypeDescriptionKey Data Fields
browser:navigatedPage navigation occurredurl, title
browser:action_startedBrowser action startingaction
browser:action_completedBrowser action finishedaction, result
browser:screenshot_capturedScreenshot takenscreenshotId

Interactive events (when interactive: true)

Section titled “Interactive events (when interactive: true)”
Event TypeDescriptionKey Data Fields
interactive:form_data:requestAgent needs user input for a formrequestId, fields, formDescription
interactive:form_data:errorForm validation failed after user inputrequestId, fields, fieldErrors
Event TypeDescription
completeFinal result event. data carries finalAnswer, stats, success, error?.
doneSignals the stream is closing (always sent last).
errorAn 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.


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.

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

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

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

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.


A robust implementation handles two kinds of failure:

  1. 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 the automate(...) call in try/catch.
  2. In-stream error events that arrive mid-task when the task runner itself fails (for example an unreachable URL). Handle these with a case "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.

Status CodeErrorDescription
400task is requiredMissing required task parameter.
400invalid URL formatMalformed URL if provided.
400maxIterations must be between 1 and 100Invalid iteration limit.
401Unauthorized - Invalid tokenMissing or invalid Bearer token.
500failed to call automate serverInternal server error.
503automate service not availableService 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;
}
}

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.


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

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

Your task description should explicitly mention pagination (“go through the first 3 pages”) to guide the agent.

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

For a complete, scheduled price monitor built as an example app, see Price Monitor.


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 want
Get some products
// Better, more specific
Extract the top 5 products with their names and prices
// Best, very detailed
Go 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 reviews

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."
}

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

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.

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.

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.


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.

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

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:started
data: {"task": "Find the top 3 trending repositories", "url": "https://github.com/trending"}
event: agent:processing
data: {"status": "Creating task plan"}
event: browser:navigated
data: {"url": "https://github.com/trending"}
event: agent:extracted
data: {"extractedData": "[{\"name\": \"awesome-ai\"}]"}
event: task:completed
data: {"success": true, "finalAnswer": "Top 3 repos: ..."}
event: complete
data: {"success": true, "finalAnswer": "Top 3 repos: ...", "stats": {}}
event: done
data: {}

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.

Terminal window
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();

Python’s requests handles buffering with iter_lines(), so the code is simpler. raise_for_status() catches HTTP errors early.

import requests
import os
import 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()

Watch for the complete event and keep its payload. Storing all events as you go also helps with debugging.

import requests
import os
import 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),
}

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