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

# Deep search

> Ask a multi-step question over an ingested workspace

`search.deep` runs an agent over a workspace and returns a `Promise<Job>`.
The agent picks its own tools, authors evidence, and can take minutes. Files
must already be [ingested](/guides/ingest-reconcile).

<Warning>
  Deep search is not chat. Each call is one question. Continue a thread only
  with that turn's `session_id`. After the search result, poll every id in
  `result.grounding_job_ids` — do not call [Ground](/sdks/typescript/ground)
  for those evidences.
</Warning>

There is no `tier` argument. `effort` is the quality and cost knob, here and on
`search.jeTesting`. Filtered search is not wrapped in the TypeScript SDK.

***

## 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.deep(workspaceId, {
  query: "Which subsidiaries missed covenant tests?",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
// Returns: succeeded Job; job.result is DeepSearchV2Result or IntelligentSearchResult

const result = job.result;
if (
  result &&
  !(result instanceof UnknownResult) &&
  (result.result_type === "deep_search_v2" || result.result_type === "intelligent_search")
) {
  console.log(result.answer, result.session_id);
}
```

***

## Method signature

```text theme={"dark"}
deep(
  workspace_id: string,
  opts: {
    query: string;
    context?: string | null;
    effort?: "low" | "medium" | "high";
    allow_clarification?: boolean;
    path_prefix?: string | null;
    paths?: string[] | null;
    session_id?: string | null;
    surface?: "deep_search" | "qa" | "workbook";
    use_kg?: boolean;
    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 for this turn                                                                                                                               |
| `opts.context`             | `string`                                | No       | Extra background the agent should treat as given                                                                                                         |
| `opts.effort`              | `"low"` / `"medium"` / `"high"`         | No       | Quality and cost. Server default when omitted is `low`                                                                                                   |
| `opts.allow_clarification` | `boolean`                               | No       | Let a genuinely ambiguous query return one question instead of an answer. Default `false`                                                                |
| `opts.path_prefix`         | `string`                                | No       | Limit tools to files under this prefix                                                                                                                   |
| `opts.paths`               | `string[]`                              | No       | Limit tools to these uploaded paths (max 64). Intersects with `path_prefix`                                                                              |
| `opts.session_id`          | `string`                                | No       | Continue an earlier turn's thread                                                                                                                        |
| `opts.surface`             | `"deep_search"` / `"qa"` / `"workbook"` | No       | Console history bucket. Omit for the public default. Does not pick the agent                                                                             |
| `opts.use_kg`              | `boolean`                               | No       | Let an unscoped run consult the workspace's knowledge graph. Server default when omitted is on; `false` makes the agent work from files and search alone |
| `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`. Which result you get depends on scope:

| Scope                           | `result_type`        |
| ------------------------------- | -------------------- |
| No `paths` and no `path_prefix` | `deep_search_v2`     |
| `paths` and/or `path_prefix`    | `intelligent_search` |

There is no exported `isDeepSearchV2Result` helper. Narrow with `result_type`
and `!(result instanceof UnknownResult)`. A follow-up stays on the thread's
original implementation even if this call adds or drops scope.

***

## Effort

`effort` maps to thinking budget. The job echoes the level as `job.effort`.

| Value           | When to use                                                  |
| --------------- | ------------------------------------------------------------ |
| `low` (default) | Routine questions; typically tens of seconds                 |
| `medium`        | Multi-document questions. On a scoped run this acts as `low` |
| `high`          | Analytical questions that justify extra wall clock           |

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const queued = await client.search.deep(workspaceId, {
  query: "Reconcile year-end cash to the bank statements.",
  effort: "high",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
// Returns: succeeded Job; job.effort echoes the requested level
console.log(job.effort);
```

***

## Scope files

Omit both `paths` and `path_prefix` for a corpus-wide, source-level run.

Pass `paths` to pin every tool to those files (unknown or inaccessible paths
are refused before a job is created). Combine with `path_prefix` as an
intersection.

```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.deep(workspaceId, {
  query: "What is the closing cash balance?",
  path_prefix: "reports/",
  paths: ["reports/cash-flow.pdf", "reports/notes.pdf"],
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
// Returns: succeeded Job; scoped runs land intelligent_search

const result = job.result;
if (result?.result_type === "intelligent_search" && !(result instanceof UnknownResult)) {
  console.log(result.interpretation, result.answer);
}
```

***

## Follow-up threads

Pass the previous result's `session_id`. Only the API key that started the
thread may continue it. A thread whose turn is still running returns `409`
`session_busy`.

Do not reuse one session across unrelated questions.

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";

const first = await client.jobs.wait(
  (
    await client.search.deep(workspaceId, {
      query: "Which subsidiaries missed covenant tests?",
      wait_seconds: 30,
    })
  ).job_id,
  { timeout: 600 },
);
// Returns: succeeded Job; read session_id from the result

const firstResult = first.result;
const sessionId =
  firstResult &&
  !(firstResult instanceof UnknownResult) &&
  (firstResult.result_type === "deep_search_v2" ||
    firstResult.result_type === "intelligent_search")
    ? firstResult.session_id
    : undefined;

if (sessionId) {
  const follow = await client.jobs.wait(
    (
      await client.search.deep(workspaceId, {
        query: "Show the supporting numbers for the first one.",
        session_id: sessionId,
        wait_seconds: 30,
      })
    ).job_id,
    { timeout: 600 },
  );
  // Returns: succeeded Job in the same thread
  const followResult = follow.result;
  if (
    followResult &&
    !(followResult instanceof UnknownResult) &&
    (followResult.result_type === "deep_search_v2" ||
      followResult.result_type === "intelligent_search")
  ) {
    console.log(followResult.answer);
  }
}
```

***

## Clarifications

With `allow_clarification: true`, a genuinely ambiguous query can finish with
`clarification` set, `answer` null, and empty evidences. Reply on the same
`session_id`.

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const job = await client.jobs.wait(
  (
    await client.search.deep(workspaceId, {
      query: "How did we do?",
      allow_clarification: true,
      wait_seconds: 30,
    })
  ).job_id,
  { timeout: 600 },
);

const result = job.result;
if (
  result &&
  !(result instanceof UnknownResult) &&
  (result.result_type === "deep_search_v2" || result.result_type === "intelligent_search") &&
  result.clarification
) {
  console.log(result.clarification);
  // Re-ask on result.session_id
}
```

***

## Context and surface

`context` is caller-supplied background, not a hidden system prompt replacement.

`surface` only groups console history (`qa`, `workbook`). Omit it from SDK
integrations. Follow-ups inherit the thread head.

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const queued = await client.search.deep(workspaceId, {
  query: "What is the stated reporting currency?",
  context: "Prefer the audited consolidated statements over drafts.",
  wait_seconds: 30,
});
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });
// Returns: succeeded Job
console.log(job.job_id);
```

***

## Result structure

`exhausted: true` means the step budget ran out. The job still succeeded; the
answer may rest on less evidence. That is not `JobFailedError`.

### Unscoped: `deep_search_v2`

| Field               | Description                                                                                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `answer`            | Direct answer. Null on clarification or if the run exhausted before authoring one                                                                                              |
| `reasoning`         | How receipted statements chain into the answer                                                                                                                                 |
| `evidences`         | Quotes with source path, page, and relevance                                                                                                                                   |
| `sql_receipts`      | Statements the run executed; `rows` are a bounded preview                                                                                                                      |
| `figures`           | Criteria tested, each pointing at receipt `result_id`s                                                                                                                         |
| `claims`            | Machine-checkable claims that cite those receipts                                                                                                                              |
| `coverage`          | Per-deliverable status (`answered` / `partial` / `unanswerable`)                                                                                                               |
| `discrepancies`     | Empty list is an answer, not an omission                                                                                                                                       |
| `session_id`        | Pass this on the next turn                                                                                                                                                     |
| `clarification`     | Set only when `allow_clarification` rejected the query                                                                                                                         |
| `degraded`          | Run broke after producing working papers; `sql_receipts` may still be useful                                                                                                   |
| `grounding_job_ids` | Ground jobs planned for this answer's evidences. Poll each with `jobs.wait` / `GET /v1/jobs/{id}` after this result. Empty when postprocessing is off or nothing is groundable |

### Scoped: `intelligent_search`

| Field               | Description                                                                                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `answer`            | Direct answer                                                                                                                                                                  |
| `interpretation`    | How the agent read the query                                                                                                                                                   |
| `reasoning`         | Evidence-authoring notes                                                                                                                                                       |
| `evidences`         | Quotes with source path and page                                                                                                                                               |
| `coverage`          | Access-label restrictions (`restricted_candidates`, `labels_required`)                                                                                                         |
| `session_id`        | Pass this on the next turn                                                                                                                                                     |
| `clarification`     | Set only when `allow_clarification` rejected the query                                                                                                                         |
| `exhausted`         | Step budget ran out                                                                                                                                                            |
| `grounding_job_ids` | Ground jobs planned for this answer's evidences. Poll each with `jobs.wait` / `GET /v1/jobs/{id}` after this result. Empty when postprocessing is off or nothing is groundable |

Search does not wait for quote locations. After the result, poll every id in
`grounding_job_ids`. An empty list means nothing was planned. A missing field
is a job from before this field existed — only then call
`client.documents.ground`.

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
const job = await client.jobs.wait(
  (
    await client.search.deep(workspaceId, {
      query: "What were total liabilities at year end?",
      wait_seconds: 30,
    })
  ).job_id,
  { timeout: 600 },
);
const result = job.result;
if (
  result &&
  !(result instanceof UnknownResult) &&
  (result.result_type === "deep_search_v2" || result.result_type === "intelligent_search")
) {
  for (const groundId of result.grounding_job_ids ?? []) {
    const grounded = await client.jobs.wait(groundId, { timeout: 300 });
    console.log(grounded.result);
  }
}
```

***

## Complete example

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

const client = new NdiClient();
const workspace = await client.workspaces.create({ name: "fy25-audit" });
const uploaded = await client.uploadAndIngest(workspace.workspace_id, "report.pdf", {
  path: "reports/report.pdf",
});
await client.jobs.wait(uploaded.ingestion_job.job_id, { timeout: 600 });

const first = await client.jobs.wait(
  (
    await client.search.deep(workspace.workspace_id, {
      query: "What were total liabilities at year end?",
      effort: "medium",
      wait_seconds: 30,
    })
  ).job_id,
  { timeout: 600 },
);

const result = first.result;
if (result?.result_type === "deep_search_v2" && !(result instanceof UnknownResult)) {
  console.log(result.answer);
  for (const receipt of result.sql_receipts ?? []) {
    console.log(receipt.result_id, receipt.sql, receipt.truncated);
  }
} else if (result?.result_type === "intelligent_search" && !(result instanceof UnknownResult)) {
  console.log(result.interpretation, result.answer);
}

const sessionId =
  result &&
  !(result instanceof UnknownResult) &&
  (result.result_type === "deep_search_v2" || result.result_type === "intelligent_search")
    ? result.session_id
    : undefined;

if (sessionId) {
  const follow = await client.jobs.wait(
    (
      await client.search.deep(workspace.workspace_id, {
        query: "Break that down by current vs non-current.",
        session_id: sessionId,
        wait_seconds: 30,
      })
    ).job_id,
    { timeout: 600 },
  );
  // Returns: succeeded Job in the same thread
  const followResult = follow.result;
  if (
    followResult &&
    !(followResult instanceof UnknownResult) &&
    (followResult.result_type === "deep_search_v2" ||
      followResult.result_type === "intelligent_search")
  ) {
    console.log(followResult.answer);
  }
}
```

***

## Error handling

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

const client = new NdiClient();
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";
try {
  const queued = await client.search.deep(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 ConflictError) {
    console.log(err.code, err.message); // session_busy
  } else 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                                              |
| ------------------------- | ------------------------------------------------- |
| `session_busy` (409)      | That thread still has a running turn              |
| `session_not_found` (404) | `session_id` is unknown or belongs to another key |
| `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="Ingest first" icon="folder">
    Search reads derived representations. Upload plus ingest, then ask.
  </Card>

  <Card title="One question per turn" icon="message-circle">
    Start a new session for a new investigation. Follow-ups belong on that
    turn's `session_id`.
  </Card>

  <Card title="Choose effort on purpose" icon="gauge">
    Leave `effort` unset for lookups. Use `high` when the extra minutes are
    worth it.
  </Card>

  <Card title="Read exhaustion" icon="clock">
    `exhausted: true` is a successful, budget-capped answer — not a failed job.
  </Card>
</CardGroup>

***

## Related search methods

| Need                                          | Method                                          |
| --------------------------------------------- | ----------------------------------------------- |
| Ranked snippets, inline                       | `client.tools.hybridSearch`                     |
| One-pass lookup job                           | `client.search.fact`                            |
| Filter/rank/count the catalog                 | HTTP `POST /v1/workspaces/{id}/filtered-search` |
| Test a ledger package, with the SQL published | `client.search.jeTesting`                       |

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

***

## Next steps

<CardGroup cols={2}>
  <Card title="Workspaces" icon="folder" href="/sdks/typescript/workspaces">
    Create a corpus, upload, and ingest before searching.
  </Card>

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

  <Card title="Journal-entry testing" icon="clipboard-check" href="/sdks/typescript/je-testing">
    Test a ledger package and read the SQL behind each figure.
  </Card>
</CardGroup>
