> ## 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.

# Custom data

> Create, filter, search, patch, and delete entries in an agent's JSON collections, and list the collections

Custom data is the agent's own storage: named collections of JSON entries, each optionally indexed for semantic search by a `searchText`. These routes are the REST twin of the [`Data`](/reference/sdk/data) runtime object and address the same collections your tools read.

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Reads need `knowledge:read` and writes `knowledge:write` on the agent; the host, the bearer header, and the error envelope are on the [REST API overview](/reference/rest/overview). Collections are not partitioned by environment.

## The entry

<ResponseField name="id" type="string">Entry id, a UUID.</ResponseField>
<ResponseField name="data" type="object">The JSON you stored.</ResponseField>
<ResponseField name="searchText" type="string">The text embedded for semantic search, when set.</ResponseField>
<ResponseField name="createdAt" type="number">Unix time in milliseconds.</ResponseField>
<ResponseField name="updatedAt" type="number">Unix time in milliseconds.</ResponseField>

Collection and entry ids are path segments: a collection named `search` cannot be read by id because `.../<collection>/search` is the search route.

## Endpoints

### GET /developer/agents/:agentId/custom-data

Lists the agent's collections with counts and timestamps.

<ParamField path="agentId" type="string" required>The agent.</ParamField>

**Response**

`200` with `{ "data": [...], "count" }`.

<ResponseField name="data" type="array">
  One item per collection: `name`, `entryCount`, `lastUpdatedAt`, `firstCreatedAt` (Unix milliseconds), and `indexes` on collections that declared one.

  <Expandable title="indexes">
    <ResponseField name="fields" type="string[]">The indexed `data` paths, one or two.</ResponseField>
    <ResponseField name="status" type="string">`pending`, `building`, `ready`, `failed`, or `rejected`.</ResponseField>
    <ResponseField name="error" type="string">Why the index is `failed` or `rejected`.</ResponseField>
    <ResponseField name="lastDeclaredAt, lastUsedAt" type="number">Unix milliseconds of the last declaration and the last filtered read that used it.</ResponseField>
  </Expandable>
</ResponseField>

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

Equivalent: `Data.collections()`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data', {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const collections: { data: Array<{ name: string; entryCount: number }>; count: number } = await response.json();
  for (const collection of collections.data) console.log(collection.name, collection.entryCount);
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### POST /developer/agents/:agentId/custom-data/:collection

Creates an entry and, when `searchText` is given, embeds it for semantic search.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField path="collection" type="string" required>Collection name; created on first write.</ParamField>
<ParamField body="data" type="any" required>The value to store: an object, array, string, number, or boolean. `null` and an empty string are refused.</ParamField>

<ParamField body="searchText" type="string">
  Text to embed. Include every term you expect a search to match. A string only; the options object the SDK's `Data.create()` types is not accepted here and answers `400` with `searchText must be a string`.
</ParamField>

**Response**

`201` with the entry.

**Errors**

| Status | Code or message               | Meaning                            | Fix                       |
| ------ | ----------------------------- | ---------------------------------- | ------------------------- |
| `400`  | `data should not be empty`    | `data` is missing, `null`, or `""` | Send a value              |
| `400`  | `searchText must be a string` | `searchText` is not a string       | Send a string, or omit it |

Equivalent: `Data.create('movies', data, 'Inception Nolan sci-fi')`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies', {
    method: 'POST',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: { title: 'Inception', year: 2010 }, searchText: 'Inception Nolan sci-fi heist dreams' }),
  });
  const entry: { id: string; data: { title: string; year: number }; createdAt: number } = await response.json();
  console.log(entry.id);
  ```

  ```bash cURL theme={null}
  curl -X POST "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "data": { "title": "Inception", "year": 2010 }, "searchText": "Inception Nolan sci-fi heist dreams" }'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/custom-data/:collection

Returns the entries that match a filter, one page at a time.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField path="collection" type="string" required>The collection.</ParamField>

<ParamField query="filter" type="string">
  A JSON [Lua Query](/reference/sdk/query) over `data`: dot notation for nested fields, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`, and `$and` or `$or` at the root. Omit it for every entry.
</ParamField>

<ParamField query="page" type="integer" default="1">Page number from 1.</ParamField>
<ParamField query="limit" type="integer" default="10">From 1 to 100; larger values are clamped.</ParamField>

**Response**

`200` with `{ "data": [...entries], "pagination": { currentPage, totalPages, totalCount, limit, hasNextPage, hasPrevPage, nextPage, prevPage } }`.

**Errors**

| Status | Code or message                           | Meaning                                                            | Fix               |
| ------ | ----------------------------------------- | ------------------------------------------------------------------ | ----------------- |
| `400`  | A filter validation message               | `filter` is not valid JSON or uses an operator outside the grammar | Fix the filter    |
| `400`  | `Pagination offset may not exceed 100000` | `(page - 1) × limit` is over 100,000                               | Narrow the filter |

Equivalent: `Data.get('movies', { year: { $gte: 2010 } }, 1, 20)`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ filter: JSON.stringify({ year: { $gte: 2010 } }), page: '1', limit: '20' });
  const response = await fetch(`https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { data: Array<{ id: string; data: Record<string, unknown> }>; pagination: { totalCount: number } } =
    await response.json();
  console.log(page.pagination.totalCount);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'filter={"year":{"$gte":2010}}' \
    --data-urlencode 'page=1' --data-urlencode 'limit=20'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/custom-data/:collection/search

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

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField path="collection" type="string" required>The collection.</ParamField>
<ParamField query="searchText" type="string" required>Natural-language query.</ParamField>
<ParamField query="limit" type="integer" default="5">From 1 to 20; larger values are clamped to 20.</ParamField>
<ParamField query="scoreThreshold" type="number" default="0.6">Minimum similarity from 0 to 1; results under it are dropped.</ParamField>

