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

# Automatic search

> One query; the service chooses the search pipeline

`search.automatic` submits one question and lets the service pick the
pipeline — fact, deep, or filtered search — instead of you calling one of
them by name. It returns a `Promise<Job>` whose result wraps the chosen
pipeline's native output. Files must already be
[ingested](/guides/ingest-reconcile).

Automatic search is single-shot: there is no session to continue and no
clarification question back — an ambiguous query is answered under the
service's best reading. Before searching, the service selects fact search for
focused lookups, high-effort deep search for analysis and investigation, or
filtered search for catalog entries. Read the result by its output type; the
selected pipeline can vary between similar queries.

***

## Basic usage

```ts theme={"dark"}
import { NdiClient, UnknownResult } from "ndi-sdk";

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const queued = await client.search.automatic(workspaceId, {
  query: "Which invoices over $500 mention copper?",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
// Returns: succeeded Job; job.result is AutomaticSearchResult

const result = job.result;
if (result && !(result instanceof UnknownResult) && result.result_type === "automatic_search") {
  console.log(result.routing.final, result.output.result_type);
}
```

***

## Method signature

```text theme={"dark"}
automatic(
  workspace_id: string,
  opts: {
    query: string;
    context?: string | null;
    path_prefix?: string | null;
    top_k?: number | null;
    wait_seconds?: number;
    idempotency_key?: string | null;
  },
): Promise<Job>
```

### Parameters

| Parameter              | Type     | Required | Description                                                                                              |
| ---------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `workspace_id`         | `string` | Yes      | Workspace that already holds ingested files                                                              |
| `opts.query`           | `string` | Yes      | The question to answer from the workspace                                                                |
| `opts.context`         | `string` | No       | Extra background the model should treat as given. Does not change access or retrieval scope              |
| `opts.path_prefix`     | `string` | No       | Narrow the search to this workspace subtree                                                              |
| `opts.top_k`           | `number` | No       | Citation-list cap when the fact pipeline answers; the other pipelines size their own output. Must be ≥ 1 |
| `opts.wait_seconds`    | `number` | No       | Hold the submission request open (max 300)                                                               |
| `opts.idempotency_key` | `string` | No       | Override the key the SDK normally mints                                                                  |

### Returns

A `Job`. On success, `job.result` is an `AutomaticSearchResult`
(`result_type: "automatic_search"`), wrapping the chosen pipeline's native,
fully typed result:

| `result.output`           | `output.result_type` |
| ------------------------- | -------------------- |
| `IntelligentSearchResult` | `intelligent_search` |
| `DeepSearchV2Result`      | `deep_search_v2`     |
| `FilteredSearchResult`    | `filtered_search`    |

Narrow the wrapper with `result_type` and `!(result instanceof UnknownResult)`,
then narrow `result.output` with its own `result_type`. Every field on
`output` means exactly what it means on that pipeline's own route.

***

## Result structure

| Field           | Description                                                             |
| --------------- | ----------------------------------------------------------------------- |
| `output`        | The selected pipeline's native result, unchanged                        |
| `output_job_id` | The job that owns the output's receipts and cursors — see below         |
| `routing`       | How the pipeline was chosen (see the routing table)                     |
| `usage`         | Combined LLM usage across every stage; null when any stage is unmetered |

### `routing`

| Field                                                   | Description                                                        |
| ------------------------------------------------------- | ------------------------------------------------------------------ |
| `selected`                                              | The choice of fact, deep, or filtered search made before retrieval |
| `final`                                                 | The pipeline that produced `output`                                |
| `reason`                                                | The router's explanation                                           |
| `router_model`                                          | The model that chose the pipeline                                  |
| `router_wall_ms` / `fact_wall_ms` / `delegated_wall_ms` | Wall time per stage, when that stage ran                           |

The `output` types above are the stable contract. Which pipeline gets selected
for a given query is not: it may vary between similar queries and change
between deployments.

***

## Receipts and pagination: `output_job_id`

The wrapper's own job retains the wrapper. The **output's** durable artifacts
— a deep result's retained SQL results (`res_…` handles), ground crops, and a
filtered result's cursor state — belong to the job named by `output_job_id`.
Use that id wherever a native result's documentation says "this job":

