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

# Store and search data

> Store JSON entries in Data collections, filter them with Lua Query, and find them by meaning with semantic search

After this guide, your tools store records in an agent-scoped collection, read them back by exact filter, and find them by meaning. [`Data`](/reference/sdk/data) holds many records per agent; for one record per end user, write to the end user's record instead ([Identify users](/build/identify-users)).

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A skill on the agent to hold the tools ([Add a tool to a skill](/build/add-a-tool)).
* A test collection name. Collections are shared by the sandbox and production ([About environments](/concepts/environments)), so a local run writes the same data the live agent reads.

<Steps>
  <Step title="Store an entry with search text">
    `Data.create` takes the collection, the object to store, and an optional string that is embedded for semantic search. Put every term a question might use into that string: it is stored beside `data` as the entry's `searchText` and is the only text `search()` matches against.

    ```ts src/skills/tools/AddKnownIssueTool.ts theme={null}
    import { LuaTool, Data } from 'lua-cli';
    import { z } from 'zod';

    export default class AddKnownIssueTool implements LuaTool {
      name = 'add_known_issue';
      description = 'Record a known product issue and its workaround so support can find it later.';
      inputSchema = z.object({
        product: z.string().describe('Product code, for example router-x1'),
        title: z.string().describe('One-line summary of the issue'),
        workaround: z.string().describe('What the customer can do until it is fixed'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        // Everything a support question might mention goes into the indexed text.
        const searchText = `${input.product} ${input.title} ${input.workaround}`;
        const entry = await Data.create('known-issues', { ...input, status: 'open' }, searchText);
        return { id: entry.id, product: entry.product, status: entry.status };
      }
    }
    ```

    The returned entry exposes the stored fields directly (`entry.product`) and through `entry.data`. Store `searchText` as a plain string in deployed code; the options object is covered under [Options you may need](#declare-an-index-for-filtered-fields).
  </Step>

  <Step title="Find entries by meaning">
    `Data.search` embeds the query and returns the closest entries as a flat array, best match first, each with a `score` from 0 to 1. `limit` is at most 20 and `scoreThreshold` defaults to 0.6.

    ```ts src/skills/tools/FindKnownIssueTool.ts theme={null}
    import { LuaTool, Data } from 'lua-cli';
    import { z } from 'zod';

    export default class FindKnownIssueTool implements LuaTool {
      name = 'find_known_issue';
      description = "Find known issues that match a customer's description of a problem.";
      inputSchema = z.object({
        problem: z.string().describe("The problem in the customer's own words"),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        const matches = await Data.search('known-issues', input.problem, 5, 0.7);
        return matches.map((match) => ({
          id: match.id,
          product: match.product,
          title: match.title,
          workaround: match.workaround,
          score: match.score,
        }));
      }
    }
    ```

    Raise the threshold when unrelated entries come back, lower it when good ones are missing; 0.7 is a sensible start for support text.
  </Step>

  <Step title="Filter entries exactly">
    `Data.get` takes a [Lua Query](/reference/sdk/query) filter over the stored fields and returns one page as `{ data, pagination }`. Entries here are plain objects, so read fields through `entry.data`. Pages hold at most 100 entries.

    ```ts src/skills/tools/ListOpenIssuesTool.ts theme={null}
    import { LuaTool, Data } from 'lua-cli';
    import { z } from 'zod';

    export default class ListOpenIssuesTool implements LuaTool {
      name = 'list_open_issues';
      description = 'List the unresolved known issues for one product.';
      inputSchema = z.object({
        product: z.string().describe('Product code, for example router-x1'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        const page = await Data.get(
          'known-issues',
          { product: input.product, status: { $ne: 'resolved' } },
          1,
          20
        );
        return {
          total: page.pagination.totalCount,
          issues: page.data.map((entry) => ({ id: entry.id, ...entry.data })),
        };
      }
    }
    ```

    A bare value means equality; `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, and `$exists` are the field operators, and `$and` and `$or` combine clauses at the root.
  </Step>

  <Step title="Update and delete">
    `Data.update` merges the fields you pass into the entry and keeps the rest; pass a replacement string as the fourth argument to re-index it. `Data.delete` removes one entry by id.

    ```ts src/skills/tools/ResolveKnownIssueTool.ts theme={null}
    import { LuaTool, Data } from 'lua-cli';
    import { z } from 'zod';

    export default class ResolveKnownIssueTool implements LuaTool {
      name = 'resolve_known_issue';
      description = 'Mark a known issue as resolved, or remove it when it was recorded by mistake.';
      inputSchema = z.object({
        id: z.string().describe('The entry id returned by add_known_issue'),
        remove: z.boolean().default(false).describe('True to delete the entry instead of resolving it'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        if (input.remove) {
          await Data.delete('known-issues', input.id);
          return { id: input.id, removed: true };
        }
        await Data.update('known-issues', input.id, {
          status: 'resolved',
          resolvedAt: new Date().toISOString(),
        });
        return { id: input.id, status: 'resolved' };
      }
    }
    ```

    A delete is permanent; there is no recycle bin.
  </Step>

  <Step title="Register the tools in a skill">
    The skill's `context` says who may add and resolve issues; only tools in a skill on `LuaAgent.skills` are compiled.

    ```ts src/skills/known-issues.skill.ts theme={null}
    import { LuaSkill } from 'lua-cli';
    import AddKnownIssueTool from './tools/AddKnownIssueTool';
    import FindKnownIssueTool from './tools/FindKnownIssueTool';
    import ListOpenIssuesTool from './tools/ListOpenIssuesTool';
    import ResolveKnownIssueTool from './tools/ResolveKnownIssueTool';

    export default new LuaSkill({
      name: 'known-issues',
      description: 'Known product issues and their workarounds',
      context:
        'When a customer describes a problem, call find_known_issue with their words before answering ' +
        'and offer the workaround. Only support staff add or resolve issues; refer customers to support.',
      tools: [new AddKnownIssueTool(), new FindKnownIssueTool(), new ListOpenIssuesTool(), new ResolveKnownIssueTool()],
    });
    ```

    The agent file below is the quickstart's; if yours differs, add only the highlighted lines to your own `LuaAgent`.

    ```ts src/index.ts highlight={3,8} theme={null}
    import { LuaAgent } from 'lua-cli';
    import weatherSkill from './skills/weather.skill';
    import knownIssuesSkill from './skills/known-issues.skill';

    const agent = new LuaAgent({
      name: 'docs-quickstart',
      persona: 'You are a weather assistant. Answer in one or two sentences.',
      skills: [weatherSkill, knownIssuesSkill],
    });
    ```
  </Step>

  <Step title="Verify">
    Store an entry, then search for it with different words. Both runs hit the real collection.

    ```bash theme={null}
    lua test --ci skill --name add_known_issue --input '{"product":"router-x1","title":"Wi-Fi drops every hour","workaround":"Disable band steering in the admin panel"}'
    lua test --ci skill --name find_known_issue --input '{"problem":"my wifi keeps disconnecting"}'
    ```

    ```text Output theme={null}
    …
    Tool returned: Array[1] of Object — fields: id, product, title, workaround, score
    Output:
    [
      {
        id: '7a112ce1-5c55-4973-b4f9-ec482d21b60c',
        product: 'router-x1',
        title: 'Wi-Fi drops every hour',
        workaround: 'Disable band steering in the admin panel',
        score: 0.734224796295166
      }
    ]
    ```

    The query shares no word with the stored title, and the match still scores above 0.7. Clean up with `resolve_known_issue` and `"remove": true`.
  </Step>

  <Step title="Release">
    `lua push` uploads a version and changes nothing for end users; `lua version create` snapshots the agent; `lua version promote <n>` makes that snapshot live and is also the rollback path ([Release an agent to production](/ship/releasing)).

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "Add known-issues skill"
    lua version promote <n>
    ```

    `lua version create` prints ``✓ Created v<n> (staged). Run `lua version promote v<n>` to deploy.``; `<n>` comes from that line, `promote` accepts `<n>` or `v<n>` and asks no confirmation, and in a script `n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')` reads it.
  </Step>
</Steps>

## Options you may need

### Declare an index for filtered fields

When a collection grows large, a filter on a field without an index slows down and eventually fails with an error naming the field. The third argument of `create()` and fourth of `update()` also accept `{ searchText, index: ['product'] }`; a nested pair such as `[['product', 'status']]` declares a compound index. At most 2 fields per index, 3 declarations per call, and 5 indexes per agent; an index is dropped 14 days after its last declaration or filtered read.

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

### Read one entry by id

`Data.getEntry('known-issues', id)` returns the entry with direct field access and throws when no entry has that id; `entry.save()` writes back fields you assigned on it.

## If it isn't working

<AccordionGroup>
  <Accordion title="search() returns nothing for text that is clearly in the entry">
    Only `searchText` is embedded, not `data`. Include the fields you expect people to search by, and re-index existing entries with `Data.update(collection, id, {}, newSearchText)`.
  </Accordion>

  <Accordion title="entry.title is undefined on get() results">
    `get()` returns plain entries: read `entry.data.title`. `search()`, `create()`, and `getEntry()` return instances with direct access.
  </Accordion>

  <Accordion title="A filter is rejected with a FILTER_* code">
    The filter used an operator or a depth outside the grammar. The codes and limits are on the [Lua Query](/reference/sdk/query#errors) page.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Data reference" href="/reference/sdk/data">Every method, return shape, limit, and error.</Card>
  <Card title="Lua Query" href="/reference/sdk/query">The full filter grammar and its limits.</Card>
  <Card title="Identify users" href="/build/identify-users">One record per end user, and keeping accounts apart.</Card>
  <Card title="Add knowledge" href="/build/add-knowledge">For documents the agent should answer from, use knowledge instead.</Card>
</Columns>
