Skip to main content

Workspace sources

A workspace (the same id as its chat session) keeps a saved source set, and every question asked inside it answers from that set. These routes read it, save it, and add or remove one file at a time.

All routes require the configured API-key header and trusted principal headers (X-User-Subject, plus the optional role/unit/level claims) exactly as the other principal-bearing routes do; see Conversations and memory. A caller who is not a member of the workspace gets 404 on every route below, the same answer an unknown workspace id gets. A member whose role is viewer may read the set and gets 403 on every write. The three workspace roles and what each may do are listed in workspace members.

Document ids are the AI-engine documents.id UUID — the aiDocumentId from a Kafka status event, the document_id in a search hit or citation, or the id from GET /workspaces/{id}/documents. They are not the partner registry's documentId. Source-selection writes refuse malformed document ids with 422; upload deletion uses the separate contract below.

Source saves, per-file selections, and ingestion auto-selection recheck that requested document rows still exist under the workspace row lock. A document deleted before that lock is acquired is omitted from the write, even if the initial lookup found it. The response contains the resolved set.

GET /workspaces/{workspace_id}/sources​

Returns the saved set, resolved against the corpus as it stands now. Any member, including a viewer, may call it.

FieldTypeMeaning
whole_corpusbooleanEvery document the caller may read.
collection_idsstring arrayWhole collections, each counting as one source.
document_idsstring arrayIndividually selected documents.
excluded_document_idsstring arrayDocuments removed from the set. Exclusion wins over selection.
source_countintegerCollections + documents + 1 when whole_corpus.
answerablebooleanfalse when the set is empty; the workspace then refuses to answer rather than widening to the corpus.
kept_own_uploadsintegerAlways 0 on a read. Only a save reports it.
issuesarrayPicks that moved (kind: "moved", with current_document_id) or vanished (kind: "missing"). Never silently dropped.

PUT /workspaces/{workspace_id}/sources​

Saves the set. owner and partner only; viewer gets 403.

{
"whole_corpus": false,
"collection_ids": ["JDIH"],
"documents": [{"document_id": "0f2b0000-0000-4000-8000-000000000001"}],
"excluded_document_ids": []
}

extra fields are refused with 422, as is a malformed document id in documents or excluded_document_ids. Only document_id is accepted per document; the stable content key that lets a pick survive re-ingestion is read from our own row.

What the save replaces. Collections, whole_corpus, and shared-corpus documents are replaced by exactly what the request names. Unticking one removes it.

What the save keeps. The workspace's own uploads that are already selected and that the request does not name stay selected. A save that omits a file is not a decision to remove it, and production lost ten uploads to exactly that. Only indexed uploads that have not been superseded are kept. The same picks are kept when saving whole_corpus: true, so switching whole-corpus mode on and off does not drop them. source_count counts those stored picks alongside the whole-corpus entry.

kept_own_uploads in the response and in the audit row (origin: "workspace-sources", change: "save") reports how many were kept. A client that sends a stale set therefore sees the count rather than discovering the gap later.

Removing one file. Omitting an own upload from documents keeps it selected. Naming it in excluded_document_ids deliberately removes it: exclusion wins over selection, and an omitted file is no longer kept by later saves once it is no longer selected. PUT replaces its exclusions, so repeat any exclusions needed for collection or whole-corpus scopes. The per-file DELETE below additionally records the removal across later saves, including whole-corpus saves. Naming a file in documents clears that recorded removal; an explicit exclusion in the same request still wins.

POST /workspaces/{workspace_id}/sources/documents​

Adds one document to the set. owner and partner only; viewer gets 403. A document the caller cannot see, or an unknown id, gets 404.

{"document_id": "0f2b0000-0000-4000-8000-000000000001"}

Additive: the stored set is read and written back with this file added, under one row lock, so two toggles cannot lose each other and a per-file action can never blank the rest of the set. Selecting a file that was removed clears the removal. Idempotent — selecting a file already selected returns the same set. Returns the resolved set.

DELETE /workspaces/{workspace_id}/sources/documents/{document_id}​

Removes one document from the set and records the decision, which is what keeps later saves from putting it back. owner and partner only; viewer gets 403. A document the caller cannot see, or an unknown id, gets 404.

The document is removed from document_ids and added to excluded_document_ids; whole-corpus mode may retain individual picks, but the exclusion removes the file from the whole-corpus scope too. Idempotent. Returns the resolved set.

DELETE /workspaces/{workspace_id}/documents/{document_id}​

Deletes one upload the workspace owns, for good: its index row, its chunks, and its raw object. owner and partner only; viewer gets 403. Success returns 204 with no body.

The route records source removal before deleting the document. Once that source update succeeds, a failure before index deletion leaves the file indexed and unselected, including through collection and whole-corpus scopes. After index deletion, it clears the removal record. An existing notebook pick of the deleted document reads as missing in issues.

A generic 500 leaves the outcome uncertain: source updates or audits may fail before or after index deletion. Read GET /workspaces/{workspace_id}/sources and GET /workspaces/{workspace_id}/documents, then retry only if the document is still listed.

StatusDetailWhen
204Empty bodyDeleted.
403{ "detail": "..." }Caller is a viewer. Nothing changes.
404{ "detail": "Document not found" }Not a document this workspace owns: another workspace's file, a shared-corpus document, a superseded version, or an unknown id. Nothing changes.
409{ "detail": "This upload is still being processed. Delete it after it finishes." }The target upload is queued, processing, or retrying, the document row is pending or processing, or an active workspace upload has no stored document id. Nothing is cancelled and nothing changes. Retry once it settles.
500{ "detail": "Document removed from the index but its raw object could not be deleted" }The index rows are gone and the raw object delete failed. Do not retry: a retry returns 404. Escalate the orphaned object to an operator.

Any queued, processing, or retrying upload in this workspace without a stored document id blocks every document delete with 409, including unrelated documents, until that upload settles.

After the active-upload guard passes, a malformed id returns 404. Upload ids (doc-…) can identify an active upload for 409; use the stored document UUID to delete a completed upload.

Re-uploading a file​

Selection follows the document, not the upload.

  • A byte-identical re-upload with the same source version returns the existing document row before selection runs. It retains the selection state, including any recorded removal. Select it again with POST …/sources/documents to restore it.
  • A new version uploaded through POST /workspaces/{id}/documents/{document_id}/replace is auto-selected when it indexes and clears the OCR review gate, replacing its predecessor in document_ids. This selects the new version even if the predecessor was unselected; the predecessor's removal record remains tied to its old id. If the predecessor was selected, it keeps answering until the new version is ready.
  • A different file is auto-selected once indexed and cleared of OCR review, without clearing a removal recorded for another file.

Migrating the Studio's per-file toggle​

A toggle built on PUT /sources inherits its failure mode: one stale payload blanks the set. Switch each per-file control to the additive calls.

ActionBeforeAfter
Tick one fileRe-send the whole set with the file addedPOST …/sources/documents with {"document_id"}
Untick one fileRe-send the whole set with the file removedDELETE …/sources/documents/{document_id}
Tick or untick a whole collectionRe-send the whole setunchanged — PUT …/sources
Save the whole Sumber railPUT …/sourcesunchanged, and own uploads it omits are now kept; read kept_own_uploads
Show selection stateJoin GET …/sources with the file listunchanged

Both per-file routes return the resolved set, so the client re-renders from the response and never merges locally.

A file a user removed before this change was recorded as an omission, not as a removal, and the safety net only keeps files that are still selected. Files already dropped stay dropped; select them again with POST …/sources/documents to bring them back.