---
title: Understanding Automate Event Flow | Tabstack
description: Consume the Server-Sent Event stream from the Tabstack automate endpoint, covering the event lifecycle, typed payloads, and how to drive a UI from each phase.
---

## How Event Streaming Works

Under the hood, `/v1/automate` delivers events over Server-Sent Events (SSE), but the Tabstack SDKs wrap that framing for you. Calling `client.agent.automate(...)` returns a stream of typed `AutomateEvent` values; iterate it with `for await ... of` in TypeScript or `for ... in` in Python. Each event has a string `event` name and a `data` payload, and switching on `event.event` narrows `event.data` to the right variant.

Consuming the stream this way lets you:

- Display progress updates to users as the agent works.
- Track the current state of the task (planning, executing, extracting, done).
- Capture extracted data as it becomes available.
- Handle errors gracefully, both before and during execution.

For a scannable summary of event ordering, filtering, and conditional events, see the [Automate Events reference](/reference/automate-events/index.md). This guide walks through the same lifecycle end-to-end with runnable code.

## The Event Lifecycle

A typical automation task flows through these phases:

### 1. Initialization

When you submit a request, the SDK opens the stream and the first events establish the task context. You’ll see `task:trace_context` (a W3C trace ID for the run, skipped if tracing is disabled), then `cdp:endpoint_connected` (the browser session has attached), then `agent:processing` / `agent:status` while the agent builds a task plan, then `task:started` carrying the full plan payload:

