> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/workflows/tutorials/integrate-with-ignition/historical-data.md).

# Historical Data

Show inspection events and their images from a Roboflow device's Event Store in Ignition, and copy them into a database for history.

A Roboflow edge device keeps every event your Workflow writes, with its images and data, in the [Event Store](https://docs.roboflow.com/deployment/self-hosted/enterprise/deployment-manager/services/event-store). Ignition reads it over HTTP, so operators can see each inspection and the image the model judged.

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-d1b94701b8ac10dd6493371ce0c9802ba3ec293e%2Fignition-events-table.png?alt=media" alt="A Perspective view with a table of recent inspections from the Roboflow Event Store and the image of the selected event"><figcaption><p>Recent inspections from the Event Store, with the selected event's image.</p></figcaption></figure>

This page covers two ways to use the events:

* Show events in a Perspective table, straight from the device: the latest ones, or a time window such as the current shift. Ignition keeps nothing, so no database is needed.
* Copy every event into your database with a gateway script, for reports and for history the device no longer keeps.

To get the latest result as Ignition tags instead (ex: the last result or a reject count), see [Live Data](/workflows/tutorials/integrate-with-ignition/live-data.md).

## Prerequisites

* A Workflow running as a stream on the device, with a [Roboflow Vision Events](/workflows/blocks/blocks/data-storage/roboflow-vision-events.md) block that has "Write to Local Event Store" turned on. In this mode the block writes each event to the device's Event Store at `http://localhost:8001` and needs no Roboflow API key.
* The Ignition gateway must reach the device on port `8001`.
* For the database option, a database connection in Ignition.

{% hint style="info" %}
These steps and screenshots are from Ignition 8.1 (Designer 8.1.55). Menu names, dialogs, and property names can differ in other versions, including 8.3.
{% endhint %}

## Look at Your Events

Open `http://<device-ip>:8001/v2/events?limit=5` in a browser. The response lists events, newest first:

```json
{
  "events": [
    {
      "id": "3714e263-8159-4748-83db-ad804dde25cc",
      "stream_id": "bottle-line-1",
      "event_schema": "quality_check",
      "event_data": {"result": "fail", "externalId": "CASE-20437"},
      "custom_metadata": {"bottleCount": 11, "line": "Line 1"},
      "images": [{"file_id": "a1f0c2...", "label": "workflow"}],
      "created_at": "2026-11-13T15:42:10.512Z"
    }
  ],
  "count": 1,
  "next_timestamp": "2026-11-13T15:42:10.512Z"
}
```

`event_data` holds the fields of the event type you picked in the block (ex: "Result" and "External ID" for Quality Check), and `custom_metadata` holds the block's "Custom Metadata". Each image is served at `http://<device-ip>:8001/images/<file_id>`. Learn more about [the Event Store REST API](https://docs.roboflow.com/deployment/self-hosted/enterprise/deployment-manager/services/event-store/rest-api).

## Show Recent Events in Perspective

1. Add a "Table" component to a Perspective view.
2. Click the binding icon next to the table's `props.data` and choose "HTTP":
   * URL: `"http://<device-ip>:8001/v2/events?limit=20"`, in quotes, since the URL is an expression.
   * Method: `GET`.
   * Turn on "Polling" and set it to `5` seconds.
   * Turn on "Cache & Share", so one request serves every session that shows the view.

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-a642b31e56d24007c5f9e72da1b151e9a1c64fcc%2Fignition-designer-http-binding.png?alt=media" alt="The Edit Binding dialog for Events.props.data with an HTTP binding: a URL expression to port 8001, Method GET, Cache and Share and Polling every 5 seconds turned on, and a binding preview of the response"><figcaption><p>The HTTP binding. The preview shows the Event Store's response and the rows the transform builds.</p></figcaption></figure>

3. Click "Add Transform", choose "Script", and paste a script that turns each event into a row:

```python
def transform(self, value, quality, timestamp):
    if value is None:
        return []
    return [
        {
            "time": e["created_at"][11:19],
            "case": e["event_data"].get("externalId"),
            "result": e["event_data"].get("result"),
            "bottles": e["custom_metadata"].get("bottleCount"),
            "image_id": e["images"][0]["file_id"] if e["images"] else None,
        }
        for e in value["events"]
    ]
```

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-4899a888613e6b261ba8ebc58f27bb7bec1caed7%2Fignition-designer-http-transform.png?alt=media" alt="The Script transform in the HTTP binding dialog, with the transform function that maps each event to time, case, result, bottles and image_id"><figcaption><p>The Script transform in the binding dialog.</p></figcaption></figure>

Change the fields to match your events. `event_data` holds the event type's fields, and `custom_metadata` holds your own (ex: `bottleCount`). `created_at` is in UTC.

The binding runs on the gateway, so only the gateway needs to reach port `8001` for the table. Learn more about [HTTP bindings](https://docs.inductiveautomation.com/docs/8.3/ignition-modules/perspective/working-with-perspective-components/bindings-in-perspective/http-bindings-in-perspective).

### Show a Time Window

To show the events since a point in time (ex: the start of the shift), add `start_time` to the binding's URL:

```
"http://<device-ip>:8001/v2/events?limit=1000&start_time="
  + replace(dateFormat(setTime(now(60000), 6, 0, 0), "yyyy-MM-dd'T'HH:mm:ssXXX"), "+", "%2B")
```

`setTime(now(60000), 6, 0, 0)` is 6:00 today in the gateway's timezone, and the expression runs again every minute. For a rolling window, use `addHours(now(60000), -8)` instead. Add `end_time` the same way to close the window.

Keep the `XXX` in the format. It adds the timezone offset (ex: `-05:00`), and the Event Store reads a time without one in the device's timezone. The `replace` encodes the `+` of a positive offset, which a URL would otherwise turn into a space.

Each request returns up to 1,000 events, newest first. If a window holds more, shorten it or copy the events into a database.

### Show an Event's Image

Show the image of the row an operator clicks:

1. Add a custom property to the view named `selectedImage`, with an empty string as its value.
2. Right-click the table and choose "Configure Events...". Under "Component Events", select "onRowClick", add a "Script" action, and add this line to its body:

```python
self.view.custom.selectedImage = event.value.image_id
```

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-0c018ca1c9d3983aee10fedac27701663ec506b9%2Fignition-designer-onrowclick.png?alt=media" alt="The Event Configuration dialog on the Events table with onRowClick selected and a Script action that sets self.view.custom.selectedImage"><figcaption><p>The <code>onRowClick</code> script on the table.</p></figcaption></figure>

3. Add an "Image" component and bind `props.source` to an expression:

```
if({view.custom.selectedImage} = "", "", "http://<device-ip>:8001/images/" + {view.custom.selectedImage})
```

To hide the `image_id` column, add it to the table's `props.columns` with `visible` set to `false`.

The script keeps the image in place when the next poll adds rows. A binding on the table's `props.selection` would lose the selection, because new data clears it.

The image loads in the operator's browser, so the operator's computer must reach port `8001`.

## Copy Events into a Database

A live query only sees what is still on the device, which removes old events as its storage fills. To keep history longer, or to report across devices, copy the events into a database. A gateway timer script runs whether or not anyone has a session open. Use one to copy new events into a table you own, then build reports on that table.

### Create the Table

This example uses PostgreSQL. Adjust the types for your database:

```sql
CREATE TABLE roboflow_events (
  id TEXT PRIMARY KEY,
  created_at TIMESTAMPTZ NOT NULL,
  stream_id TEXT,
  event_schema TEXT,
  event_data TEXT,
  image_id TEXT
);
```

The event `id` is the primary key, so an event the script reads twice is stored once.

### Create a Cursor Tag

Create a memory tag of type String (ex: `[default]Roboflow/EventCursor`). The script stores the time of the newest event it copied there.

### Add the Timer Script

1. In Designer, open "Scripting" > "Gateway Events" in the Project Browser and select "Timer".
2. Add a timer script. In "Choose Timer Settings", give it a name, set "Delay (ms)" to `5000`, and keep "Fixed Delay".

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-9f84be3b9062822351af742ae2b4e9a82d271073%2Fignition-designer-timer-settings.png?alt=media" alt="The Choose Timer Settings dialog with the name Copy Roboflow Events, Delay 5,000 ms, Enabled, Fixed Delay and Shared threading"><figcaption><p>The timer settings.</p></figcaption></figure>

3. Paste this script and replace the device address and database connection name:

```python
DEVICE = "http://192.168.1.50:8001"
DB = "MyDatabase"
CURSOR_TAG = "[default]Roboflow/EventCursor"

client = system.net.httpClient(timeout=10000, version="HTTP_1_1")
cursor = system.tag.readBlocking([CURSOR_TAG])[0].value or "1970-01-01T00:00:00Z"

response = client.get(DEVICE + "/v2/events", params={
    "start_time": cursor,
    "sort_direction": "asc",
    "limit": 1000,
})

if response.good:
    body = response.json
    for event in body["events"]:
        if system.db.runScalarPrepQuery("SELECT 1 FROM roboflow_events WHERE id = ?", [event["id"]], DB):
            continue
        images = event["images"]
        system.db.runPrepUpdate(
            "INSERT INTO roboflow_events (id, created_at, stream_id, event_schema, event_data, image_id) "
            "VALUES (?, CAST(? AS TIMESTAMPTZ), ?, ?, ?, ?)",
            [event["id"], event["created_at"], event["stream_id"], event["event_schema"],
             system.util.jsonEncode(event["event_data"]), images[0]["file_id"] if images else None],
            DB)
    if body["events"]:
        system.tag.writeBlocking([CURSOR_TAG], [body["next_timestamp"]])
else:
    system.util.getLogger("Roboflow").warn("Event Store returned %s" % response.statusCode)
```

<figure><img src="https://1126624165-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoO0Ceoy9R0lHDZATcPyh%2Fuploads%2Fgit-blob-bbfd9fb20f6bb1e2bd6bb6ecc30a2118477be1d2%2Fignition-designer-gateway-timer-script.png?alt=media" alt="The Gateway Event Scripts window with the Timer section selected, a timer named Copy Roboflow Events at 5,000 ms, and the script that copies events into the roboflow_events table"><figcaption><p>The timer script in "Gateway Event Scripts".</p></figcaption></figure>

Each run asks for events from the cursor time on, oldest first, up to 1,000 at a time. It stores the new ones and moves the cursor to the newest. If more than 1,000 events arrived, the next run copies the rest. The event at the cursor time comes back on every run, and the `SELECT` skips it.

To copy only some events, add filters to `params` (ex: `"event_schema": "quality_check"` or `"metadata_filter": "line:eq:Line 1"`).

{% hint style="warning" %}
The device removes old images as its storage fills. Rows older than that keep their data, but their image no longer loads. To keep images longer, download them in the script with `client.get(DEVICE + "/images/" + file_id).body` and store the bytes in your database.
{% endhint %}

## If the Event Store Requires a Key

The Event Store accepts requests without a key unless `API_KEY` is set on the service. If it is, send the key in an `X-API-Key` header: add `headers={"X-API-Key": key}` to `client.get(...)`, or add the header to the HTTP binding. Keep the key out of the view, ex: in a tag the binding reads.

## Troubleshooting

<table data-search="false"><thead><tr><th>Problem</th><th>Fix</th></tr></thead><tbody><tr><td>Status <code>401</code></td><td>The Event Store has an <code>API_KEY</code>. Send <code>X-API-Key</code>.</td></tr><tr><td>The table stays empty</td><td>From the gateway's host, run <code>curl http://&#x3C;device-ip>:8001/health</code>. If it fails, the gateway can't reach the device.</td></tr><tr><td>Rows stop arriving in the database</td><td>Check the cursor tag. If it holds a time in the future, clear it.</td></tr><tr><td>The image doesn't load for new events</td><td>The operator's computer can't reach port <code>8001</code>, or the session uses HTTPS. See <a href="/workflows/tutorials/integrate-with-ignition/live-video.md#perspective-over-https">Perspective Over HTTPS</a>.</td></tr><tr><td>The image doesn't load for old events</td><td>The device removed it. See the warning under Copy Events into a Database.</td></tr></tbody></table>

## Related

* [Live Video](/workflows/tutorials/integrate-with-ignition/live-video.md)
* [Live Data](/workflows/tutorials/integrate-with-ignition/live-data.md)
