Browse reference data
Reference data you pushed can be read back: which
datasets exist, what their declarations look like, every data version ever
created, and the rows of any version that still holds them. Browsing needs the
read scope only and never changes anything.
| Operation | Route | Answers |
|---|---|---|
listDatasets | GET /datasets | Which datasets your rulesets declare, and where each stands. |
getDataset | GET /datasets/{datasetId} | One dataset's declaration and which data version the active ruleset reads. |
listDatasetVersions | GET /datasets/{datasetId}/versions | The dataset's append-only version history, newest first. |
listDatasetRows | GET /datasets/{datasetId}/versions/{version}/rows | One version's rows in key order, optionally filtered. |
Every list is cursor-paginated: pages carry items and an opaque
nextCursor, and a null nextCursor means the last page. Treat the cursor
as an opaque string — never derive a page number or an ordering from it.
List datasets
Each item is one (ruleset, dataset) pair of your tenant. Narrow the list with
the exact ruleset name and the exact dataset name, or page through all of it
with limit (1 to 100, 50 by default) and cursor:
import { client, listDatasets } from '@one-unity/rule-line-sdk';
client.setConfig({
baseUrl: 'https://api.rule-line.example.com',
auth: 'rl_key_your_api_key',
});
const page = await listDatasets({ query: { limit: 50 } });
if (page.error) {
throw new Error(`list failed: ${page.error.code}`);
}
for (const dataset of page.data.items) {
const rows = dataset.latestVersion ? `${dataset.latestVersion.rowCount} row(s)` : 'no data yet';
console.log(`${dataset.rulesetName}/${dataset.name}: ${dataset.state}, ${rows}`);
}
GET /datasets?limit=50 HTTP/1.1
Host: api.rule-line.example.com
Authorization: Bearer rl_key_your_api_key
{
"items": [
{
"id": "44444444-4444-4444-8444-444444444444",
"rulesetName": "invoice-routing",
"workingOrActiveVersionId": "33333333-3333-4333-8333-333333333333",
"name": "creditors",
"declared": true,
"state": "declared",
"currentSchemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"latestVersion": {
"version": 3,
"kind": "delta",
"rowCount": 2,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"createdAt": "2026-09-29T08:30:00.000Z"
}
}
],
"nextCursor": null
}
Items are ordered by ruleset name, then dataset name. state is one of:
state | Meaning |
|---|---|
declared | The ruleset declares the dataset and nothing is out of date. A dataset nothing has been pushed to yet is declared with a null latestVersion. |
schemaChanged | The dataset is declared, but its newest data version was validated against a different declaration. A push is due. |
notDeclared | The ruleset no longer declares the dataset, or declares it in a form a push cannot be validated against. |
workingOrActiveVersionId is the id of one version of the ruleset the dataset
belongs to: the working version's id when the ruleset has one, otherwise its
newest activated version's id. It changes whenever a version of the ruleset is
created or activated, so do not store it as the ruleset's identity. Use
rulesetName as the stable key. The member is null only when this key can see
no version of that ruleset, and it is present on every item and on the detail of
one dataset.
latestVersion is the dataset's newest data version whatever declaration it
was validated against. currentSchemaHash is the hash of the declaration a
push would be validated against; it is null when the dataset is not
declared.
To walk every dataset without handling cursors yourself, use the generated iterator:
import { listDatasetsIterator } from '@one-unity/rule-line-sdk';
for await (const dataset of listDatasetsIterator({ limit: 50 })) {
console.log(dataset.id, dataset.name, dataset.state);
}
Read one dataset
getDataset adds the declaration itself and the data version the active
ruleset reads:
import { client, getDataset } from '@one-unity/rule-line-sdk';
client.setConfig({
baseUrl: 'https://api.rule-line.example.com',
auth: 'rl_key_your_api_key',
});
const detail = await getDataset({ path: { datasetId: '44444444-4444-4444-8444-444444444444' } });
if (detail.error) {
throw new Error(`dataset read failed: ${detail.error.code}`);
}
console.log(`state: ${detail.data.state}`);
console.log(`the active ruleset reads version: ${detail.data.activeReads?.readsVersion ?? 'none yet'}`);
GET /datasets/44444444-4444-4444-8444-444444444444 HTTP/1.1
Host: api.rule-line.example.com
Authorization: Bearer rl_key_your_api_key
{
"id": "44444444-4444-4444-8444-444444444444",
"rulesetName": "invoice-routing",
"workingOrActiveVersionId": "33333333-3333-4333-8333-333333333333",
"name": "creditors",
"declared": true,
"state": "declared",
"currentSchemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"latestVersion": {
"version": 3,
"kind": "delta",
"rowCount": 2,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"createdAt": "2026-09-29T08:30:00.000Z"
},
"declaration": {
"schema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"vatId": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "vatId", "name"],
"additionalProperties": false
},
"key": ["id"],
"indexes": ["vatId"]
},
"schemaSource": {
"kind": "draft",
"rulesetVersionId": "66666666-6666-4666-8666-666666666666",
"version": 4,
"draftRevision": 7
},
"activeReads": {
"rulesetVersionId": "77777777-7777-4777-8777-777777777777",
"version": 3,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"readsVersion": 3
}
}
declarationis the declaration exactly as the ruleset stores it: the row schema, thekeyfields and theindexes. Only key and index fields can be used to filter rows (see below).schemaSourcesays where a push would take that declaration from — the ruleset's working draft ("kind": "draft"), otherwise its latest active version ("kind": "active").activeReadsdescribes how the active ruleset version reads this dataset: the hash of its own declaration andreadsVersion, the data version it reads under that hash.readsVersionisnullwhen nothing has been pushed under that hash yet, or the rows were removed. It does not move when a draft changes the schema — the active ruleset keeps reading the last version valid under its own declaration until the draft is activated.
Read the version history
Every push is recorded in an append-only history. The list is newest first and
can be narrowed to the versions validated against one declaration with
schemaHash:
import { client, listDatasetVersionsIterator } from '@one-unity/rule-line-sdk';
client.setConfig({
baseUrl: 'https://api.rule-line.example.com',
auth: 'rl_key_your_api_key',
});
for await (const version of listDatasetVersionsIterator({ datasetId: '44444444-4444-4444-8444-444444444444' }, { limit: 50 })) {
const state = version.rowsRemovedAt ? `rows removed ${version.rowsRemovedAt}` : `${version.rowCount} row(s)`;
console.log(`v${version.version} (${version.kind}) by ${version.actor.kind}: ${state}`);
}
GET /datasets/44444444-4444-4444-8444-444444444444/versions?limit=50 HTTP/1.1
Host: api.rule-line.example.com
Authorization: Bearer rl_key_your_api_key
{
"items": [
{
"version": 3,
"kind": "delta",
"baseVersion": 2,
"rowCount": 2,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"contentHash": "3b7e0a9d5c1f4826b8d0e2a4c6f81357a9bdf1e3c5072849ab6cd8ef01234567",
"actor": { "kind": "apiKey", "id": "55555555-5555-4555-8555-555555555555" },
"reason": "Supplier C-1003 closed",
"createdAt": "2026-09-29T08:30:00.000Z",
"rowsRemovedAt": null
},
{
"version": 2,
"kind": "full",
"baseVersion": null,
"rowCount": 3,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"contentHash": "c4d1e8a2b7f9035d6e1a4c8b0f2d5a7913e6b8c0d2f4a6b8c1e3d5f70912a4b6",
"actor": { "kind": "user", "id": "88888888-8888-4888-8888-888888888888" },
"reason": null,
"createdAt": "2026-09-01T07:00:00.000Z",
"rowsRemovedAt": "2026-09-29T02:00:00.000Z"
}
],
"nextCursor": null
}
Each entry names who pushed it (actor.kind is apiKey or user, with the
key's or user's id — never secret material), the reason they gave, the
baseVersion a delta was applied on, and two hashes: schemaHash for the
declaration the rows were validated against and contentHash for the
version's content. rowsRemovedAt is set once retention has removed the
version's rows. The history entry itself is never removed — see
Personal data in reference data.
Page through a version's rows
listDatasetRows returns one version's rows in key order. The path names the
dataset and the version number; rows are read from that one immutable version,
so a push that lands while you are paging never mixes two versions in your
result:
import { client, listDatasetRows } from '@one-unity/rule-line-sdk';
client.setConfig({
baseUrl: 'https://api.rule-line.example.com',
auth: 'rl_key_your_api_key',
});
const page = await listDatasetRows({
path: { datasetId: '44444444-4444-4444-8444-444444444444', version: 3 },
query: { limit: 100 },
});
if (page.error) {
throw new Error(`rows read failed: ${page.error.code}`);
}
console.log(`version ${page.data.version.version} holds ${page.data.version.rowCount} row(s)`);
for (const { key, row } of page.data.items) {
console.log(key, row);
}
GET /datasets/44444444-4444-4444-8444-444444444444/versions/3/rows?limit=100 HTTP/1.1
Host: api.rule-line.example.com
Authorization: Bearer rl_key_your_api_key
{
"version": {
"version": 3,
"rowCount": 2,
"schemaHash": "9f2c1d7a5b3e48c6a0d4f8e2b6c1a3d5e7f90a2b4c6d8e0f1a3b5c7d9e1f3a5b",
"rowsRemovedAt": null
},
"items": [
{
"key": "[\"C-1001\"]",
"row": { "id": "C-1001", "vatId": "DE000000001", "name": "Example Supplies GmbH" }
},
{
"key": "[\"C-1002\"]",
"row": { "id": "C-1002", "vatId": "DE000000012", "name": "Sample Freight AG" }
}
],
"nextCursor": null,
"offset": 0
}
key is the row's canonical key text: the key field values as a JSON array,
the same string a lookup step records in a run's trace. To read every row
without handling cursors yourself, use listDatasetRowsIterator, which takes
the path first and the query second:
import { listDatasetRowsIterator } from '@one-unity/rule-line-sdk';
for await (const { key, row } of listDatasetRowsIterator(
{ datasetId: '44444444-4444-4444-8444-444444444444', version: 3 },
{ limit: 100 },
)) {
console.log(key, row);
}
Every count comes from version metadata
version.rowCount (and every rowCount in the lists above) is the count the
version was created with. It is stored with the version — it is not counted
while you read, and it is never derived from a cursor. It is also not a
filtered total: when you filter, version.rowCount still reports the whole
version, and no response carries the number of rows that matched. Counting
matches would cost a second full scan and would be stale by the next page. If
you need the number of matches, page through them and count.
Where a page sits
offset is the zero-based position of items[0] among the version's rows, in
the order the page is returned in. A page of items.length rows therefore
covers positions offset to offset + items.length - 1, and a console or a
script that wants to show "rows 51–100" displays offset + 1 to
offset + items.length. The first page has offset: 0; the page a
nextCursor opens has offset equal to the rows returned before it. The server
issues the number inside the signed cursor, so it is right on any page you
reach by following nextCursor, including one you re-open from a cursor you
kept, for as long as that cursor is valid (see Cursors: one hour).
After that the cursor is refused and you start again from the first page.
When you filter, offset counts within the matches: the first page of a
filtered read is still offset: 0, and the next is the number of matches
already returned. As with every count above, a filtered page states its
position only — no response carries how many rows matched in total.
Filter the rows
Pass one to three filter query parameters to narrow a page. Each is JSON text
in the same shape as a lookup step's match criterion, so trying a lookup by
hand returns exactly what the lookup would:
{ "field": "vatId", "operator": "equal", "value": "DE000000001" }
| Member | Meaning |
|---|---|
field | The dot-path of a key or index field of the declaration. Any other field is refused. |
operator | One of equal, notEqual, lessThan, lessThanInclusive, greaterThan, greaterThanInclusive, in, notIn, contains, doesNotContain, approxEqual. |
value | The value to compare with. in and notIn need a list; any other value for those two operators is refused. |
flags | Optional. trim strips surrounding whitespace on both sides of the comparison and caseInsensitive compares case-folded text. Legal on text fields only: a flag on any other field is refused. |
tolerance | Required for approxEqual, which has no default tolerance and is refused without one; ignored by every other operator. It is the allowed difference on a number field, as text ("0.01"), so it is compared exactly. |
A filter is checked against the declaration of the version whose rows you are reading, not against the dataset's current declaration: a field that has since been removed or re-indexed is still filterable on an older version that declared it, and a field the older version did not index is refused there even if the current declaration indexes it.
A row must satisfy every filter. At most three filters are accepted — a fourth
is refused as a validation failure — and each is at most 4,096 characters of
JSON. Comparing a field with a value of another type matches nothing (or, for
notEqual and notIn, everything); it is not an error.
In the SDK, pass the JSON text as a string, or a list of up to three strings:
import { client, listDatasetRows } from '@one-unity/rule-line-sdk';
client.setConfig({
baseUrl: 'https://api.rule-line.example.com',
auth: 'rl_key_your_api_key',
});
const filtered = await listDatasetRows({
path: { datasetId: '44444444-4444-4444-8444-444444444444', version: 3 },
query: {
filter: [
JSON.stringify({ field: 'vatId', operator: 'contains', value: 'de00000', flags: ['caseInsensitive'] }),
JSON.stringify({ field: 'id', operator: 'in', value: ['C-1001', 'C-1002'] }),
],
},
});
if (filtered.error) {
throw new Error(`filtered read failed: ${filtered.error.code}`);
}
console.log(`${filtered.data.items.length} row(s) on this page`);
Over raw HTTP each filter is a separate, URL-encoded filter parameter:
GET /datasets/44444444-4444-4444-8444-444444444444/versions/3/rows?filter=%7B%22field%22%3A%22vatId%22%2C%22operator%22%3A%22equal%22%2C%22value%22%3A%22DE000000001%22%7D HTTP/1.1
Host: api.rule-line.example.com
Authorization: Bearer rl_key_your_api_key
A filter that is not valid JSON, is not in the shape above, names a field
that is not a key or index field, uses approxEqual without a tolerance,
gives in or notIn something other than a list, or puts a text flag on a
field that is not text is refused with
DATASET_FILTER_INVALID.
Cursors
The nextCursor of a row page is bound to everything that produced it: the
dataset, the version, the exact filters and the page size. Send it back
unchanged with the same path, the same filter parameters and the same
limit. A cursor:
- cannot be reused against another version, another filter or another
limit; - expires one hour after it was issued;
- is only valid for the tenant that received it, and cannot be edited.
Any of these renders
DATASET_CURSOR_INVALID.
Start again from the first page. The dataset list and the version history
paginate the same way, each bound to its own filters and page size.
When a version's rows were removed
A superseded data version loses its rows after the raw-content retention
window, unless it is still in use. Asking for the rows of such a version is a
404 with a code of its own — distinct from the plain
NOT_FOUND of a dataset or version that
never existed:
{
"type": "https://<problem-type-host>/problems/dataset-version-removed",
"title": "The rows of this dataset version were removed by the retention policy.",
"status": 404,
"code": "DATASET_VERSION_REMOVED"
}
DATASET_VERSION_REMOVED
means the version existed and its history entry is still listed (with
rowsRemovedAt set), but its rows are gone. Read a newer version instead.
type is a stable identifier for the kind of problem, not a page to fetch.
Every deployment sends it as https:// followed by rule-line.dev/problems/
and the kebab-case form of the code; the example above writes the host as
<problem-type-host>. Branch on status and code, not on type or title.
When a row query times out
A row query that runs past its time limit is a 504 with a code of its own,
not a generic server error. Narrow the filter parameters, so the query has
less to scan, and retry:
{
"type": "https://<problem-type-host>/problems/dataset-query-timeout",
"title": "The dataset row query exceeded its time limit. Narrow the filters and retry.",
"status": 504,
"code": "DATASET_QUERY_TIMEOUT"
}
DATASET_QUERY_TIMEOUT
says nothing is wrong with the request itself: the same request can succeed on a
retry, and a request with narrower filters is likelier to.
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | A path or query parameter fails validation — a limit outside 1 to 100, a version that is not a positive integer, a fourth filter. |
| 400 | DATASET_CURSOR_INVALID | The cursor is invalid, expired, or does not belong to this request. |
| 400 | DATASET_FILTER_INVALID | A row filter is malformed or names a field that cannot be filtered on. |
| 401 | AUTHENTICATION_REQUIRED | The key is missing, malformed or revoked. |
| 403 | FORBIDDEN | The key lacks the read scope. |
| 404 | NOT_FOUND | The dataset or version does not exist or is not visible to this key. |
| 404 | DATASET_VERSION_REMOVED | The version's rows were removed by retention. |
| 504 | DATASET_QUERY_TIMEOUT | The row query exceeded its time limit. Narrow the filters and retry. |
See the GET /datasets reference and
the reference for
GET /datasets/{datasetId}/versions/{version}/rows
for the complete schemas.