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

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import AutomaticSearchResult

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

    result = job.result
    if isinstance(result, AutomaticSearchResult):
        print(result.routing.final, result.output.result_type)
```

***

## Method signature

```text theme={"dark"}
def automatic(
    workspace_id: UUID | str,
    *,
    query: str,
    context: str | None = None,
    path_prefix: str | None = None,
    top_k: int | None = None,
    wait_seconds: int = 0,
    idempotency_key: str | None = None,
) -> Job
```

### Parameters

| Parameter         | Type            | Required | Description                                                                                              |
| ----------------- | --------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `workspace_id`    | `UUID` or `str` | Yes      | Workspace that already holds ingested files                                                              |
| `query`           | `str`           | Yes      | The question to answer from the workspace                                                                |
| `context`         | `str`           | No       | Extra background the model should treat as given. Does not change access or retrieval scope              |
| `path_prefix`     | `str`           | No       | Narrow the search to this workspace subtree                                                              |
| `top_k`           | `int`           | No       | Citation-list cap when the fact pipeline answers; the other pipelines size their own output. Must be ≥ 1 |
| `wait_seconds`    | `int`           | No       | Hold the submission request open (max 300)                                                               |
| `idempotency_key` | `str`           | 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
result:

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

Every field on `output` means exactly what it means on that pipeline's own
route — citations, completeness signals, and pagination included.

***

## 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; `None` 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":

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import AutomaticSearchResult, FilteredSearchResult

with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    job = client.jobs.wait(
        client.search.automatic(
            workspace_id,
            query="All invoices over $500, largest first.",
            wait_seconds=30,
        ).job_id,
        timeout=600,
    )
    result = job.result
    if isinstance(result, AutomaticSearchResult):
        # The output's own job — receipts, retained rows, cursor ownership.
        output_job = client.jobs.get(result.output_job_id)
        print(output_job.kind, output_job.status)

        # A filtered output pages through filtered-search, no re-routing:
        output = result.output
        if isinstance(output, FilteredSearchResult) and output.next_cursor:
            next_page = client.search.filtered_page(
                workspace_id,
                cursor=output.next_cursor,
                wait_seconds=30,
            )
            print(next_page.job_id)
```

***

## Reading the three output shapes

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models.jobs import (
    AutomaticSearchResult,
    DeepSearchV2Result,
    FilteredSearchResult,
    IntelligentSearchResult,
)

with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    job = client.jobs.wait(
        client.search.automatic(
            workspace_id,
            query="What were total liabilities at year end?",
            context="Prefer the audited consolidated statements over drafts.",
            wait_seconds=30,
        ).job_id,
        timeout=600,
    )
    result = job.result
    if isinstance(result, AutomaticSearchResult):
        output = result.output
        if isinstance(output, IntelligentSearchResult):
            print(output.answer, output.interpretation)
        elif isinstance(output, DeepSearchV2Result):
            print(output.answer)
            for receipt in output.sql_receipts:
                print(receipt.result_id, receipt.sql)
        elif isinstance(output, FilteredSearchResult):
            print(output.total_matches, output.exhaustive)
            for row in output.rows:
                print(row.source_path, row.summary)

```

***

## Async client

```python theme={"dark"}
import asyncio

from ndi_sdk import AsyncNdiClient
from ndi_sdk.models.jobs import AutomaticSearchResult


async def main() -> None:
    async with AsyncNdiClient() as client:
        workspace_id = "550e8400-e29b-41d4-a716-446655440001"
        queued = await client.search.automatic(
            workspace_id,
            query="What is the reporting period?",
            wait_seconds=30,
        )
        job = await client.jobs.wait(queued.job_id, timeout=600)
        result = job.result
        if isinstance(result, AutomaticSearchResult):
            print(result.routing.final)


asyncio.run(main())
```

***

## Error handling

```python theme={"dark"}
from ndi_sdk import InvalidRequestError, JobFailedError, NdiClient

with NdiClient() as client:
    workspace_id = "550e8400-e29b-41d4-a716-446655440001"
    try:
        queued = client.search.automatic(
            workspace_id,
            query="What were total liabilities at year end?",
        )
        client.jobs.wait(queued.job_id, timeout=600)
        # Returns: succeeded Job, or raises for request/job failure
    except InvalidRequestError as exc:
        print(exc.code, exc.message)
    except JobFailedError as exc:
        print(exc.job.status, exc.job.error)
```

| 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` (or `isinstance`) — 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 a census or an agent investigation, call
    `search.filtered` or `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                 | `client.search.filtered` |

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/python/search">
    The agent loop, sessions, and clarifications — when you choose it yourself.
  </Card>

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