> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://platform.bctrl.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://platform.bctrl.ai/_mcp/server.

# React to Run Events

> Stream a run's events over SSE and alert, log, or intervene the moment something goes wrong.

Every run emits a structured [event stream](/sdk/events) - navigations, network failures, console errors. Subscribe over server-sent events and turn the ones you care about into alerts or interventions, while the run is still going.

```ts TypeScript
import { Bctrl } from "@bctrl/sdk";

const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });

// streamUrl mints a ready-to-use SSE endpoint for the run.
const url = bctrl.runs.events.streamUrl(runId);

let networkFailures = 0;

const source = new EventSource(url);
source.onmessage = (message) => {
  const event = JSON.parse(message.data);

  switch (event.type) {
    case "network.failed":
      if (++networkFailures >= 5) {
        notify(`run ${runId}: ${networkFailures} failed requests`); // your alerting
      }
      break;
    case "console.message":
      // surface page-side errors in your own logs
      break;
    case "navigation":
      console.log("now at", event.time);
      break;
  }
};
```

```python Python
from bctrl import Bctrl

bctrl = Bctrl()

network_failures = 0

# A plain blocking iterator - run it in a worker per active run.
for event in bctrl.runs.events.stream(run_id):
    if event["type"] == "network.failed":
        network_failures += 1
        if network_failures >= 5:
            notify(f"run {run_id}: {network_failures} failed requests")
    elif event["type"] == "console.message":
        pass  # surface page-side errors in your own logs
```

Filter at the source instead of in your handler when you only want one kind of event:

```ts
const url = bctrl.runs.events.streamUrl(runId, { type: "console.message" });
```

## Reacting, not just watching

The stream pairs with the rest of the API. A burst of `network.failed` events might mean the proxy exit went bad - stop the runtime and restart on a fresh sticky key. A page that's clearly stuck is a good moment to mint a [takeover link](/cookbook/embed-live-view) and page a human, with the URL right in the alert.

For a after-the-fact version of the same data - paged, filterable, no connection to hold - use `bctrl.runs.events.list(runId)` instead. And if you want the human-readable rollup rather than the firehose, that's [activity](/sdk/events).

## Next

* [Events & Activity](/sdk/events) - types, filters, and the activity view
* [Embed a Live Browser View](/cookbook/embed-live-view) - escalate to human eyes
* [Watch a Run From the Terminal](/cookbook/terminal-observability) - the same stream in your shell