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

# Data

> Agent-scoped JSON collections with filtering and semantic search

`Data` stores JSON entries in named collections that belong to the agent and retrieves them by id, by a [Lua Query](/reference/sdk/query) filter, or by semantic search over an entry's `searchText`. Available in tools, jobs, webhooks, triggers, processors, and workflow code steps. For one record per end user, use [`User`](/reference/sdk/user) instead.

*Verified against lua-cli 3.33.0.*

```ts theme={null}
import { Data } from 'lua-cli';
```

## Quick example

```ts theme={null}
import { Data } from 'lua-cli';

const entry = await Data.create('movies', { title: 'Inception', year: 2010 }, 'Inception Nolan sci-fi thriller');
const results = await Data.search('movies', 'mind-bending thriller', 5, 0.7);
const recent = await Data.get('movies', { year: { $gte: 2020 } }, 1, 20);
```

## Methods

### collections()

Lists the agent's collections with entry counts, timestamps, and the status of any declared indexes.

```ts theme={null}
Data.collections(): Promise<CustomDataCollectionsResponse>
```

**Returns**

<ResponseField name="data" type="CustomDataCollectionInfo[]">
  One entry per collection.

  <Expandable title="properties">
    <ResponseField name="name" type="string">Collection name.</ResponseField>
    <ResponseField name="entryCount" type="number">Number of entries.</ResponseField>
    <ResponseField name="lastUpdatedAt" type="number">Unix time in milliseconds of the newest write.</ResponseField>
    <ResponseField name="firstCreatedAt" type="number">Unix time in milliseconds of the oldest entry.</ResponseField>

    <ResponseField name="indexes" type="CustomDataIndexStatus[]">
      Present when the agent declared indexes: `fields`, `status` (`pending`, `building`, `ready`, `failed`, or `rejected`), `error` on `failed` and `rejected`, `lastDeclaredAt`, and `lastUsedAt`.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="count" type="number">Number of collections.</ResponseField>

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const { data: collections } = await Data.collections();
for (const collection of collections) {
  for (const index of collection.indexes ?? []) {
    console.log(collection.name, index.fields, index.status, index.error ?? '');
  }
}
```

**Errors** — `Failed to list custom data collections`.

<Info>
  Local runs only. The deployed runtime doesn't expose `collections()` yet and fails with `Data.collections is not a function`. Inspect index status from `lua test`.
</Info>

### create()

Creates an entry and, when `searchText` is given, indexes it for `search()`.

```ts theme={null}
Data.create(
  collectionName: string,
  data: Record<string, any>,
  optionsOrSearchText?: string | CreateCustomDataOptions
): Promise<DataEntryInstance>
```

<ParamField path="collectionName" type="string" required>
  Collection name, for example `movies`.
</ParamField>

<ParamField path="data" type="Record<string, any>" required>
  Any JSON-serializable object.
</ParamField>

<ParamField path="optionsOrSearchText" type="string | { searchText?: string; index?: Array<string | string[]> }">
  A string sets `searchText`, the text `search()` embeds; include every term you expect queries to use. An object also accepts `index`: the `data` fields this agent filters on, each a single path (`'businessId'`) or a compound of two paths (`['country', 'businessId']`). Declare indexes on every write; the platform builds them asynchronously and removes an index 14 days after its last declaration or matching filtered read. A compound index serves filters on its leftmost field alone or on both fields. Over-limit or invalid declarations are rejected and reported in `collections()`, never trimmed.
</ParamField>

**Returns**

<ResponseField name="entry" type="DataEntryInstance">
  The stored entry. Fields of `data` are readable directly (`entry.title`) and through `entry.data`.

  <Expandable title="properties">
    <ResponseField name="id" type="string">Entry id, for example `entry_abc123`.</ResponseField>
    <ResponseField name="collectionName" type="string">The collection the entry belongs to.</ResponseField>
    <ResponseField name="data" type="Record<string, any>">The stored object.</ResponseField>
  </Expandable>
</ResponseField>

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const ticket = await Data.create('tickets', { subject: 'Login fails', status: 'open', accountId: 'acct_abc123' }, 'login fails password reset');
console.log(ticket.id, ticket.subject);
```

**Errors** — `Failed to create custom data entry` when the platform rejects the write.

<Info>
  Local runs only. The deployed runtime doesn't accept the options object yet and fails with `searchText must be a string`. In deployed code, pass `searchText` as a plain string.
</Info>

### get()

Returns one page of entries that match a filter.

```ts theme={null}
Data.get(collectionName: string, filter?: LuaQuery, page?: number, limit?: number): Promise<GetCustomDataResponse>
```

<ParamField path="collectionName" type="string" required>
  Collection to read.
</ParamField>

<ParamField path="filter" type="LuaQuery">
  Matches fields of each entry's `data`. Grammar, operators, and limits are on the [Lua Query](/reference/sdk/query) page. Omit it to page through everything.
