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.

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.

ParameterTypeRequiredDescription
workspaceIdstringYesThe workspace.
params.querystringYesThe question, up to 4,096 characters.
params.contextstringNoBackground the answer takes as given, up to 20,000 characters. Default: none.
params.path_prefixstringNoSearch only the files under this folder. Default: the whole workspace.
params.pathsstring[]NoSearch exactly these files, by path, as well as any under path_prefix.
params.top_knumberNoHow 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_schemaRecord<string, unknown>NoA JSON Schema the answer must follow; the answer's structured_answer holds it.
params.wait_secondsnumberNo0 to 60 (MAX_WAIT_SECONDS).
params.idempotency_keystringNoUp to 200 characters. Minted per call when omitted.

params is an AutomaticSearchParams.

automatic-search.ts
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.

ParameterTypeRequiredDescription
workspaceIdstringYesThe workspace.
params.querystringYesThis turn's question, up to 4,096 characters.
params.contextstringNoAs on automatic search.
params.path_prefixstringNoAs on automatic search. Changing it starts a fresh conversation.
params.pathsstring[]NoAs on automatic search. Changing it starts a fresh conversation.
params.session_idstringNoContinue a conversation: an earlier turn's answer.session_id. Default: start one.
params.allow_clarificationbooleanNoLet the agent ask back when a question is ambiguous. Default: true.
params.clarification_answersArray<{ question, answer? }>NoAnswers to the last turn's clarifying questions, in order. Needs session_id.
params.output_schemaRecord<string, unknown>NoA JSON Schema for this turn's answer only.
params.wait_secondsnumberNo0 to 60 (MAX_WAIT_SECONDS).
params.idempotency_keystringNoUp to 200 characters. Minted per call when omitted.

params is a QaParams.

qa.ts
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.

AnswerEvidencesGround jobs
Automatic search, routing.final === "fact"result.output.evidencesresult.output.grounding_job_ids: jobs the search already ran to locate the quotes. Read them.
Automatic search, routing.final === "agent"result.output.answer.evidencesNone (grounding_job_ids is null). Ground the quotes yourself.
QA turnresult.answer.evidencesNone. 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.

search-evidences.ts
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.

read-grounding.ts
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 the component as sheet for 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.
ground-agent-answer.ts
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");
    }
  }
}

On this page