- [TypeScript](#tab-panel-22)
- [Python](#tab-panel-23)

```
import Tabstack from "@tabstack/sdk";


const client = new Tabstack();


const stream = await client.agent.automate({
  task: "Find the product price",
  url: "https://example.com/product",
});


for await (const event of stream) {
  switch (event.event) {
    case "cdp:endpoint_connected":
      console.log("Browser session attached");
      break;
    case "task:started":
      console.log("Task started:", event.data.task);
      console.log("Plan:", event.data.plan);
      console.log("Success criteria:", event.data.successCriteria);
      console.log("Action items:", event.data.actionItems);
      break;
    // ... phases 2+ continue this switch
  }
}
```

```
from tabstack import Tabstack


client = Tabstack()


stream = client.agent.automate(
    task="Find the product price",
    url="https://example.com/product",
)


for event in stream:
    match event.event:
        case "cdp:endpoint_connected":
            print("Browser session attached")
        case "task:started":
            print("Task started:", event.data.task)
            print("Plan:", event.data.plan)
            print("Success criteria:", event.data.success_criteria)
            print("Action items:", event.data.action_items)
        # ... phases 2+ continue this match
```

The `task:started` payload includes the task string, the URL, the page title, a human-readable `plan`, the `successCriteria` string the agent will evaluate against, and an `actionItems` list. Display any or all of these so users can see what the automation will do.

### 2. Planning and Processing

The agent emits `agent:processing` events whenever it’s thinking, and `agent:status` events when it wants to surface a human-readable status message. The SDK exposes the fields directly (no manual JSON parsing) so you can react to them as typed data:

- [TypeScript](#tab-panel-24)
- [Python](#tab-panel-25)

```
// continues the switch (event.event) from step 1
switch (event.event) {
  case "agent:processing":
    if (event.data.hasScreenshot) {
      console.log("Analyzing page:", event.data.operation);
    } else {
      console.log("Thinking:", event.data.operation);
    }
    break;
  case "agent:status":
    console.log("Status:", event.data.message);
    break;
}
```

```
# continues the match event.event from step 1
match event.event:
    case "agent:processing":
        if event.data.has_screenshot:
            print("Analyzing page:", event.data.operation)
        else:
            print("Thinking:", event.data.operation)
    case "agent:status":
        print("Status:", event.data.message)
```

`agent:processing` is high-frequency. Use it to drive a “Thinking…” indicator, but filter it out of logs you want to stay quiet.

### 3. Execution Steps

As the agent executes the plan, it emits events for each iteration. A typical loop is `agent:step` (iteration begins) → `agent:reasoned` (the agent’s reasoning output) → `agent:action` (the concrete browser action) with `browser:navigated`, `browser:action_started`, and `browser:action_completed` interleaved:

- [TypeScript](#tab-panel-26)
- [Python](#tab-panel-27)

```
// continues the switch (event.event) from step 1
switch (event.event) {
  case "agent:step":
    console.log("Step", event.data.currentIteration + 1);
    break;
  case "agent:reasoned":
    console.log("Reasoning:", event.data.reasoning);
    break;
  case "agent:action":
    console.log("Action:", event.data.action, "--", event.data.value);
    break;
  case "browser:navigated":
    console.log("Navigated:", event.data.url, "(title:", event.data.title, ")");
    break;
  case "browser:action_started":
    console.log("Browser action started");
    break;
  case "browser:action_completed":
    console.log("Browser action completed");
    break;
}
```

```
# continues the match event.event from step 1
match event.event:
    case "agent:step":
        print("Step", event.data.current_iteration + 1)
    case "agent:reasoned":
        print("Reasoning:", event.data.reasoning)
    case "agent:action":
        print("Action:", event.data.action, "--", event.data.value)
    case "browser:navigated":
        print("Navigated:", event.data.url, "(title:", event.data.title, ")")
    case "browser:action_started":
        print("Browser action started")
    case "browser:action_completed":
        print("Browser action completed")
```

`agent:action.action` is an operation name like `extract`, `click`, `type`, or `done`; `agent:action.value` is a short human-readable description of that operation. Use `event.data.currentIteration` to drive a “Step N of M” UI.

### 4. Interactive Form Input (when `interactive: true`)

If interactive mode is enabled and the agent encounters a form requiring user data, the stream pauses mid-task with an `interactive:form_data:request` event. Your client should:

1. Read the `requestId` and `fields` array to determine what data the agent needs.
2. Collect the values (from a user prompt, database, or another system).
3. Submit them via `client.agent.automateInput(requestId, { fields })` (TS) / `client.agent.automate_input(request_id, fields=...)` (Python), where `fields` is a list of `{ ref, value }` entries. Pass `{ cancelled: true }` / `cancelled=True` to abort instead.

- [TypeScript](#tab-panel-28)
- [Python](#tab-panel-29)

```
// (setup as in step 1, with `interactive: true` in the automate() call)
case 'interactive:form_data:request':
  console.log('Form fields needed:', event.data.fields)
  console.log('Page:', event.data.pageTitle, '-', event.data.pageUrl)
  // Collect values, then:
  //   await client.agent.automateInput(event.data.requestId, { fields: [{ ref, value }] })
  break
```

```
# (setup as in step 1, with interactive=True in the automate() call)
case "interactive:form_data:request":
    print("Form fields needed:", event.data.fields)
    print("Page:", event.data.page_title, "-", event.data.page_url)
    # Collect values, then:
    #   client.agent.automate_input(event.data.request_id, fields=[{"ref": ..., "value": ...}])
```

Once you submit, the agent fills the form and resumes. If validation fails, you’ll receive an `interactive:form_data:error` event describing which fields need correction. For a complete walkthrough, see the [Interactive Mode guide](/guides/interactive-mode/index.md).

### 5. Data Extraction

When the agent extracts data, it emits `agent:extracted` with the payload in `event.data.extractedData` (TS) / `event.data.extracted_data` (Python). The field is a string. When the task asks for structured data, the agent typically returns JSON, but it may wrap that JSON in a Markdown code fence. Strip the fence before parsing. This fence wrapping isn’t documented in the SDK or spec; it’s model-output non-determinism. The code strips fences defensively so your handler doesn’t break when they appear:

- [TypeScript](#tab-panel-30)
- [Python](#tab-panel-31)

````
// (setup as in step 1)
case 'agent:extracted': {
  const raw = event.data.extractedData
  // The agent sometimes wraps JSON in ```json ... ``` fences; strip them.
  const unfenced = raw
    .replace(/^\s*```(?:json|markdown)?\s*\n?/g, '')
    .replace(/\n?```\s*$/g, '')
    .trim()
  let data: unknown = unfenced
  try {
    data = JSON.parse(unfenced)
  } catch {
    // Not valid JSON, the agent returned a plain string answer.
  }
  console.log('Extracted:', data)
  break
}
````

````
import json
import re


# (setup as in step 1)


case "agent:extracted":
    raw = event.data.extracted_data
    # The agent sometimes wraps JSON in ```json ... ``` fences; strip them.
    unfenced = re.sub(r"^\s*```(?:json|markdown)?\s*\n?", "", raw)
    unfenced = re.sub(r"\n?```\s*$", "", unfenced).strip()
    try:
        data = json.loads(unfenced)
    except json.JSONDecodeError:
        # Not valid JSON, the agent returned a plain string answer.
        data = unfenced
    print("Extracted:", data)
````

You may receive multiple `agent:extracted` events if the task involves extracting from different pages or elements.

### 6. Completion

A successful run ends with four landmark events: `task:validated` (the completion check passed), `task:completed` (the agent decides the task is done), `complete` (final result with summary stats), and `done` (stream terminator with an empty payload):

- [TypeScript](#tab-panel-32)
- [Python](#tab-panel-33)

```
// (setup as in step 1)
case 'task:validated':
  console.log('Validation:', event.data.completionQuality)
  console.log('Observation:', event.data.observation)
  break
case 'task:completed':
  console.log('Completed:', event.data.finalAnswer)
  break
case 'complete':
  if (event.data.success) {
    console.log(`Finished in ${event.data.stats.durationMs}ms:`, event.data.finalAnswer)
  } else {
    console.error(`Failed (${event.data.error?.code}):`, event.data.error?.message)
  }
  break
```

```
# (setup as in step 1)
case "task:validated":
    print("Validation:", event.data.completion_quality)
    print("Observation:", event.data.observation)
case "task:completed":
    print("Completed:", event.data.final_answer)
case "complete":
    if event.data.success:
        print(f"Finished in {event.data.stats.duration_ms}ms:", event.data.final_answer)
    else:
        code = event.data.error.code if event.data.error else "UNKNOWN"
        msg = event.data.error.message if event.data.error else ""
        print(f"Failed ({code}):", msg)
```

`task:completed.data` carries `{finalAnswer, success}`. `complete.data` carries `{finalAnswer, stats, success}` plus an optional structured `error` for `success: false` cases, where `stats` is `{actions, durationMs, endTime, iterations, startTime}`, useful for showing “Completed in N seconds across M iterations.” When the `for await` / `for ... in` loop exits, the stream is finished; there is no separate close call to make.

## Handling Errors

Errors surface in two places:

1. **HTTP-level exceptions** raised before the stream opens (e.g. a malformed body, missing task, auth failure, rate limit). These are the SDK’s typed error classes: `BadRequestError`, `AuthenticationError`, `RateLimitError`, etc. Wrap the `automate(...)` call and the iteration loop in try/catch.
2. **In-stream `error` events** that arrive mid-task when the task runner itself fails (e.g. an unreachable URL). The payload shape is `{ success: false, error: { code, message, timestamp } }`, where `timestamp` is an ISO-8601 string. The SDK exposes this as a typed variant of the `AutomateEvent` union, so a regular `case 'error':` (TypeScript) / `case "error":` (Python) narrows `event.data` for you.

- [TypeScript](#tab-panel-34)
- [Python](#tab-panel-35)

```
import Tabstack, {
  BadRequestError,
  AuthenticationError,
  RateLimitError,
} from "@tabstack/sdk";


const client = new Tabstack();


try {
  const stream = await client.agent.automate({
    task: "Find the product price",
    url: "https://example.com/product",
  });


  for await (const event of stream) {
    if (event.event === "error") {
      console.error("In-stream error:", event.data.error.message);
      break;
    }
    // ... handle the rest of the events
  }
} catch (err) {
  if (err instanceof BadRequestError) {
    console.error("Bad request:", err.message);
  } else 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 {
    throw err;
  }
}
```

```
from tabstack import Tabstack, BadRequestError, AuthenticationError, RateLimitError


client = Tabstack()


try:
    stream = client.agent.automate(
        task="Find the product price",
        url="https://example.com/product",
    )
    for event in stream:
        if event.event == "error":
            print("In-stream error:", event.data.error.message)
            break
        # ... handle the rest of the events
except BadRequestError as exc:
    print("Bad request:", exc)
except AuthenticationError:
    print("Auth failed, check your API key")
except RateLimitError:
    print("Rate limited, back off and retry")
```

If a task is terminated early, you’ll also see a `task:aborted` event before the stream closes. Agent-level aborts (the `success: false` path) also surface inside the `complete` event with a structured `error` payload. See the [Automate events reference](/reference/automate-events/index.md) for the distinction between the two.

## Building a Client

Here’s a pattern for consuming the full stream and keeping an incrementally-updated state object you can bind to a UI:

- [TypeScript](#tab-panel-36)
- [Python](#tab-panel-37)

````
import Tabstack from "@tabstack/sdk";


const client = new Tabstack();


type ClientState = {
  status: "starting" | "running" | "completed" | "error";
  plan: string | null;
  currentStep: number;
  extractedData: unknown[];
  result: string | null;
  error: string | null;
};


const state: ClientState = {
  status: "starting",
  plan: null,
  currentStep: 0,
  extractedData: [],
  result: null,
  error: null,
};


const stream = await client.agent.automate({
  task: "Find the product price",
  url: "https://example.com/product",
});


for await (const event of stream) {
  switch (event.event) {
    case "task:started":
      state.status = "running";
      state.plan = event.data.plan;
      break;
    case "agent:step":
      state.currentStep = event.data.currentIteration + 1;
      break;
    case "agent:extracted": {
      const raw = event.data.extractedData;
      const unfenced = raw
        .replace(/^\s*```(?:json|markdown)?\s*\n?/g, "")
        .replace(/\n?```\s*$/g, "")
        .trim();
      try {
        state.extractedData.push(JSON.parse(unfenced));
      } catch {
        state.extractedData.push(unfenced);
      }
      break;
    }
    case "task:completed":
      state.status = "completed";
      state.result = event.data.finalAnswer;
      break;
    case "error":
      state.status = "error";
      state.error = event.data.error.message;
      break;
  }
}


console.log(state);
````

````
import json
import re
from tabstack import Tabstack


client = Tabstack()


state = {
    "status": "starting",
    "plan": None,
    "current_step": 0,
    "extracted_data": [],
    "result": None,
    "error": None,
}


stream = client.agent.automate(
    task="Find the product price",
    url="https://example.com/product",
)


for event in stream:
    match event.event:
        case "task:started":
            state["status"] = "running"
            state["plan"] = event.data.plan
        case "agent:step":
            state["current_step"] = event.data.current_iteration + 1
        case "agent:extracted":
            raw = event.data.extracted_data
            unfenced = re.sub(r"^\s*```(?:json|markdown)?\s*\n?", "", raw)
            unfenced = re.sub(r"\n?```\s*$", "", unfenced).strip()
            try:
                state["extracted_data"].append(json.loads(unfenced))
            except json.JSONDecodeError:
                state["extracted_data"].append(unfenced)
        case "task:completed":
            state["status"] = "completed"
            state["result"] = event.data.final_answer
        case "error":
            state["status"] = "error"
            state["error"] = event.data.error.message


print(state)
````

## Key Takeaways

- **Events stream in real-time.** Update your UI as each event arrives rather than waiting for the whole run to finish.
- **Switch on `event.event`.** The SDK narrows `event.data` to the right typed variant for each case.
- **Track progress with `agent:step`.** Use `currentIteration` / `current_iteration` to show “Step N of M”.
- **`extractedData` may be fenced.** When the agent returns JSON, it sometimes wraps it in ` ```json ` fences. Strip before parsing, and fall back gracefully if the string isn’t JSON.
- **Always handle errors.** Wrap the call in try/catch for HTTP-level failures, and add a `case 'error':` arm for in-stream runtime failures. `event.data.error.message` is typed.
- **`complete` carries the canonical final state.** Inspect `event.data.success` and `event.data.error?.code` for agent-level aborts (`TASK_ABORTED`, `MAX_ITERATIONS`, etc.). The in-stream `error` event is reserved for task-runner crashes, not agent decisions.
- **The loop ends naturally.** When the `for await` / `for ... in` iteration returns, the stream is finished; there’s no separate close to call.

## See also

- [Automate Events reference](/reference/automate-events/index.md): scannable summary of event ordering, filtering, and conditional events.
- [API reference, automate](/api/resources/agent/methods/automate/index.md): complete event schema with all payload fields.
- [Interactive Mode guide](/guides/interactive-mode/index.md): how to handle `interactive:form_data:*` events end-to-end.
