Skip to main content

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.

OperationRouteAnswers
listDatasetsGET /datasetsWhich datasets your rulesets declare, and where each stands.
getDatasetGET /datasets/{datasetId}One dataset's declaration and which data version the active ruleset reads.
listDatasetVersionsGET /datasets/{datasetId}/versionsThe dataset's append-only version history, newest first.
listDatasetRowsGET /datasets/{datasetId}/versions/{version}/rowsOne 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:

stateMeaning
declaredThe ruleset declares the dataset and nothing is out of date. A dataset nothing has been pushed to yet is declared with a null latestVersion.
schemaChangedThe dataset is declared, but its newest data version was validated against a different declaration. A push is due.
notDeclaredThe 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
}
}
  • declaration is the declaration exactly as the ruleset stores it: the row schema, the key fields and the indexes. Only key and index fields can be used to filter rows (see below).
  • schemaSource says where a push would take that declaration from — the ruleset's working draft ("kind": "draft"), otherwise its latest active version ("kind": "active").
  • activeReads describes how the active ruleset version reads this dataset: the hash of its own declaration and readsVersion, the data version it reads under that hash. readsVersion is null when 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" }
MemberMeaning
fieldThe dot-path of a key or index field of the declaration. Any other field is refused.
operatorOne of equal, notEqual, lessThan, lessThanInclusive, greaterThan, greaterThanInclusive, in, notIn, contains, doesNotContain, approxEqual.
valueThe value to compare with. in and notIn need a list; any other value for those two operators is refused.
flagsOptional. 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.
toleranceRequired 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​

StatusCodeWhen
400VALIDATION_FAILEDA path or query parameter fails validation — a limit outside 1 to 100, a version that is not a positive integer, a fourth filter.
400DATASET_CURSOR_INVALIDThe cursor is invalid, expired, or does not belong to this request.
400DATASET_FILTER_INVALIDA row filter is malformed or names a field that cannot be filtered on.
401AUTHENTICATION_REQUIREDThe key is missing, malformed or revoked.
403FORBIDDENThe key lacks the read scope.
404NOT_FOUNDThe dataset or version does not exist or is not visible to this key.
404DATASET_VERSION_REMOVEDThe version's rows were removed by retention.
504DATASET_QUERY_TIMEOUTThe 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.