# Fuzzy search

Fuzzy-search a local list with agent.searchInList() to resolve loose user phrasing to a concrete record before executing an action.

`searchInList()` fuzzy-matches a query against a local array, with no network call. Use it inside action handlers to resolve a user's loose phrasing ("the acme account", "jon from billing") to a concrete record before you build action parameters or feed data to a [Task Agent](/reference/task-agent-api/).

:::note[Prerequisites]
The [SDK is installed and initialized](/start/install/). Call `searchInList()` on an agent instance, typically inside an action's `execute` handler.
:::

## Why fuzzy matching

In a conversational interface, users refer to records the way they remember them, not the way your database stores them. They type "acme" when the account is named "ACME Corporation Ltd", "jon smth" when the user is "Jon Smith", and they misspell both. An exact or substring lookup returns nothing for any of these, which forces the agent to come back with "no results" for a record that plainly exists.

Fuzzy matching closes that gap: it compares the query approximately, tolerating typos, partial words, and missing pieces. Each candidate gets a relevance `score` between `0` (perfect match) and `1` (barely related), and `searchInList()` drops anything weaker than `0.4` and returns the rest sorted best-first. Your handler then decides what to do with the confidence it has: auto-select a clear winner, or return the top candidates so the agent can ask the user to pick.

## Signature

```typescript
searchInList<T>(
  items: T[],
  query: string,
  options: { searchKeys: string[] },
): { item: T; refIndex: number; score?: number }[];
```

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `items` | `T[]` | The list to search. Objects or primitives. |
| `query` | `string` | The search string, usually a user-supplied name or term. |
| `options.searchKeys` | `string[]` | Property paths to match against each item, for example `["name", "id"]`. |

## Return value

An array of matches, sorted most-relevant first:

| Key | Type | Description |
| :--- | :--- | :--- |
| `item` | `T` | The matched element from `items`. |
| `refIndex` | `number` | Its index in the original `items` array. |
| `score` | `number` | Relevance. `0` is a perfect match; lower is better. Present when a query is given. |

Edge cases:

- An empty or whitespace-only `query` returns every item in original order, with no `score`.
- Empty `items` or empty `searchKeys` returns `[]`.
- Matches weaker than the built-in `0.4` threshold are dropped.

## Resolve an account

Match against every field a user might reference: name, domain, or ID in one call. Here "acme" resolves to the ACME Corporation record even though the user never typed the full name:

```javascript title="lookup-account.js"
foldspace("when", "ready", () => {
  const agent = foldspace.agent({ /* …common setup… */ });

  const accounts = [
    { id: "acc-001", name: "ACME Corporation Ltd", domain: "acme.com", plan: "Enterprise" },
    { id: "acc-002", name: "Acmar Logistics", domain: "acmar.io", plan: "Pro" },
    { id: "acc-003", name: "Northwind Traders", domain: "northwind.com", plan: "Pro" },
  ];

  agent.addActionHandlers({
    lookup_account: {
      execute: async (params) => {
        const matches = agent
          .searchInList(accounts, params.query, { searchKeys: ["name", "domain", "id"] })
          .filter((r) => r.score < 0.4);

        if (matches.length === 0) {
          return { found: false, message: "No matching account." };
        }

        // A single confident hit: hand the agent the resolved record.
        if (matches.length === 1 || matches[0].score < 0.2) {
          return { found: true, account: matches[0].item };
        }

        // Ambiguous: return the candidates so the agent asks the user to pick.
        return { found: true, candidates: matches.map((r) => r.item) };
      },
    },
  });
});
```

## Resolve a user

The same pattern resolves people. "jon smth" matches "Jon Smith" despite the missing letters, and searching `email` too lets "jon@" or a partial address work:

```javascript
const matches = agent
  .searchInList(users, params.query, { searchKeys: ["name", "email"] })
  .filter((r) => r.score < 0.4)
  .map((r) => r.item);

const user = matches[0]; // best match, or undefined if nothing cleared the bar
```

## Try exact matches first

Cheap exact or substring checks first, fuzzy `searchInList()` as the fallback. It is faster and avoids surprising matches when the user pasted an exact ID or email:

```javascript
const exact = users.find(
  (u) => u.email === query || u.name === query,
);

const match =
  exact ??
  agent
    ?.searchInList?.(users, query, { searchKeys: ["name", "email"] })
    .filter((r) => r.score < 0.4)[0]?.item;
```

Optional-chain the call (`agent?.searchInList?.(...)`) when the agent may not be ready in the calling context.

## Pick a score cutoff

The score is your confidence dial. Two thresholds cover most actions:

- `< 0.4`: "plausible". Use it to build a candidate list and let the agent confirm with the user.
- `< 0.2`: "confident". Use it to auto-select a single record without a confirmation round-trip, for example when "Q1 creators" should silently resolve to the "Q1 Creators 2026" list.

:::tip
Prefer the looser cutoff plus a confirmation step for destructive or high-stakes actions (deleting, billing, messaging someone). Save auto-select for lookups where a near-miss is cheap to undo.
:::

## Related

- [Task Agent API](/reference/task-agent-api/): feed resolved records into a background task.
- [Execute actions](/guides/executing-actions/): write the `execute` handler this API is called from.
- [SDK APIs](/reference/sdk-apis/): the full programmatic surface of the agent instance.
