---
title: "Events and live UI"
description: "Build a live application UI on resource snapshots, durable events, and streaming drafts."
canonical_url: "https://hoplite.sh/docs/factory/events"
markdown_url: "https://hoplite.sh/docs/factory/events.md"
---

# Events and live UI
URL: /docs/factory/events
LLM index: /llms.txt
Description: Build a live application UI on resource snapshots, durable events, and streaming drafts.
Related: /docs/factory, /docs/factory/webhooks

# Events and live UI

Build a live view of your factory's work with Hoplite's resource snapshots, durable events, and streaming drafts. Durable events identify resources and status changes; fetch the affected resource to update your UI. An event is not a complete replacement for the thread, message, or run response.

## Load a consistent view

1. Choose your project and thread filters, then capture a cursor from `GET /api/events/head`.
2. Read the resource snapshots needed to render the view.
3. Replay `GET /api/events` from that cursor with the same filters.
4. Continue polling, or connect to `GET /api/events/stream` for SSE.

Capture the head before reading snapshots. Events committed while you read those snapshots can then be replayed without leaving a gap.

```bash title="Capture the event head"
curl --fail-with-body --get "$HOPLITE_API_URL/api/events/head" \
  --header "X-Api-Key: $HOPLITE_API_KEY" \
  --data-urlencode "projectId=$HOPLITE_PROJECT_ID"
```

Keep the service credential on your backend. Relay authorized updates to your frontend through your own application session.

## Event types

Every event carries `id`, `type`, `orgId`, `projectId`, `threadId`, `runId`, `sequence`, `occurredAt`, and a `data` object. `data` contains identities and public status only. Message text, tool input and output, prompts, provider payloads, storage locations, and build commands are never published; read the resource through the API for details.

| Type | Published when | `data` fields |
| --- | --- | --- |
| `thread.updated`, `thread.deleted` | A thread is created, deleted, or changes status, title, archive state, project, or configuration version | `id`, `status` |
| `run.<status>` | A run is created or changes status, for example `run.running`, `run.completed`, `run.failed` | `id`, `status` |
| `message.persisted` | A message is saved | `id`, `role` |
| `tool.updated` | A tool call is recorded or changes status | `id`, `status` |
| `approval.requested`, `approval.resolved` | A tool approval is requested or resolved | `id`, `sourceEventId` |
| `checkpoint.created` | A checkpoint is saved | `id` |
| `workspace.recovery.updated` | A workspace lifecycle change is recorded | `id` |
| `preview.updated` | A preview is created or changes status | `id`, `status` |
| `pull_request.updated` | A pull request is created or its state, draft flag, head, merge, or checks summary changes | `id`, `status` |
| `automation.execution.updated` | An automation execution is created or changes status | `id`, `status` |
| `operation.updated` | An idempotent write receipt is created or changes state | `id`, `status` |
| `platform.source.imported` | A Platform source version is stored | `id`, `sha256`, `byteSize`, `fileCount` |
| `platform.apply.<status>` | A Platform draft apply is queued, applied, conflicted, or failed | `id`, `status`, `sourceId` (the result) |
| `platform.build.<status>` | A Platform build is queued, building, ready, or failed | `id`, `status`, `sourceId`, `artifactSha256` when ready |
| `platform.head.updated` | A Platform project head moves | `id` (the project), `sourceId`, `revision` |

Filter with `types` using the exact names, for example `types=run.completed,run.failed,platform.build.ready`.

## Reconnect and replay

Use the JSON event's `id` to deduplicate deliveries. Use its `cursor` to resume: the SSE `id:` field contains that cursor, and your client sends it as `Last-Event-ID` when reconnecting with identical filters. Polling clients advance using the response's `nextCursor`.

For example, a durable frame has this shape; `opaque-cursor` stands for the server-issued value:

```text title="Durable SSE event"
id: opaque-cursor
event: run.completed
data: {"id":"event_example","orgId":"org_example","projectId":"project_example","threadId":"thread_example","runId":"run_example","sequence":"42","type":"run.completed","data":{"id":"run_example","status":"completed"},"occurredAt":"2026-09-22T10:00:00.000Z","publishedAt":"2026-09-22T10:00:01.000Z","cursor":"opaque-cursor"}

```

Store the cursor unchanged after processing the frame. Do not send `event_example` or the sequence number as `Last-Event-ID`. Heartbeats also carry a replay cursor; draft frames do not.

Cursors expire after seven days. On `410 cursor_expired`, capture a new head, reload the resources, and replay from that new cursor in that order.

Streams recheck authorization and close when access expires. Reconnect with a valid credential; do not treat a closed stream as proof that the run has stopped.

## Streaming drafts

Thread-filtered streams also emit draft snapshots with stable message IDs and increasing revisions. Replace the draft content for that message when a newer revision arrives. Appending each snapshot would duplicate text.

Draft frames do not carry a durable replay cursor. Persisted messages supersede their drafts, and `draft.removed` removes a draft. After reconnecting, reconcile against persisted messages rather than assuming every draft frame was delivered.

## Sitemap

See the full [sitemap](/docs/sitemap.md) for all pages.
Well-known sitemap: [/docs/.well-known/sitemap.md](/docs/.well-known/sitemap.md).
