The 13 actions
The order below is the order of KNOWLEDGE_ACTIONS. Examples show the SDK call; over HTTP and MCP the same fields go as JSON. Answers are shortened: ... marks left-out content, and // comments are explanations, not part of the answer.
discover
Needs: Always available
When to use it. First, whenever the agent does not already know what is in scope. One call returns the documents, what is inside each one and which actions are on, so later calls can use real sheet, column, section and field names.
| Field | Type | Notes |
|---|
| action | string | "discover" |
| source_ids | string[] | Narrow to these sources. Ids outside the project are ignored. |
| document_ids | string[] | Narrow to these documents. Also asks for their structure in full, past the cap a whole-page discover applies. |
| limit | integer | Documents per page. Default 50, clamped to 1 to 200. |
| cursor | string | The next_cursor of a previous page, sent back verbatim. |
answer = client.search_knowledge_base("discover")
const answer = await client.searchKnowledgeBase("discover");
{
"success": true,
"documents": [
{"id": "...", "name": "costs.xlsx", "source_id": "finance",
"kind": "spreadsheet", // "spreadsheet" or "text", decided by MIME
"document_type": "...", "mode": "hybrid", "failure_reason": null,
"structure": {...}} // sheets / sections + last_page / keys / chunks
],
"count": 50, "has_more": true,
"next_cursor": "...", "next_page": {"action": "discover", "cursor": "..."},
"document_types": [...], // census of the whole scope
"fields_by_type": {...}, // extracted field names, types, counts
"available_actions": {"search": "...", "compute": "... (OFF)", ...},
"next_action": {"for a clause or a fact": {"action": "search", "query": "..."}, ...}
}
A spreadsheet structure lists sheets with name, column headers, row count and frame_key (the name compute will give that sheet). A document with headings lists sections and last_page, JSON lists keys, anything else falls back to chunks.
Lists inside a structure are capped, with the rest reported as more_columns, more_sections, more_keys or more_sheets. Naming the documents in document_ids returns them whole.
fields_by_type and document_types are computed over the whole scope, not only the current page.
available_actions covers the other twelve actions. An action that is off for this workspace is listed with " (OFF)" appended, never hidden.
search
Needs: Always available
When to use it. Questions answered by reading text. To look up an ID, code, SKU or invoice number, send the bare identifier alone (for example "2525"), not the whole question.
| Field | Type | Notes |
|---|
| action | string | "search" |
| query | string | Required. Words, or a bare content identifier. |
| source_ids | string[] | Narrow to these sources. |
| document_ids | string[] | Narrow to these documents (intersects with source_ids). |
| top_k | integer | How many passages. Default 10. |
| mode | "hybrid" | "graph" | Omit it and the search decides from the documents in scope. "graph" also searches the Context Graph. |
answer = client.search_knowledge_base("search", query="annual leave carry over")
const answer = await client.searchKnowledgeBase("search", { query: "annual leave carry over" });
{
"success": true,
"hits": [
{"document_id": "...", "document_name": "HR leave policy", "chunk_text": "...",
"score": 0.83, "source_id": "hr", "chunk_idx": 4}
],
"usage": {...},
"hint": {"action": "compute", "reason": "aggregate question over a spreadsheet"} // only on a mixed scope
}
chunk_idx is where the passage sits in its document. Pass it to get_chunks as start to read around it.
A query that names a file (for example "what is in report_2024.xlsx") searches document names instead and adds "matched_by": "filename" to the same hit shape. mode is ignored on that path. If no visible document has that name, the ordinary search runs.
An aggregate question (sum, total, average, count, top, per, a "by column" grouping, a numeric comparison and so on) over a scope where every document is a spreadsheet is refused with a next_action pointing at compute. See refusals below.
get_doc
Needs: Always available
When to use it. Reading one whole document by its system id, taken from discover, list or a search hit. Never pass an invoice number or other content identifier here.
| Field | Type | Notes |
|---|
| action | string | "get_doc" |
| document_id | string (uuid) | Required. |
answer = client.search_knowledge_base("get_doc", document_id="3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10")
const answer = await client.searchKnowledgeBase("get_doc", { document_id: "3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10" });
{
"success": true,
"document": {
"id": "...", "name": "...", "text": "...", "acl": [...], "status": "completed",
...,
"failure_reason": null, // one FAILURE_REASONS code, or null
"failure_message": null // the fixed sentence for that code
}
}
A document that failed to upload carries failure_reason, one fixed code, and failure_message, its fixed sentence. See the table under More.
A document that is absent, not visible to the caller, or outside the project all answer the same "document not found: <id>".
get_docs
Needs: Always available
When to use it. Several whole documents at once, by id.
| Field | Type | Notes |
|---|
| action | string | "get_docs" |
| document_ids | string[] | Required. |
| max_chars | integer | Total document text to return before stopping. Default 200,000, floor 1,000. |
answer = client.search_knowledge_base("get_docs", document_ids=["3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10", "8d1e7f02-6b3c-4a59-8e2d-1f0c9b7a6e35"])
const answer = await client.searchKnowledgeBase("get_docs", { document_ids: ["3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10", "8d1e7f02-6b3c-4a59-8e2d-1f0c9b7a6e35"] });
{
"success": true,
"documents": [{...}, {...}], // same per-document shape as get_doc
"count": 2,
"remaining_document_ids": ["..."], // only when the budget ran out
"next_page": {"action": "get_docs", "document_ids": ["..."]}
}
An id that is absent or not visible is simply missing from documents, never an error.
The first document is always returned, even if it alone exceeds max_chars.
get_chunks
Needs: Always available
When to use it. Walking one long document in order, a range at a time, or reading around a search hit by starting at its chunk_idx.
| Field | Type | Notes |
|---|
| action | string | "get_chunks" |
| document_id | string (uuid) | Required. |
| start | integer | First chunk position, 0-based. Default 0. |
| end | integer | Last position, inclusive. At most 25 chunks come back per call whatever is asked. |
answer = client.search_knowledge_base("get_chunks", document_id="3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10", start=4)
const answer = await client.searchKnowledgeBase("get_chunks", { document_id: "3f2b8c1e-5a4d-4e8f-9b61-2c7d0a9e4f10", start: 4 });
{
"success": true,
"document_id": "...", "total_chunks": 120, "start": 4, "end": 28,
"chunks": [{"position": 4, "text": "...", "language": "en"}, ...],
"has_more": true, "next_start": 29,
"next_page": {"action": "get_chunks", "document_id": "...", "start": 29}
}
list
Needs: Always available
When to use it. Browsing documents without searching. Cheaper than discover: it does not fetch structure, fields or the census.
| Field | Type | Notes |
|---|
| action | string | "list" |
| source_ids | string[] | Narrow to these sources. |
| document_ids | string[] | Narrow to these documents. |
| limit | integer | Default 50, clamped to 1 to 200. |
| cursor | string | The next_cursor of a previous page. |
answer = client.search_knowledge_base("list", source_ids=["hr"], limit=20)
const answer = await client.searchKnowledgeBase("list", { source_ids: ["hr"], limit: 20 });
{
"success": true,
"documents": [{"id": "...", "name": "...", "source_id": "hr", "kind": "text",
"document_type": null, "mode": "hybrid", "failure_reason": null,
"status": "completed"}],
"count": 20, "has_more": true,
"next_cursor": "...", "next_page": {"action": "list", "cursor": "..."}
}
A malformed cursor is refused with "invalid cursor", never read as page one.
Needs: On unless discover marks it (OFF)
When to use it. The structured fields a question names (dates, amounts, parties) and each document’s value for them. It does not filter or compare by value: read the values and compare them yourself.
| Field | Type | Notes |
|---|
| action | string | "query_meta" |
| query | string | Required. A question naming a field from fields_by_type. |
| source_ids | string[] | Narrow to these sources. |
| limit | integer | Candidates to return. Default 20, clamped to 1 to 100. |
answer = client.search_knowledge_base("query_meta", query="invoice amount and invoice date")
const answer = await client.searchKnowledgeBase("query_meta", { query: "invoice amount and invoice date" });
{
"success": true,
"question": "...",
"resolved_fields": [...],
"documents": [{"id": "...", "source_id": "...", "name": "...",
"document_type": "...", "structured_data": {...}, ...}],
"count": 3
}
Refused for document_ids. The same aggregate refusal as search applies over an all-spreadsheet scope.
compute
Needs: Code execution on for the workspace (on by default)
When to use it. Any figure derived from spreadsheets (total, average, count, ranking, margin, comparison) and finding the exact row that matches one id or value. It runs over every row of the sheets in scope, not a sample.
| Field | Type | Notes |
|---|
| action | string | "compute" |
| query | string | Required. What to compute, in plain words, with columns named as discover spelled them. |
| source_ids | string[] | Narrow to these sources. |
| document_ids | string[] | Narrow to these documents. |
answer = client.search_knowledge_base("compute", query="total amount by region in costs.xlsx")
const answer = await client.searchKnowledgeBase("compute", { query: "total amount by region in costs.xlsx" });
{
"success": true, // whether the generated code ran
"result": ...,
"code": "...", "stdout": "", "error": null,
"execution_time": 0.41,
"documents_used": ["..."],
"provider_tokens": {"llm_input": 812, "llm_output": 164},
"attempts": 1 // 1 or 2 code-generation calls
}
Nothing tabular in scope comes back as a result, for example {"success": false, "error": "no tabular (CSV/TSV/XLSX) documents found in scope for compute()"}, not as a transport error.
The project’s redaction rules apply to the whole answer, except the top-level list of document ids.
map_reduce
Needs: On unless discover marks it (OFF)
When to use it. Asking the same question of every document in scope and getting one answer per document, for example "which contracts mention X", where search would return a handful of passages and miss the rest. Not for figures: that is compute.
| Field | Type | Notes |
|---|
| action | string | "map_reduce" |
| query | string | Required. The question to ask of each document. |
| source_ids | string[] | Narrow to these sources. |
| document_ids | string[] | Narrow to these documents. |
| limit | integer | Documents to read, newest first. Default 25, at most 200. |
answer = client.search_knowledge_base("map_reduce", query="Does this contract have an auto-renewal clause?")
const answer = await client.searchKnowledgeBase("map_reduce", { query: "Does this contract have an auto-renewal clause?" });
{
"success": true,
"results": [
{"document_id": "...", "document_name": "...", "data": {...}},
{"document_id": "...", "document_name": "...", "error": "..."}
],
"processed": 24, "failed": 1, "considered": 25,
"hint": {"action": "compute", "reason": "..."} // only when some targets are spreadsheets
}
Refused for any instruction when every document the call would read is a spreadsheet, because it reads a slice of each document and would merge partial totals. The check runs before any model call.
get_neighbors
Needs: Documents uploaded in graph mode (Context Graph)
When to use it. What is one step from a named thing, and which way each link points.
| Field | Type | Notes |
|---|
| action | string | "get_neighbors" |
| entity | string | Required. An entity id from an earlier graph answer (preferred), or its name as written in the documents. |
| source_ids | string[] | Narrow to these sources. |
| limit | integer | Default 20, clamped to 1 to 200. |
answer = client.search_knowledge_base("get_neighbors", entity="Acme Corp")
const answer = await client.searchKnowledgeBase("get_neighbors", { entity: "Acme Corp" });
{
"success": true,
"entity": {"id": "...", "name": "acme corp", "type": "ORG"},
"neighbors": [{"id": "...", "name": "...", "type": "PERSON", "category": "MEMBERSHIP",
"label": "works_for", "evidence": "...", "direction": "incoming"}],
"count": 7
}
traverse
Needs: Documents uploaded in graph mode (Context Graph)
When to use it. Everything within a few steps of a named thing.
| Field | Type | Notes |
|---|
| action | string | "traverse" |
| entity | string | Required. An entity id (preferred) or name. |
| depth | integer | Hops, 1 to 5. Default 2. |
| category | enum | Only follow this kind of connection. |
| source_ids | string[] | Narrow to these sources. |
| limit | integer | Paths. Default 50, clamped to 1 to 200. |
answer = client.search_knowledge_base("traverse", entity="Acme Corp", depth=2, category="HIERARCHICAL")
const answer = await client.searchKnowledgeBase("traverse", { entity: "Acme Corp", depth: 2, category: "HIERARCHICAL" });
{
"success": true,
"entity": {"id": "...", "name": "...", "type": "ORG"},
"paths": [{"target_id": "...", "target": "...", "target_type": "PERSON",
"category": "HIERARCHICAL", "label": "reports_to", "evidence": "...", "hops": 2}],
"count": 12
}
Needs: Documents uploaded in graph mode (Context Graph)
When to use it. Connections of a kind across the corpus, with no starting entity (an entity is refused; for how two named things connect, traverse from one). Also the way in when every name in an answer is masked: take a source_id or target_id from its result.
| Field | Type | Notes |
|---|
| action | string | "find_related" |
| category | enum | The kind of connection. At least one of category, label or entity_type is required. |
| label | string | The exact relationship wording as extracted, for example reports_to. |
| entity_type | enum | Keep only relationships with an entity of this type on one end. |
| source_ids | string[] | Narrow to these sources. |
| limit | integer | Relationships. Default 50, clamped to 1 to 200. |
answer = client.search_knowledge_base("find_related", category="CREATION", entity_type="PRODUCT")
const answer = await client.searchKnowledgeBase("find_related", { category: "CREATION", entity_type: "PRODUCT" });
{
"success": true,
"relationships": [{"source_id": "...", "source": "...", "source_type": "ORG",
"target_id": "...", "target": "...", "target_type": "PRODUCT",
"category": "CREATION", "label": "makes", "evidence": "..."}],
"count": 9
}
Needs: Context Graph documents, and community summaries on
When to use it. The themes each source groups into, ranked against a question.
| Field | Type | Notes |
|---|
| action | string | "community_summary" |
| query | string | Optional. Without one, the main themes. A question no summary clears the relevance floor for gets the closest themes, marked loose_match. |
| source_ids | string[] | Narrow to these sources. |
| limit | integer | Communities. Default 3, clamped to 1 to 10. |
answer = client.search_knowledge_base("community_summary", query="supplier risk")
const answer = await client.searchKnowledgeBase("community_summary", { query: "supplier risk" });
{
"success": true,
"communities": [{"summary": "...", "entity_count": 14, "relationship_count": 22,
"hierarchy_level": 0, "relevance": 0.7121}],
"count": 3
}
Community summaries can be off for a workspace. Then discover marks this action (OFF) and never suggests it, and a call is refused with a sentence that says why.
A community is built and read inside one source. Summaries are withheld (success false, with an error saying why) from a caller who cannot see every passage in scope, and refused when the call names documents, since a summary covers a whole source.
Graph answers and masking. Entity names, types, labels, evidence and community summaries come from document text, so every string in a graph answer goes through the project’s redaction rules. The entity ids (id, source_id, target_id) are exempt: they carry no text, and they are the only handle a model can pass back as the next entity when the names are masked. An id the caller cannot see answers entity not found, the same as an unknown name. Rebuilding the graph makes new ids.