```ts theme={"dark"}
import { NdiClient, UnknownResult } from "ndi-sdk";

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const queued = await client.search.automatic(workspaceId, {
  query: "All invoices over $500, largest first.",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });

const result = job.result;
if (result && !(result instanceof UnknownResult) && result.result_type === "automatic_search") {
  // The output's own job — receipts, retained rows, cursor ownership.
  const outputJob = await client.jobs.get(result.output_job_id);
  console.log(outputJob.kind, outputJob.status);

  const output = result.output;
  if (output.result_type === "filtered_search" && output.next_cursor) {
    // Paging a filtered output does not re-route — see below.
    console.log(output.next_cursor);
  }
}
```

The TypeScript SDK has no `filtered_page` method. Page a filtered output
through the native [filtered-search route](/api-reference/filtered-search)
(`POST .../filtered-search` with `{ "type": "page", "cursor": "…" }`), or use
the Python SDK's `search.filtered_page`. Pages execute without another routing
decision.

***

## Reading the three output shapes

```ts theme={"dark"}
import { NdiClient, UnknownResult } from "ndi-sdk";

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const queued = await client.search.automatic(workspaceId, {
  query: "What were total liabilities at year end?",
  context: "Prefer the audited consolidated statements over drafts.",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });

const result = job.result;
if (result && !(result instanceof UnknownResult) && result.result_type === "automatic_search") {
  const output = result.output;
  if (output.result_type === "intelligent_search") {
    console.log(output.answer, output.interpretation);
  } else if (output.result_type === "deep_search_v2") {
    console.log(output.answer);
    for (const receipt of output.sql_receipts ?? []) {
      console.log(receipt.result_id, receipt.sql);
    }
  } else {
    console.log(output.total_matches, output.exhaustive);
    for (const row of output.rows ?? []) {
      console.log(row.source_path, row.summary);
    }
  }

}
```

***

## Error handling

```ts theme={"dark"}
import { InvalidRequestError, JobFailedError, NdiClient } from "ndi-sdk";

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
try {
  const queued = await client.search.automatic(workspaceId, {
    query: "What were total liabilities at year end?",
  });
  await client.jobs.wait(queued.job_id, { timeout: 600 });
  // Returns: succeeded Job, or throws for request/job failure
} catch (err) {
  if (err instanceof InvalidRequestError) {
    console.log(err.code, err.message);
  } else if (err instanceof JobFailedError) {
    console.log(err.job.status, err.job.error);
  } else {
    throw err;
  }
}
```

| Code                    | When                                           |
| ----------------------- | ---------------------------------------------- |
| `invalid_request` (422) | Blank query, or `top_k` outside its bounds     |
| `invalid_path_prefix`   | `path_prefix` is not a usable workspace prefix |
| `workspace_not_found`   | Workspace id is unknown                        |

***

## Best practices

<CardGroup cols={2}>
  <Card title="Read the output by its type" icon="shapes">
    Branch on `output.result_type` — the selected pipeline may vary between
    similar queries and between deployments.
  </Card>

  <Card title="Use output_job_id for artifacts" icon="receipt">
    Retained rows, receipts, and cursors belong to the output's job, not the
    wrapper's.
  </Card>

  <Card title="Ask an answerable question" icon="message-circle">
    There is no clarification back. Put the disambiguation into `query` or
    `context` up front.
  </Card>

  <Card title="Pick a pipeline yourself when you know" icon="route">
    If you already know you want an agent investigation, call `search.deep`
    directly and skip the router.
  </Card>
</CardGroup>

***

## Related search methods

| Need                                          | Method                                                         |
| --------------------------------------------- | -------------------------------------------------------------- |
| One-pass lookup job                           | `client.search.fact`                                           |
| Agent investigation, sessions, clarifications | `client.search.deep`                                           |
| Filter/rank/count the catalog                 | `POST .../filtered-search` (not wrapped in the TypeScript SDK) |

See [Search methods](/concepts/search-methods) and the [Search guide](/guides/search).

***

## Next steps

<CardGroup cols={2}>
  <Card title="Deep search" icon="search" href="/sdks/typescript/search">
    The agent loop, sessions, and clarifications — when you choose it yourself.
  </Card>

  <Card title="Job management" icon="clock" href="/sdks/typescript/job-management">
    Wait, stream events, and cancel long-running jobs.
  </Card>
</CardGroup>