</ParamField>

<ParamField path="page" type="number" default={1}>
  Page number, starting at 1.
</ParamField>

<ParamField path="limit" type="number" default={10}>
  Entries per page. The maximum is 100.
</ParamField>

**Returns**

<ResponseField name="data" type="CustomDataEntry[]">
  Plain entries, not `DataEntryInstance` objects: read fields through `entry.data`.

  <Expandable title="properties">
    <ResponseField name="id" type="string">Entry id.</ResponseField>
    <ResponseField name="data" type="Record<string, any>">The stored object.</ResponseField>
    <ResponseField name="createdAt" type="number">Unix time in milliseconds.</ResponseField>
    <ResponseField name="updatedAt" type="number">Unix time in milliseconds.</ResponseField>
    <ResponseField name="searchText" type="string">The indexed text, when set.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="pagination" type="Pagination">
  `currentPage`, `totalPages`, `totalCount`, `limit`, `hasNextPage`, `hasPrevPage`, `nextPage` (number or `null`), and `prevPage` (number or `null`).
</ResponseField>

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const page = await Data.get('tickets', { status: 'open', accountId: 'acct_abc123' }, 1, 50);
const subjects = page.data.map((entry) => entry.data.subject);
console.log(subjects, page.pagination.totalCount);
```

**Errors** — an invalid filter is rejected with a `FILTER_*` code (see [Lua Query errors](/reference/sdk/query#errors)). A filter on an undeclared field of a large collection fails with an error that names the field and the `index` declaration to add.

### getEntry()

Returns one entry by id.

```ts theme={null}
Data.getEntry(collectionName: string, entryId: string): Promise<DataEntryInstance>
```

<ParamField path="collectionName" type="string" required>Collection to read.</ParamField>
<ParamField path="entryId" type="string" required>The entry's `id`.</ParamField>

**Returns** — the entry as a [`DataEntryInstance`](#dataentryinstance).

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const ticket = await Data.getEntry('tickets', 'entry_abc123');
console.log(ticket.subject, ticket.data.status);
```

**Errors** — `Failed to get custom data entry` when no entry has that id.

### update()

Merges fields into an entry and optionally replaces its `searchText`.

```ts theme={null}
Data.update(
  collectionName: string,
  entryId: string,
  data: Record<string, any>,
  optionsOrSearchText?: string | CreateCustomDataOptions
): Promise<UpdateCustomDataResponse>
```

<ParamField path="collectionName" type="string" required>Collection that holds the entry.</ParamField>
<ParamField path="entryId" type="string" required>The entry's `id`.</ParamField>
<ParamField path="data" type="Record<string, any>" required>Fields to add or replace. Other fields are kept.</ParamField>

<ParamField path="optionsOrSearchText" type="string | { searchText?: string; index?: Array<string | string[]> }">
  As on `create()`: a string replaces `searchText`; an object may also refresh index declarations.
</ParamField>

**Returns**

<ResponseField name="status" type="string">`success` on a completed write.</ResponseField>
<ResponseField name="message" type="string">Human-readable result.</ResponseField>

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

await Data.update('tickets', 'entry_abc123', { status: 'closed', closedAt: Date.now() });
```

**Errors** — `Failed to update custom data entry` when no entry has that id or the platform rejects the write.

<Info>
  Local runs only. The deployed runtime doesn't accept the options object yet and fails with `searchText must be a string`. In deployed code, pass `searchText` as a plain string.
</Info>

### search()

Returns the entries whose `searchText` is semantically closest to a query.

```ts theme={null}
Data.search(collectionName: string, searchText: string, limit?: number, scoreThreshold?: number): Promise<DataEntryInstance[]>
```

<ParamField path="collectionName" type="string" required>Collection to search.</ParamField>
<ParamField path="searchText" type="string" required>Natural-language query.</ParamField>
<ParamField path="limit" type="number" default={10}>Maximum number of results. The maximum is 20.</ParamField>
<ParamField path="scoreThreshold" type="number" default={0.6}>Minimum similarity from 0 to 1. Results with a lower score are dropped.</ParamField>

**Returns** — a flat array of [`DataEntryInstance`](#dataentryinstance), each with a `score` from 0 to 1, best match first. Not the `{ data, pagination }` envelope that `get()` returns.

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const matches = await Data.search('tickets', 'cannot sign in', 5, 0.7);
for (const match of matches) {
  console.log(match.subject, match.score);
}
```

**Errors** — none beyond network errors.

### delete()

Deletes one entry.

```ts theme={null}
Data.delete(collectionName: string, entryId: string): Promise<DeleteCustomDataResponse>
```

<ParamField path="collectionName" type="string" required>Collection that holds the entry.</ParamField>
<ParamField path="entryId" type="string" required>The entry's `id`.</ParamField>

**Returns**

