---
title: "Events & state · Scrape.ly Docs"
description: "Platform events, state persistence, reboot, and status messages."
url: https://scrape.ly/docs/sdk-python-events
markdown: https://scrape.ly/docs/sdk-python-events.md
---

# Events & state

> Platform events, state persistence, reboot, and status messages.

The platform sends events to a running Actor over a WebSocket. These events report resource usage, tell the Actor to save its state, and warn it before a migration or an abort. This page covers subscribing to events, persisting state with `Actor.use_state()`, rebooting after a migration and setting the run's status message.

## Subscribing to events

Use `Actor.on()` to subscribe and `Actor.off()` to unsubscribe. Both are synchronous and need an initialized SDK. Events are members of Crawlee's `Event` enum, which `scrapely` re-exports.

```python
import asyncio

from scrapely import Actor, Event


async def on_aborting() -> None:
    await asyncio.sleep(1)
    await Actor.exit()


async def main() -> None:
    async with Actor:
        Actor.on(Event.ABORTING, on_aborting)
        ...
```

A listener can take no arguments or one argument, the event data, and it can be sync or async. `Actor.off(event)` without a listener removes every listener for that event.

### Event names

| Member | Value | When `Actor.on` listeners receive it |
| --- | --- | --- |
| `Event.PERSIST_STATE` | `persistState` | Every `persist_state_interval_millis` (60 s by default), when the platform sends it, on `migrating`, on `Actor.reboot()` and at shutdown |
| `Event.SYSTEM_INFO` | `systemInfo` | When the platform sends memory and CPU samples. Off the platform, every second |
| `Event.MIGRATING` | `migrating` | The run is about to move to another server |
| `Event.ABORTING` | `aborting` | A user or the API requested a graceful abort. The platform sends `persistState` first |
| `Event.EXIT` | `exit` | Defined for compatibility. Scrape.ly doesn't send it |

`PERSIST_STATE` data has an `is_migrating` flag. `SYSTEM_INFO` data is Crawlee's `EventSystemInfoData`. The platform's deprecated `cpuInfo` message and any unknown messages are ignored.

Crawlee defines more events, such as `SESSION_RETIRED`, `BROWSER_LAUNCHED`, `BROWSER_RETIRED`, `BROWSER_CLOSED`, `PAGE_CREATED`, `PAGE_CLOSED` and `CRAWLER_STATUS`. Crawlers emit these on Crawlee's own event manager.

> [!NOTE]
> Crawlers and `use_state()` listen to Crawlee's global event manager, not the Actor's. Platform `persistState`, `migrating` and `aborting` events reach only `Actor.on()` listeners. Forward them yourself when you need to. See [State persistence](#state-persistence).

### No automatic shutdown handlers

The SDK doesn't exit on `aborting` or reboot on `migrating` by itself. `InitOptions.graceful_shutdown` is accepted but not used. If you want those behaviours, register the listeners, as the official templates do for `aborting`.

If the container gets `SIGTERM` or `SIGINT`, the SDK runs a bounded shutdown and exits with code 0. See [Signals](https://scrape.ly/docs/sdk-python-lifecycle.md#signals).

### Connection

When `ACTOR_EVENTS_WEBSOCKET_URL` is set, `ScrapelyEventManager` connects to it with the run token in the background and reconnects with backoff. A failed connection never blocks `Actor.init()`. Without the URL, the SDK uses Crawlee's `LocalEventManager`.

## State persistence

A run can move to another server, and anything held only in memory is lost. Keep state that must survive in a key-value store.

### `Actor.use_state()`

```python
state = await Actor.use_state(default_value={"processed": 0, "cursor": None})
state["processed"] += 1
```

`use_state(default_value=None, key=None, kvs_name=None)` returns a mutable dict backed by a key-value store record. The default key is `APIFY_GLOBAL_STATE`, and the default store is the run's default key-value store. Change the dict in place. Rebinding the name (`state = {...}`) doesn't change what gets saved.

The dict is saved whenever Crawlee's global event manager emits `persistState`. That happens about every minute while a crawler runs, and once when the crawler finishes. If no crawler is running, or if you need a save at a specific moment, save the dict explicitly:

```python
store = await Actor.open_key_value_store()          # pass name=... if you used kvs_name
await store.persist_autosaved_values()
```

To save state before a migration, forward the platform event:

```python
async def on_migrating(event_data) -> None:
    store = await Actor.open_key_value_store()
    await store.persist_autosaved_values()
    await Actor.reboot()


Actor.on(Event.MIGRATING, on_migrating)
```

After the restart, `use_state()` loads the saved value, so the Actor resumes where it stopped.

```python
state = await Actor.use_state(default_value={"cursor": 0}, key="crawl-state", kvs_name="state-store")
```

### `Actor.reboot()`

```python
await Actor.reboot(options=None)
```

`reboot()` restarts the run in a fresh container with the same run ID and the same default storages. It runs these steps in order:

1. Emits `PERSIST_STATE` with `is_migrating=True` to `Actor.on()` listeners.
2. Waits one second.
3. Calls `POST /v2/actor-runs/{runId}/reboot`.
4. Waits for the platform to stop the container. The wait is `RebootOptions.custom_after_sleep_millis`, or `metamorph_after_sleep_millis` (300000 ms by default).
5. If the container is still alive after the wait, flushes pending charges and calls `sys.exit(0)`.

Off the platform, `reboot()` logs a warning and returns. Without a run ID, it raises `RuntimeError`.

## Status messages

```python
from scrapely import Actor, SetStatusMessageOptions

await Actor.set_status_message("Downloading page 3")
await Actor.set_status_message("Completed", is_terminal=True)
await Actor.set_status_message(
    "Retrying upstream",
    SetStatusMessageOptions(level="WARNING"),
)
```

`set_status_message(message, options=None, *, is_terminal=None)` first logs `[Status message]: <message>` at `options.level` (`DEBUG`, `INFO`, `WARNING` or `ERROR`). It then sends `PUT /v2/actor-runs/{runId}` with `statusMessage` and `isStatusMessageTerminal`, and returns the updated `ActorRun`. Without API credentials, it only logs, and it returns an empty `ActorRun`.

`is_terminal` overrides `SetStatusMessageOptions.is_status_message_terminal`. A terminal message is meant to be the run's final message.

`Actor.status_message` is a separate value. It holds the message passed to `exit()` or `fail()`. On the platform, `exit()` and `fail()` also save that message on the run as a terminal message.

## What's next

- [Storages](https://scrape.ly/docs/sdk-python-storages.md) covers where persisted state lives.
- [Errors & deployment](https://scrape.ly/docs/sdk-python-deploy.md) covers testing and troubleshooting.
- The [API reference](https://scrape.ly/docs/sdk-python-reference.md) lists every signature.

---

- Previous: [Storages](https://scrape.ly/docs/sdk-python-storages.md)
- Next: [Actors & webhooks](https://scrape.ly/docs/sdk-python-actors.md)
- All docs: [Docs index](https://scrape.ly/docs.md), [llms.txt](https://scrape.ly/llms.txt)