**Response**

`200` with `{ "data": [...entries with score], "count" }`; each entry carries a `score` from 0 to 1. This is a flat list, not the paginated envelope the filter route returns.

**Errors**

| Status | Code or message      | Meaning                 | Fix          |
| ------ | -------------------- | ----------------------- | ------------ |
| `400`  | A validation message | `searchText` is missing | Send a query |

Equivalent: `Data.search('movies', 'mind-bending thriller', 5, 0.7)`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ searchText: 'mind-bending thriller', limit: '5', scoreThreshold: '0.7' });
  const response = await fetch(`https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/search?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const results: { data: Array<{ id: string; score: number; data: Record<string, unknown> }>; count: number } =
    await response.json();
  for (const hit of results.data) console.log(hit.score.toFixed(2), hit.data['title']);
  ```

  ```bash cURL theme={null}
  curl -G "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/search" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    --data-urlencode 'searchText=mind-bending thriller' --data-urlencode 'limit=5' --data-urlencode 'scoreThreshold=0.7'
  ```
</CodeGroup>

### GET /developer/agents/:agentId/custom-data/:collection/:entryId

Returns one entry.

**Errors**

| Status | Code or message               | Meaning                                         | Fix           |
| ------ | ----------------------------- | ----------------------------------------------- | ------------- |
| `404`  | `Custom data entry not found` | No such entry in this collection for this agent | Check the ids |

Equivalent: `Data.getEntry('movies', '<entryId>')`.

### PUT /developer/agents/:agentId/custom-data/:collection/:entryId

Merges new fields into an entry and optionally re-embeds it.

<ParamField body="data" type="object" required>Fields to merge into the stored `data`.</ParamField>
<ParamField body="searchText" type="string">New text to embed.</ParamField>

**Response**

`200` with `{ "status": "success", "message": "Custom data entry updated" }`.

**Errors**

| Status | Code or message               | Meaning                    | Fix                      |
| ------ | ----------------------------- | -------------------------- | ------------------------ |
| `400`  | `data should not be empty`    | `data` is missing or empty | Send the fields to merge |
| `404`  | `Custom data entry not found` | No such entry              | Check the ids            |

Equivalent: `Data.update('movies', '<entryId>', { rating: 9 }, 'Inception Nolan sci-fi')`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/<<ENTRY_ID>>', {
    method: 'PUT',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ data: { rating: 9 } }),
  });
  const result: { status: string; message: string } = await response.json();
  console.log(result.status);
  ```

  ```bash cURL theme={null}
  curl -X PUT "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/<<ENTRY_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "data": { "rating": 9 } }'
  ```
</CodeGroup>

### PATCH /developer/agents/:agentId/custom-data/:collection/:entryId

Sets and unsets top-level fields of `data` atomically, and sets or clears the embedding.

<ParamField body="set" type="object">Top-level fields to write.</ParamField>
<ParamField body="unset" type="string[]">Top-level fields to remove.</ParamField>

<ParamField body="searchText" type="string or null">
  A string re-embeds the entry; `null` clears both the text and its vector; omit it to leave the embedding alone.
</ParamField>

**Response**

`200` with `{ "status": "success", "message" }`; `404` `Custom data entry not found` when the entry does not exist.

### DELETE /developer/agents/:agentId/custom-data/:collection/:entryId

Deletes one entry.

**Response**

`200` with `{ "status": "success", "message": "Custom data entry deleted" }`; `404` `Custom data entry not found` when the entry does not exist.

Equivalent: `Data.delete('movies', '<entryId>')`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/<<ENTRY_ID>>', {
    method: 'DELETE',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const result: { status: string } = await response.json();
  console.log(result.status);
  ```

  ```bash cURL theme={null}
  curl -X DELETE "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/custom-data/movies/<<ENTRY_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

## See also

* [`Data`](/reference/sdk/data) — the same collections from agent code, with declared indexes
* [Lua Query](/reference/sdk/query) — the filter grammar the `filter` parameter accepts
* [Store and search data](/build/store-and-search-data) — choosing filters or semantic search
* [User data](/reference/rest/user-data) — one record per end user instead of a collection
