Workspaces
Search
client.search answers questions over the ingested files of a workspace, and cites the files it read. Each call creates a job: read its result with jobs.wait. A file that isn't ingested isn't searched. Automatic searches are limited to 60 per account every 10 minutes; past that, a call raises RateLimitError (rate_limited) once its retries are spent. See Search for what the answers hold.
Automatic search
client.search.automatic(workspaceId, params, options?) returns APIPromise<DocumentJob>. Its result has result_type: "automatic_search", the routing it took, and output: a fact answer or an agent answer. One question, one answer, with no follow-ups. See Automatic search.
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId | string | Yes | The workspace. |
params.query | string | Yes | The question, up to 4,096 characters. |
params.context | string | No | Background the answer takes as given, up to 20,000 characters. Default: none. |
params.path_prefix | string | No | Search only the files under this folder. Default: the whole workspace. |
params.paths | string[] | No | Search exactly these files, by path, as well as any under path_prefix. |
params.top_k | number | No | How many evidences a fact answer cites, 1 to 50. Default: 6. |
params.mode | "auto" | "fact" | "agent" | No | "auto" (default) lets the document service choose; "fact" and "agent" skip the routing step. |
params.output_schema | Record<string, unknown> | No | A JSON Schema the answer must follow; the answer's structured_answer holds it. |
params.wait_seconds | number | No | 0 to 60 (MAX_WAIT_SECONDS). |
params.idempotency_key | string | No | Up to 200 characters. Minted per call when omitted. |
params is an AutomaticSearchParams.
const job = await client.search.automatic(workspaceId, {
query: "What were total liabilities at year end?",
path_prefix: "reports/2025",
wait_seconds: 60,
});
const done = await client.jobs.wait(job.job_id);QA
client.search.qa(workspaceId, params, options?) returns APIPromise<DocumentJob>. Its result is an agent answer: result_type: "automatic_agent", with answer and catalog. A turn sent while the last one still runs rejects with ConflictError, code session_busy, and a session_id that isn't a conversation in this workspace with NotFoundError, code session_not_found. See QA.
| Parameter | Type | Required | Description |
|---|---|---|---|
workspaceId | string | Yes | The workspace. |
params.query | string | Yes | This turn's question, up to 4,096 characters. |
params.context | string | No | As on automatic search. |
params.path_prefix | string | No | As on automatic search. Changing it starts a fresh conversation. |
params.paths | string[] | No | As on automatic search. Changing it starts a fresh conversation. |
params.session_id | string | No | Continue a conversation: an earlier turn's answer.session_id. Default: start one. |
params.allow_clarification | boolean | No | Let the agent ask back when a question is ambiguous. Default: true. |
params.clarification_answers | Array<{ question, answer? }> | No | Answers to the last turn's clarifying questions, in order. Needs session_id. |
params.output_schema | Record<string, unknown> | No | A JSON Schema for this turn's answer only. |
params.wait_seconds | number | No | 0 to 60 (MAX_WAIT_SECONDS). |
params.idempotency_key | string | No | Up to 200 characters. Minted per call when omitted. |
params is a QaParams.
const first = await client.jobs.wait(
(await client.search.qa(workspaceId, { query: "What was total freight spend in FY2024?" })).job_id,
);
const { session_id } = (first.result as { answer: { session_id: string } }).answer;
const followUp = await client.search.qa(workspaceId, { query: "And for FY2023?", session_id });Evidences and where they are
Every answer cites the passages it rests on, as evidences. Where they sit depends on the route that answered, so read routing.final, not the mode you sent: a search in auto mode can go either way, and a QA turn always comes from the agent. A job's result is typed unknown, so the examples below declare the fields they read.
| Answer | Evidences | Ground jobs |
|---|---|---|
Automatic search, routing.final === "fact" | result.output.evidences | result.output.grounding_job_ids: jobs the search already ran to locate the quotes. Read them. |
Automatic search, routing.final === "agent" | result.output.answer.evidences | None (grounding_job_ids is null). Ground the quotes yourself. |
| QA turn | result.answer.evidences | None. Ground the quotes yourself. |
Each evidence has source_path and source_file_id (the cited file), page and component (a sheet or section; either may be null), quote (the passage as it appears in the file) and relevance (why it matters). See Evidences.
type Evidence = {
source_path: string;
source_file_id: string | null;
page: number | null;
component: string | null;
quote: string;
relevance: string;
};
type Answer = { evidences: Evidence[]; grounding_job_ids: string[] | null };
type SearchResult = { routing: { final: "fact" | "agent" }; output: Answer & { answer: Answer } };
const job = await client.search.automatic(workspaceId, { query: "What were total liabilities at year end?" });
const result = (await client.jobs.wait(job.job_id)).result as SearchResult;
const answer = result.routing.final === "fact" ? result.output : result.output.answer;
answer.evidences.forEach((evidence, i) => console.log(i + 1, evidence.source_path, evidence.page, evidence.quote));Read the Ground jobs of a fact answer
A fact answer lists, in grounding_job_ids, Ground jobs the search ran over its cited files. Read each with jobs.wait; one may still be running when the answer arrives. In each job's result, a target's id is the 1-based position of its evidence in evidences ("3"), or "3#1" for one part of a quote stitched together from several places. A target whose status is "found" has the location in matches[0].location; see Ground for its keys.
type GroundResult = {
targets: { id: string; status: string; matches: { location: Record<string, unknown> }[] }[];
};
const locations = new Map<number, Record<string, unknown>>();
for (const jobId of answer.grounding_job_ids ?? []) {
const ground = (await client.jobs.wait(jobId)).result as GroundResult;
for (const target of ground.targets) {
const number = Number(target.id.split("#")[0]);
const match = target.matches[0];
if (target.status === "found" && match && !locations.has(number)) {
locations.set(number, match.location);
}
}
}
answer.evidences.forEach((evidence, i) => console.log(i + 1, evidence.quote, locations.get(i + 1) ?? "not located"));Ground an agent answer yourself
An agent answer, from an automatic search the service sent to the agent or from a QA turn, runs no Ground jobs: its quotes come without locations. To locate them, call client.documents.ground once per cited file, with a workspace_file source naming the workspace and the evidence's source_file_id, and up to 30 quotes as targets. Each is a Ground job, billed like any other.
- Give each target the evidence's position as its
id, so you can match the results back. - Pass the page as a hint when the evidence has one (
"page 12"), and thecomponentassheetfor a spreadsheet. - Split a stitched quote. An agent may join parts of a passage with
…; the whole quote then matches nowhere. Send each part as its own target.
const byFile = new Map<string, { id: string; text: string; hint?: string }[]>();
answer.evidences.forEach((evidence, i) => {
if (!evidence.source_file_id) return;
// A quote stitched with "…" matches nowhere whole: look for each part (of 6 characters or more).
const split = evidence.quote.split(/\s*(?:…|\.{3,})\s*/).filter((part) => part.length >= 6);
const parts = split.length >= 2 ? split : [evidence.quote];
const targets = byFile.get(evidence.source_file_id) ?? [];
parts.forEach((text, part) =>
targets.push({
id: parts.length === 1 ? String(i + 1) : `${i + 1}#${part}`,
text,
...(evidence.page !== null && { hint: `page ${evidence.page}` }),
}),
);
byFile.set(evidence.source_file_id, targets);
});
for (const [fileId, targets] of byFile) {
for (let start = 0; start < targets.length; start += 30) {
const job = await client.documents.ground({
source: { type: "workspace_file", workspace_id: workspaceId, file_id: fileId },
targets: targets.slice(start, start + 30),
options: { max_matches: 1 },
});
const ground = (await client.jobs.wait(job.job_id)).result as GroundResult;
for (const target of ground.targets) {
const match = target.status === "found" ? target.matches[0] : undefined;
console.log(target.id, match ? match.location : "not located");
}
}
}