<ResponseField name="status" type="string">`success` on a completed delete.</ResponseField>
<ResponseField name="message" type="string">Human-readable result.</ResponseField>

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

await Data.delete('tickets', 'entry_abc123');
```

**Errors** — `Failed to delete custom data entry` when no entry has that id.

## DataEntryInstance

The object `create()`, `getEntry()`, and `search()` return. Fields of `data` are readable and writable directly (`entry.subject`) and through `entry.data`; direct assignments stay local until `save()`.

<ResponseField name="id" type="string">Entry id.</ResponseField>
<ResponseField name="collectionName" type="string">The entry's collection.</ResponseField>
<ResponseField name="data" type="Record<string, any>">The stored object.</ResponseField>
<ResponseField name="score" type="number">Similarity from 0 to 1. Set on `search()` results only.</ResponseField>

### update()

Merges fields into the entry on the server and locally.

```ts theme={null}
entry.update(data: Record<string, any>, searchText?: string): Promise<Record<string, any>>
```

**Returns** — the merged `data`.

**Errors** — `Failed to update custom data entry`, with the platform's error as `cause`.

### patch()

Sets and removes top-level fields, and optionally replaces or clears `searchText`, in one request.

```ts theme={null}
entry.patch(mutation: { set?: Record<string, any>; unset?: string[]; searchText?: string | null }): Promise<Record<string, any>>
```

<ParamField path="mutation.set" type="Record<string, any>">Fields to set. A `null` value is stored as `null`.</ParamField>
<ParamField path="mutation.unset" type="string[]">Top-level fields to remove.</ParamField>
<ParamField path="mutation.searchText" type="string | null">A replacement `searchText`, or `null` to clear it.</ParamField>

**Returns** — the `data` after the mutation.

**Errors** — `Failed to patch custom data entry`, with the platform's error as `cause`.

### unset()

Removes top-level fields; shorthand for `patch({ unset: fields })`.

```ts theme={null}
entry.unset(...fields: string[]): Promise<Record<string, any>>
```

### delete()

Deletes the entry.

```ts theme={null}
entry.delete(): Promise<boolean>
```

**Returns** — `true`.

**Errors** — `Failed to delete custom data entry`, with the platform's error as `cause`.

### save()

Writes the whole local `data` to the server, optionally with a replacement `searchText`.

```ts theme={null}
entry.save(searchText?: string): Promise<boolean>
```

**Returns** — `true`.

**Example**

```ts theme={null}
import { Data } from 'lua-cli';

const ticket = await Data.getEntry('tickets', 'entry_abc123');
ticket.status = 'closed';
ticket.closedAt = Date.now();
await ticket.save();
```

**Errors** — `Failed to save data entry`, with the platform's error as `cause`.

### toJSON()

Returns `{ ...data, score, id, collectionName }`, which is what `JSON.stringify(entry)` and `console.log(entry)` print.

## Limits

| Limit                                              | Value                                                                             |
| -------------------------------------------------- | --------------------------------------------------------------------------------- |
| `get()` entries per page                           | 100                                                                               |
| `search()` results                                 | 20                                                                                |
| `scoreThreshold`                                   | 0 to 1                                                                            |
| Fields per index                                   | 2                                                                                 |
| Index declarations per call                        | 3                                                                                 |
| Indexes per agent                                  | 5                                                                                 |
| Queued, unbuilt declarations per agent             | 10                                                                                |
| Index kept after last declaration or filtered read | 14 days                                                                           |
| Index field path                                   | Dotted, up to 5 segments of letters, digits, `_`, `-`; no `$`, no numeric segment |
| Filter size, depth, and operands                   | See [Lua Query limits](/reference/sdk/query#limits)                               |

## Types

`DataEntryInstance` and `LuaQuery` are exported. The result shapes (`CustomDataEntry`, `GetCustomDataResponse`, `CreateCustomDataOptions`, `CustomDataCollectionsResponse`, `UpdateCustomDataResponse`, `DeleteCustomDataResponse`) are not; name them from the method that returns them.

```ts theme={null}
import { Data } from 'lua-cli';
import type { DataEntryInstance, LuaQuery } from 'lua-cli';

type GetPage = Awaited<ReturnType<typeof Data.get>>;
type CreateOptions = Exclude<Parameters<typeof Data.create>[2], string | undefined>;

export async function openTickets(filter: LuaQuery): Promise<GetPage> {
  return Data.get('tickets', filter);
}

export function firstHit(results: DataEntryInstance[]): DataEntryInstance | undefined {
  return results[0];
}

export const indexed: CreateOptions = { index: ['accountId', ['country', 'accountId']] };
```

## See also

* [`User`](/reference/sdk/user) — one persistent record per end user
* [Lua Query](/reference/sdk/query) — the filter grammar `get()` accepts
* [Store and search data](/build/store-and-search-data) — how-to
* [Custom data REST API](/reference/rest/custom-data) — the same collections over HTTP
