intelligent-search

deep_search

Run an agentic search over a workspace. Cost class: job.

The caller receives a 202 with a queued :class:~ndi_service.app.platform_api.results.Job immediately, and polls GET /jobs/{id} for the result once the job succeeds.

Every run uses one configuration: a reasoning engine with a thinking budget, the internal CLI tool surface, and the workspace's knowledge graph when one is built (the 2026-09 five-lane evaluation picked it as the most accurate lane).

One implementation serves every scope: the source-level agent (deep_search_v2). It browses, reads, and computes over the uploaded files exactly as an API caller would, writes its own SQL over the tables in scope, and every executed statement lands on the result as a receipt (sql_receipts) — a figure and the query behind it travel together. (Jobs created before the 2026-09 consolidation may carry the retired classic loop's intelligent_search kind; they stay readable via GET /jobs.)

Scope never changes the configuration: paths scopes the run to exactly those uploaded files — every tool the agent runs (search, listings, reads, ask-file, SQL, the shell's ndi commands) sees only them and the content ingested from them, enforced server-side against the stored job. Each entry must name a visible uploaded source file (file_not_found / access_denied otherwise, before any job is created). With path_prefix the two add up: the run covers the folder's files plus the named ones.

Pass the result's session_id to ask a follow-up. With the same folder/file scope and access grants, the agent resumes that transcript. Changing either starts a fresh conversation and returns its new session_id, so prior tool results cannot cross the new scope boundary. Older transcripts without a scope snapshot also restart. Classic thread ids return session_not_found; retrying the original request with its idempotency key still returns that historical job.

allow_clarification lets the agent ask instead of guessing on a genuinely ambiguous query: the succeeded result then carries the questions in clarification_questions (and as text in clarification) with answer null. Answer them in the same thread — send the result's session_id with the same query and clarification_answers, or with any refined query. Off (the default) the agent never asks back.

Retrieval only: this job never grounds. To pin a quote to a region of its source document, call document-operations ground on the file the citation names with its quote as the target text — separately, and only for the citations you actually need a locator for.

exhausted: true means the step budget ran out before the agent finished — a successful, partial response, not a failure.

Args: body: Search parameters: query, context, allow_clarification, clarification_answers, path_prefix, paths, session_id. context: Resolved workspace plus the caller's access predicate. session: Database session. wait_seconds: Bounded inline wait (0 → immediate 202). idempotency_key: Replaying with the same key returns the original job. response: Injected by FastAPI for status-code management. temporal: Temporal client used to dispatch the workflow. search_model: Internal engine pin; overrides the configuration's engine, never its tooling. search_playbook: Internal procedure pin; retired with the classic loop — refused. report_usage: Internal usage-accounting switch. postprocessing: Internal optional-postprocessing mode; none skips the detached presentation/grounding sidecars after commit.

Returns: A queued :class:~ndi_service.app.platform_api.results.Job. On success the result is a :class:~ndi_service.app.platform_api.results.DeepSearchV2Result, naming itself in result_type.

post/v1/workspaces/{workspace_id}/deep-search

Request

Path parameters

workspace_idstring uuid required

Workspace identifier.

Query parameters

wait_secondsinteger

Bounded inline wait. 0 (the default) returns 202 immediately. Above 0 the route returns 200 with a terminal job if the work finished in time, and 202 with the still-running job if it did not. Either way, read status.

Headers

Idempotency-Keystring nullable

Replaying a request with the same key returns the original job with 200, never a second job. The key is kept with the job for replay and never cleared, so it must not contain personal data.

Request body

querystring required

What to answer, in the caller's own words. Stripped before its length is checked; a blank query is refused rather than run.

contextstring nullable

Free-form background for the model — the engagement, the entity, what the caller already knows. It informs the answer; it does not widen or narrow which files the caller may see.

path_prefixstring nullable

Put every uploaded file under one folder subtree, workspace-relative, in the run's scope; paths, where the body takes it, adds more files. Omit both for the whole workspace.

allow_clarificationboolean

Let the agent finish the run with clarifying questions instead of an answer, when the query is genuinely ambiguous and the ambiguity materially changes the answer. The result then carries the questions in clarification_questions (and as text in clarification), with answer and reasoning null and evidences empty; answer them in the same thread via the result's session_id, with clarification_answers or a follow-up query. Off, the default, means the agent always answers with its best reading, stated in the answer.

effort'high' | 'medium' | 'low' nullable

Deprecated and ignored: the service resolves the run's configuration itself. Accepted so clients released before the effort levels were retired keep working; omit it.

use_kgboolean nullable

Deprecated and ignored: the service decides whether the knowledge graph is consulted. Accepted so clients released before it was retired keep working; omit it.

include_answerboolean nullable

Deprecated and ignored: every run answers. Accepted so clients released before it was retired keep working; omit it.

pathsstring[] nullable

Scope the run to exactly these uploaded files. Every retrieval tool the agent runs — search, listings, reads, table queries, the shell — sees only these files and the content ingested from them; everything else is invisible to the run. Each entry names an uploaded source file (never a folder — scope folders with path_prefix; the two add up, so the run covers the folder's files plus these). No limit on how many. Unknown or inaccessible paths are refused up front.

session_idstring uuid nullable

Continue a prior deep-search conversation: the id echoed on an earlier result's session_id. With the same folder/file scope and access grants, the agent resumes that thread's context. A changed scope or grants starts a fresh conversation and returns a new session_id; older threads without a scope snapshot also restart. Omit to start a new thread. Only the API key that started the thread may continue it; a thread whose turn is still running returns 409 session_busy.

surface'deep_search' | 'qa' | 'workbook'

Which console history this run belongs to. It does not pick the agent, the chassis, or the engine: an omitted value is deep_search (the public API and the Deep Search page). The NDI console sends qa or workbook so Activity and thread lists can split those skins. A follow-up (session_id) inherits the thread head's surface; a conflicting value on the follow-up body is ignored.

output_schemaobject nullable

A JSON Schema (Draft 2020-12, object root, local non-recursive $ref only, no pattern, patternProperties or unevaluated*) the answer must follow. When set, the agent writes answer as JSON text conforming to it, the run checks that and asks the agent to correct a mismatch, and the result carries the parsed value in structured_answer. A field the source does not state comes back null, whatever type the schema gives it. Explanation goes to reasoning where the result has one, and evidences are unchanged. If the answer still misses the schema, the run succeeds with answer as written and structured_answer null. Omit for a prose answer.

Response

Successful Response

job_idstring uuid required

Server-minted id. Poll GET /v1/jobs/{job_id} or pass wait_seconds on create.

kind'intelligent_search' | 'deep_search_v2' required

deep_search_v2, the source-level agent — every new run, scoped or not. intelligent_search appears only on deep-search jobs created before the 2026-09 consolidation retired the classic scoped loop; those stay readable here.

status'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' required

cancelled is a first-class terminal status, not a failed variant.

Conflating them makes error-rate metrics lie and forces every client to string-match an error code.

namestring nullable

Display name (the console's 'project'). Public Parse, Split, Classify, Extract, and Ground runs supply a stable server default when blank; citation-generated Ground jobs are excluded. mutable via PATCH /v1/jobs/{job_id}.

project_idstring uuid nullable

Caller-minted grouping id shared by jobs submitted together. Filterable on GET /v1/jobs.

workspace_idstring uuid nullable

The workspace this job reads. Null for a document operation whose source is an upload, a URL, or a prior parse result.

idempotency_keystring nullable

Key that was replayed to return this job. Absent when the create call had no Idempotency-Key.

forceboolean nullable

The request's own force flag, echoed regardless of outcome. Present for kinds whose request carries one (e.g. kg_build, where a true value cleared the durable store before the job ran) so a caller can tell that apart from a build that failed leaving the prior store untouched — result alone cannot, since it is absent on failure either way.

querystring nullable

The question this job was asked, for the search kinds that carry one (intelligent_search, qa_file, qa_tables, filtered_search); null for every other kind. Read from the stored request, so it rides the same retention path as result: null once result_state leaves available. Narrow by design — the frozen access context never reaches the wire. File scope is summarised separately as query_scope.

effortstring nullable

How hard this run worked, for the search kinds that carry it. A deep-search job (kind deep_search_v2) created before the effort knob was removed echoes its requested level: 'low', 'medium', or 'high'; newer deep-search jobs, which all run one configuration, read null. An intelligent_search row carries an internal selector instead: 'fast' is fact-search, 'automatic' is automatic-search, and 'deep' (or the legacy 'balanced') is a deep-search or console QA chat job created before the 2026-09 consolidation — those two are not distinguishable by this field. Null for every other kind. Read from the stored request, so it rides the same retention path as query and result: null once result_state leaves available.

surface'deep_search' | 'qa' | 'workbook' nullable

Which console history this deep-search job belongs to. deep_search is the public route and the Deep Search page; qa and workbook are the console chat skins over the same agent. It does not pick the implementation. Null for fact-search and every non-deep-search kind. Jobs stored before this field existed echo deep_search. Read from the stored payload, so it rides the same retention path as query.

result_state'available' | 'expired' | 'not_retained' | 'cancelled' | 'storage_unavailable'

Why Job.result is null.

available with a null result means the job has not succeeded yet; expired means its content was removed; not_retained means the client is provisioned zero_retention and already had its one chance to read it; cancelled means the job, or the root job it ran for, was cancelled. A cancelled job serves no results (it is not charged, including delivered parts) — not its own progressive values, and not a delegated child's result. It is a read state only, derived on the wire; no row stores it. storage_unavailable withholds a retained result while its workspace binding refuses access; Job.result_error explains the current refusal. A late poll returns the job with a state, never a 404 — "this expired" and "this never existed" are a scheduling problem and a bug respectively, and a 404 makes them identical.

unitsinteger

Measured work in legacy credit units, retained even when a failed or cancelled job is not billed. Kept for compatibility: credits is the authoritative billed amount.

creditsinteger nullable

Final credits billed for this job, priced once under credit_schedule_version. Null until usage_final is true. Never changes afterwards, including after payload expiry or deletion. Failed and cancelled-before-completion jobs finalize with 0 credits, including delivered parts; ones finalized before that policy keep their credits. Always null on a stage job that Automatic Search delegated: its work is billed on the parent job.

usage_finalboolean

True once credits is final and will not change. False while the job is queued or running, and until any cleanup of its delegated stages has been accounted for. A failed tool job (read_file, run_sql, hybrid_search, grep, file_metadata, folder_metadata) created with an Idempotency-Key stays false for one hour after it fails, because a retry with the same key re-runs it; after that hour it is final and the retry returns the stored error.

credit_schedule_versioninteger nullable

Version of the credit schedule credits were priced under. Null until usage_final is true.

usage_finalized_atstring date-time nullable

When credits became final. Null until usage_final is true.

created_atstring date-time required

When the job row was created.

started_atstring date-time nullable

When execution began. Null while still queued.

finished_atstring date-time nullable

When the job reached a terminal status.

payload_expires_atstring date-time nullable

Cleanup deadline after explicit deletion. Null for retained content; Zero retention uses its delivery window.

deleted_atstring date-time nullable

When the caller hid this run from listings, via delete_job. A soft delete: the job still reads by id and still counts toward GET /v1/usage — what it stops doing is appearing in GET /v1/jobs.

Changes

No recorded changes to this operation across all 1 revision of this API.