> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrating Session Events

> What changed in the session event stream, old name to new, and how to update a client.

The session event stream was rebuilt. Types are dotted (`thread.item_posted`), a record has one shape with a `source`,
events are read by id instead of by time, and the WebSocket carries listeners instead of subscriptions. The model is
described in [Session Events](https://docs.blobhub.io/blob-types/workflow/workflows/session-events.md); this page maps the old names to the new.

There is **no compatibility period**. The backend, the worker, the CLI, the SDK and the web app moved together. A client
installed before the cutover stops reading events until it is upgraded: the old commands and frames are gone, not
deprecated. Upgrade every BlobHub client you run, the worker, the CLI and the SDK, to its current release.

History was migrated. Stored session events kept their time and were renamed to the new types, and their `source.kind`
is `unknown`, since the old rows did not say what caused them. Event ids are new: an id or cursor saved from before
the cutover no longer resolves. Expired rows, rows of deleted sessions, rows of an unknown type and malformed rows
were not migrated. `execution_started` and `execution_ended` were realtime frames only and never stored, so migrated
history holds no `execution.*` records.

The execution log was moved to a new table, and each of its rows kept its `message`, `attributes` and `data` as they
were, so a `data_id` still downloads. Two things differ for a reader of `list_execution_events`: every migrated row has
a new `id` (the old one is not served) and a `source` with `kind` `unknown`, and `created_at` is written with six
fractional digits. The event types, `flow.*` and `processor.*`, are unchanged.

## Types

| Old name | New type | Block |
| - | - | - |
| `session_closing` | `session.closing` | `session: {status}` |
| `session_closed` | `session.closed` | `session: {status}` |
| `session_deleting` | `session.deleting` | `session: {status}` |
| `session_deleted` | `session.deleted` | `session: {status}` |
| `session_object_modified` | `object.written` | `object: {alias, type}` instead of `session_object` |
| `session_object_deleted` | `object.deleted` | `object: {alias, type}` instead of `session_object` |
| `session_thread_item_posted` | `thread.item_posted` | `object: {alias, type: "thread"}` and `item: {id, ...}`; the old `session_object.item_id` is `item.id` |
| `session_graph_changed` | `graph.changed` | `object: {alias, type: "graph"}` and `changes` beside it, not inside `session_object` |
| `execution_started` | `execution.started` | `execution`; now a stored record, raised once after the definition is validated |
| `execution_ended` | `execution.ended` | `execution: {id, status, route?, error?}`; now a stored record |
| `session_object_changed` | none | Removed. It was never raised; listen for `object.written` and `object.deleted` |

The stream also raises types that had no old name: `session.created`, `session.updated`, `session.reopened`,
`execution.created` and `execution.stop_requested`.

## Parameters and keys

| Old | New | Notes |
| - | - | - |
| `created_since`, `created_before` on `list_session_events` and `list_execution_events` | `after`, `before` | Event ids, exclusive, not times. To start at a time, form its [boundary id](https://docs.blobhub.io/blob-types/workflow/workflows/session-events.md#boundary-ids) |
| `created_since` used as a delta cursor | `after: <last id>` plus the settle recipe | Re-read from a boundary id a minute older than the last event seen and drop ids already held; see [Settle time](https://docs.blobhub.io/blob-types/workflow/workflows/session-events.md#settle-time) |
| `session_id` on `list_session_events` | `session` | An id or an alias |
| `session_events` in the response | `events` | The response is `{events, cursor, session?}` |
| `execution_events` in the response | `events` | The response is `{events, cursor, execution?}` |
| default `limit` of 10 | default `limit` of 100 | The range stays 1-1000 |

Executions now expose a public `source`, `{kind, user_id?, listener?, client?, depth}`, on `get_execution`,
`list_executions` and `create_execution`. No earlier response carried a field for it. An existing execution whose
internal trigger was `api` or `scheduler` carries the matching `source`, `{kind: "api", depth: 0}` or
`{kind: "scheduler", depth: 0}`; one with any other trigger was left without a `source`.

`created_since` is unchanged on `list_session_thread_items`.

## WebSocket

| Old | New |
| - | - |
| `subscription/add` | `listener/add` |
| `subscription/remove` with `scope`, `scope_id`, `target`, `target_id` and `types` | `listener/remove` with only the `listener_id` that `listener/add` returned |
| `subscription/event` | `listener/event` |
| `scope` and `scope_id` | `revision_id` |
| `target` and `target_id` | `session` |
| `types` | `filter.types`; entries are globs on dotted names, where `*` is one segment and `**` zero or more |

`listener/add` returns a `listener_id`, and each `listener/event` frame carries it with the whole event record in
`event`; the frame no longer repeats the scope and target. A repeated `subscription/add` for the same session merged
its `types` into one subscription; each `listener/add` now creates its own listener, so keep every `listener_id`. See
[Listener](https://docs.blobhub.io/web-socket/server/listener.md#add-a-listener).

## What to change in your code

A poll loop. Before, a time cursor:

```json theme={null}
{"command": "list_session_events", "session_id": "${session_id}", "created_since": "2026-11-09T17:04:05.123456Z",
 "ascending": true}
```

After, an id cursor, with the settle recipe applied by re-reading from an older boundary:

```json theme={null}
{"command": "list_session_events", "session": "${session_id}", "after": "00065fdaf3076d80-7kq2m9zt",
 "ascending": true}
```

The response key is now `events`. Keep the last id you held, and every so often re-read from the boundary id of a
timestamp a minute older, dropping ids you already have.

A subscription. Before:

```json theme={null}
{
  "jsonrpc": "2.0", "id": 1, "method": "subscription/add",
  "params": {"scope": "revision", "scope_id": "${revision_id}", "target": "session", "target_id": "${session_id}",
             "types": ["session_thread_item_posted"]}
}
```

After:

```json theme={null}
{
  "jsonrpc": "2.0", "id": 1, "method": "listener/add",
  "params": {"revision_id": "${revision_id}", "session": "${session_id}",
             "filter": {"types": ["thread.item_posted"]}}
}
```

A `session_deleted` handler. Before, the type was read from the frame's `params`:

```json theme={null}
{"jsonrpc": "2.0", "method": "subscription/event",
 "params": {"scope": "revision", "scope_id": "${revision_id}", "target": "session", "target_id": "${session_id}",
            "type": "session_deleted"}}
```

After, it is read from the event inside the frame:

```json theme={null}
{"jsonrpc": "2.0", "method": "listener/event",
 "params": {"listener_id": "${listener_id}",
            "event": {"id": "00065fdaf3076d80-7kq2m9zt", "session_id": "${session_id}", "revision_id": "${revision_id}",
                      "created_at": "2026-11-09T17:04:05.123456Z", "type": "session.deleted",
                      "source": {"kind": "platform", "depth": 0}, "session": {"status": "deleted"},
                      "expires_at": 1794330245}}}
```

Match on `params.event.type` equal to `session.deleted`. Add a `filter` of `{"types": ["session.deleted"]}` to receive
only that type, and remember that the listeners of a deleted session are removed once the event is pushed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.