Technical docs

Storage

Overview

Support reference for the org Storage page (`/storage`) and the org storage APIs behind it.

For the organization private API surface under `/api/private`, see `private-api.md`. For managed SQLite databases and the `/databases` workspace, see `databases.md`.

What users can do in `/storage`

  • Browse folders with URL-backed navigation via `?path=...`.
  • Switch between list and grid views.
  • Search the current folder, sort, hide/show columns, and filter by file type and modified date.
  • Select multiple items and bulk delete.
  • Create folders.
  • Upload by file picker, drag-and-drop, or folder upload.
  • Download files or download folders as `.zip` archives.
  • Copy file or folder paths.
  • Share folders with AI teammates and Access Groups (the group tab is available where the organization supports Access Group sharing).
  • Open non-folder items in the visible `Docs` workspace via `/docs?path=...`.

Personal documents

  • Each active organization user's personal folder has the canonical path `.users/{userId}`. New and restored memberships provision the directory; the root Storage listing also ensures the current user's directory exists when presenting it. At `/storage`, each user sees a `My Documents` shortcut to their own folder; it is a view of the same path, not a separate copy. Owners and admins can browse `.users`, where user folders display their names with the numeric owner ID. Other members do not see the `.users` root in the top-level listing; they still reach their own shortcut and folders shared with them.
  • A user can create, upload, edit, and delete contents inside their personal directory. The `.users` container and `.users/{userId}` directory roots cannot themselves be renamed, moved, copied, deleted, or created through ordinary Storage mutations. A top-level `My Documents` folder name is reserved for the shortcut, while preexisting folders with that name can still be edited beneath the folder.
  • With Storage Access Group enforcement enabled, a user and their Personal Ally receive read/write access to that user's personal directory alongside any group-granted paths. Regular AI teammates acting for a user still need both the user's access and their own shared-folder grant; a share is not an organization-wide grant. When enforcement is off, member sharing remains limited to their writable personal folder paths rather than all organization Storage.

Current file actions in the UI

  • Files: `Download`, `Details`, `Copy Path`, `Delete`.
  • `Details` currently opens the file in Docs rather than opening the unused Storage details panel.
  • A file in `Indexing failed` state also has `Reindex`.
  • Folders: `Download`, `Share`, `Reindex failed files`, `Copy Path`, `Delete`.
  • Folder `Download` retrieves a server-generated `{folder name}.zip`; archive generation preserves empty nested folders and skips symlinks.
  • The toolbar has `Reindex` for the currently open folder; at root this covers the full organization storage tree.
  • The Storage UI does not currently expose rename, copy, move, or a dedicated restore action, even though org APIs exist for rename, copy, and move.

Deletion and trash retention

  • Deleting a file or folder outside `.trashbin` moves it into the organization-level `.trashbin` directory instead of erasing its bytes immediately.
  • The deleted entry keeps its original directory chain beneath `.trashbin`, and its name receives a UTC deletion timestamp. Repeated deletions of the same path remain separate.
  • Deleting a file or folder that is already inside `.trashbin` erases it permanently.
  • Deletion removes matching folder-share records and vector-index records immediately, even while the bytes remain in `.trashbin`.
  • Exact Storage search and indexing backfills exclude `.trashbin`, so deleted content is not returned or re-enqueued.
  • A managed daily maintenance job permanently purges entries after `30 days` and removes empty mirrored directories left behind.

Upload rules

  • The `/storage` UI and backend upload registry accept: `png`, `jpg`, `jpeg`, `gif`, `webp`, `jxl`, `avif`, `bmp`, `tif`, `tiff`, `heic`, `heif`, `svg`, `ico`, `woff`, `woff2`, `ttf`, `otf`, `pdf`, `doc`, `docx`, `xls`, `xlsm`, `xlsx`, `ppt`, `pptx`, `epf`, `ert`, `efd`, `pages`, `numbers`, `key`, `rtf`, `odt`, `ods`, `odp`, `txt`, `htm`, `html`, `md`, `markdown`, `csv`, `json`, `webmanifest`, `yaml`, `yml`, `tsv`, `ts`, `tsx`, `js`, `mjs`, `jsx`, `py`, `sql`, `sqlite`, `db3`, `db`, `sqlite3`, `db-wal`, `db-shm`, `db3-wal`, `db3-shm`, `sqlite-wal`, `sqlite-shm`, `sqlite3-wal`, `sqlite3-shm`, `pls`, `pks`, `pkb`, `pck`, `spc`, `trg`, `java`, `jsp`, `css`, `scss`, `sh`, `log`, `xml`, `toml`, and `ini`.
  • The backend validates the file extension and checks detected content for binary formats; for XLSX it also preflights the spreadsheet. It rejects unsupported or mismatched content instead of trusting the supplied browser MIME type. Text-like formats use extension-only checks except CSV, which rejects detected binary content. Accepted uploads receive the canonical MIME for their extension. Upload support does not mean a file is text-editable, vector-indexable, or previewable.
  • SQLite database uploads (`.sqlite`, `.db3`, `.db`, `.sqlite3`) require detected SQLite content. Their WAL/SHM companions are supported as extension-only uploads. SQLite files are not text-write formats or vector-indexed documents.
  • The Oracle SQL source extensions `pls`, `pks`, `pkb`, `pck`, `spc`, and `trg` use the SQL MIME allowlist. `java` and `jsp` are uploadable source formats. These additional upload formats do not expand the narrower vector-indexing allowlist below.
  • Max upload size: `10 MB` per file by default. The frontend uses `NEXT_PUBLIC_STORAGE_MAX_UPLOAD_SIZE_MB`; organization and private upload API routes both use `STORAGE_MAX_UPLOAD_SIZE_MB` on the backend. Configure them consistently; both fall back to `10 MB` for unset or invalid values.
  • In the `/storage` UI, individual files cannot be uploaded to root (`/`).
  • The org and private upload APIs still allow root uploads when `path` is omitted.
  • Root-level folder uploads are allowed.
  • Duplicate handling offers `Skip existing` and `Replace`.
  • Selecting or dropping a ZIP in Storage expands its supported files in the browser before upload into a folder named after the archive (without `.zip`), preserving nested paths. The archive itself is not uploaded. ZIP extraction shares the normal per-file upload limit and uses a default 100 MiB decoded-data budget and 1,000-file budget across the selected batch; unsupported entries appear as error rows and do not block accepted siblings. Invalid, password-protected, oversized, or unsafe archives produce an error row instead of an upload. ZIPs are processed in queued jobs so another archive is not expanded while the previous job is uploading.
  • When a folder upload contains unreadable local file or folder entries, readable siblings can still upload. The unreadable entries remain in the upload queue as non-retryable error rows with an `Unable to read this file/folder...` message.
  • Upload UI supports pause, resume, retry, cancel, and persistent progress.

Sharing behavior

  • Only folders can be shared.
  • Root (`/`) and the `.users` container cannot be shared. Sharing permissions are checked per folder, not solely by organization role: owners and admins can share writable organization paths; members can share writable paths inside their own personal directory with AI teammates. A folder's `access.can_share` reports this capability, and denied share or unshare requests return `403`.
  • Sharing uses a dedicated dialog with AI teammates in the org. Personal Allies are excluded from the editable candidate and holder lists; organization-owned system AI teammates remain available as ordinary share candidates.
  • The dialog always shows a decorative `Ally` row with both `Read` and `Write` checked and disabled.
  • The dialog text for Ally says `Full storage access is automatic.`
  • Permissions are `read` and `write`.
  • In the current dialog, enabling `write` also enables `read`.
  • If `write` is removed, `read` remains checked unless it is inherited from a parent share.
  • `read` is disabled when it is inherited or when `write` is checked, so the dialog never allows write-only access.
  • Parent-folder permissions appear as inherited (`from_parent: true`) and cannot be toggled from the child folder.
  • When a shared folder is renamed or moved, direct share records for that folder follow the new path.
  • Share records for nested folders under the renamed or moved folder are updated to the matching nested path under the new location.
  • After a move, parent-folder shares at the destination also apply through normal inherited permissions.
  • Deleting a folder or file path removes matching share records for that path and nested paths.
  • The share dialog seeds its initial employee list from `permission_holders` in the storage tree response. Additional candidates are loaded through a paginated search query as the user scrolls or searches.
  • For owners and admins, where Access Group sharing is available, the folder dialog has `AI Teammates` and `Access Groups` tabs. On the group tab, choose `No direct access`, `Read`, or `Manage` for each group. Inherited group access is labeled `Inherited from {path}` and must be changed at the parent. Root cannot be granted through this dialog. Members sharing their personal folders see AI teammate controls, not group-management controls. Storage folder rows and details can show group grants and their inherited origin.
  • With Storage Access Group enforcement enabled, ordinary members can read folders shared with their active groups and write in folders where they have a `Manage` grant and in their own personal directory. Owners and admins have organization-wide Storage access and can manage organization-wide sharing. A user can list a parent folder leading to a granted subtree without reading unrelated files in that parent. For ordinary AI teammates running for an organization user, allowed Storage paths are the intersection of the user's effective grants (including the personal folder) and the teammate's shared-folder grants; personal Ally inherits its owner's user access. Public AI teammate access uses that teammate's folder grants, and organization-credential calls retain organization-wide Storage access.

Indexing and search

  • Storage rows can show indexing states: `Indexed`, `Queued for indexing`, `Indexing…`, `Indexing failed`, `Not indexable`.
  • A failed file can be manually queued again through `Reindex`. Files in any other index state do not expose this action.
  • `Reindex failed files` on a folder queues every failed indexed document in that folder and its subfolders. The toolbar `Reindex` action does the same for the currently open folder, including the complete storage tree when root is open.
  • Reindex feedback is `File queued for reindexing`, `{count} file(s) queued for reindexing`, or `No failed files to reindex`.
  • Vector indexing currently supports `.txt`, `.md`, `.csv`, `.xlsx`, `.docx`, `.pdf`, and `.pptx`.
  • Exact storage search can also extract text from formats such as `docx`, `xlsx`, `pptx`, and `pdf`.
  • Exact search runs bounded scans:
  • at most `1000` matched files are counted
  • at most `50` matched files are inspected for the result listing
  • at most `10` of those files carry surrounding content lines
  • the whole scan has a `30`-second timeout and the per-file inspection has a `30`-second budget
  • Search responses report `truncated` when the match count is only a lower bound and `resultsTruncated` when the listing is partial, so a capped scan is not mistaken for a complete result set.
  • For employees, scans start from their readable shared-folder roots, so a truncated scan spends its budget on files that can actually be returned.
  • Valid uploads can still show `Not indexable`.
  • Vector embeddings use OpenAI by default or the configured OpenAI-compatible Custom provider.
  • Deployment operators can set `EMBEDDING_DIMENSIONS` to the positive-integer vector length returned by the selected embedding model; it defaults to `1536`. New Qdrant collections use that size, and embedding responses are checked against it.
  • Qdrant cannot resize an existing collection. If its dense-vector size differs from `EMBEDDING_DIMENSIONS`, collection initialization stops and indexing cannot continue until an operator recreates the collection and reindexes the documents.
  • The backend includes an operator-only reinitialization script. It deletes every stored vector before rebuilding the collection for the configured dimension, so a full reindex is required afterwards.

Spreadsheet extraction

  • CSV and XLSX attachments can be converted into bounded text previews and row-range results for chat and AI-teammate workflow runs.
  • Vector indexing uses a separate extraction profile for CSV and XLSX. It renders the complete spreadsheet without preview-character, CSV-line, worksheet, row-per-sheet, or cell-per-row caps. XLSX ZIP and XML preflight safety limits still apply.
  • XLSX sheet indexes are one-based. The first worksheet has `index: 1`, and index-based selection uses the same one-based values.
  • Row numbers are also absolute and one-based. Sparse worksheets keep their original positions, so a populated row at a high Excel row number is returned with that row number rather than being compacted.
  • XLSX preflight rejects invalid ZIP containers, missing workbook or worksheet XML, excessive entry counts, and XML content above configured extraction limits. One XML entry can use the full `25 MB` aggregate uncompressed XML budget.
  • Merge-heavy worksheets and worksheets with full-column data validation are sanitized before loading by removing merge and validation metadata that carries no cell text.
  • If a workbook from a third-party generator cannot be loaded directly, extraction retries after removing text-irrelevant drawing, comment, table, merge, and data-validation parts and normalizing prefixed SpreadsheetML namespaces. Cell values, shared strings, styles, and worksheet row positions remain available to the parser.

Org API routes

  • `GET /api/organizations/{orgId}/storage?path=...&with_permissions=...&with_index_status=...`
  • Returns a permission-aware listing: entries include access metadata and applicable Access Group folder grants when requested, and inaccessible files are omitted. Revoked or unavailable paths can return `404` rather than exposing their contents.
  • Returns `{ files, permission_holders }`. `permission_holders` lists the visible employees who hold a share on any file or folder in the returned tree, with `employee_id`, `name`, `role`, optional `is_system`, and `owned_by_user_id` (null for organization-owned teammates). A user's root listing includes the own `My Documents` shortcut with canonical `access.path` set to `.users/{userId}`; admins can list `.users` with display names. A member's request to list the `.users` container returns `404` when Storage enforcement is enabled.
  • `with_permissions` and `with_index_status` are optional booleans and both default to `true`, preserving the annotated response used by the Storage page.
  • `with_permissions=false` skips folder-share annotation and employee-holder lookup, so `shared` arrays and `permission_holders` are empty. `with_index_status=false` skips file index-status and hash annotation.
  • Docs tree requests set both flags to `false` because the Docs tree needs names and entry types but does not display sharing or indexing metadata.
  • `GET /api/organizations/{orgId}/storage/employees/{employeeId}/access`
  • Returns one AI teammate's shared folders, visible `permission_holders`, and `effective_permissions` for each folder. Effective permission rows include `permission` (`read` or `write`) and `from_parent`. Each shared folder also includes `can_share`; paths beneath `.users/{userId}` can include `personal_directory_display_name` for the owner.
  • `GET /api/organizations/{orgId}/storage/employees/{employeeId}/entries/search?q=...&limit=...`
  • Searches files and folders within the AI teammate's readable storage scope. Under enforced Access Group rules, personal Ally search follows its initiating owner's Storage grants rather than exposing the entire organization tree.
  • `q` is optional and capped at 128 characters. `limit` is clamped to `1` through `20` and defaults to `20`.
  • Returns `{ entries, truncated }`; each entry contains `name`, canonical absolute `path`, and `type` (`file` or `folder`).
  • `GET /api/organizations/{orgId}/storage/stats?path=...`
  • `POST /api/organizations/{orgId}/storage/upload`
  • `GET /api/organizations/{orgId}/storage/download?path=...`
  • A file path returns the file bytes with its MIME type and filename.
  • A folder path streams an `application/zip` attachment named `{folder name}.zip`; the server resolves the full archive entry list before sending the response.
  • `DELETE /api/organizations/{orgId}/storage/file?path=...`
  • `POST /api/organizations/{orgId}/storage/file/reindex?path=...`
  • Queues one file only when its current index state is `failed`; returns `404` without an indexed-document record and `409` for any other state.
  • `POST /api/organizations/{orgId}/storage/folder`
  • `POST /api/organizations/{orgId}/storage/folder/reindex?path=...`
  • Queues all failed indexed documents below the folder recursively and returns `{ reindexed }`. Root (`/`) covers the organization storage tree.
  • `POST /api/organizations/{orgId}/storage/folder/rename`
  • `POST /api/organizations/{orgId}/storage/folder/copy`
  • `POST /api/organizations/{orgId}/storage/folder/move`
  • `GET /api/organizations/{orgId}/storage/file/content?path=...`
  • `POST /api/organizations/{orgId}/storage/file/content`
  • `PUT /api/organizations/{orgId}/storage/file/content`
  • `GET /api/organizations/{orgId}/storage/folder/share?path=...`
  • `POST /api/organizations/{orgId}/storage/folder/share`
  • `DELETE /api/organizations/{orgId}/storage/folder/share`

Docs file-content endpoints

  • The Docs tree creates files and folders inline. File creation accepts relative nested paths, adds `.md` when a supported file name has no extension, and treats a trailing slash as a folder request.
  • `GET` reads an existing editable text file.
  • `POST` creates an empty editable text file and returns `201` with its canonical path, size, and modification time.
  • `POST` never overwrites. An existing path returns `409` with code `STORAGE_PATH_CONFLICT` and message `File already exists`.
  • `PUT` updates an existing editable text file and returns `404` when the file does not exist.
  • All three routes support `.txt`, `.md`, `.htm`, `.html`, `.csv`, `.json`, `.webmanifest`, `.yaml`, `.yml`, `.ts`, `.tsx`, `.js`, `.mjs`, `.jsx`, `.py`, `.sql`, `.css`, `.scss`, `.sh`, `.log`, `.xml`, `.toml`, `.ini`, and `.tsv`.
  • They also accept the exact extensionless basenames `LICENSE` and `LICENCE`, case-insensitively and at any folder depth. Docs opens these files in the raw code editor. Other extensionless names and near-matches remain unsupported.
  • Create paths must be valid relative storage paths. Absolute paths, parent traversal, empty segments, and invalid path forms are rejected.
  • The organization upload API enforces `STORAGE_MAX_UPLOAD_SIZE_MB` per file (default `10 MB`), the same limit as the private upload API. The Docs text file-content endpoints above are separate from file upload; current code does not show a separate size rule for them beyond file handling in the caller.

AI teammate storage permissions

  • Non-system AI teammates are still limited to folders explicitly shared with them.
  • `storage.list` returns only content inside shared folders for non-system AI teammates.
  • `storage.semantic_search` searches indexed documents inside readable shared folders for non-system AI teammates.
  • `storage.search`, `storage.read_file`, and `storage.stat_file` require readable shared-folder access for non-system AI teammates.
  • `storage.read_file` currently supports `.txt`, `.md`, `.json`, `.yaml`, `.yml`, `.htm`, `.html`, `.csv`, `.tsv`, `.xlsx`, `.ts`, `.tsx`, `.js`, `.jsx`, `.py`, `.sql`, `.css`, `.scss`, `.sh`, `.log`, `.xml`, `.toml`, `.ini`, `.docx`, text-layer `.pdf`, and model-viewable image files.
  • `storage.get_temp_file_url` also requires readable shared-folder access for non-system AI teammates.
  • `storage.write_file` and `storage.edit_file` require writable shared-folder access for non-system AI teammates.
  • `storage.write_file` and `storage.edit_file` support `.txt`, `.md`, `.htm`, `.html`, `.csv`, `.tsv`, `.json`, `.webmanifest`, `.yaml`, `.yml`, `.ts`, `.tsx`, `.js`, `.mjs`, `.jsx`, `.py`, `.sql`, `.css`, `.scss`, `.sh`, `.log`, `.xml`, `.toml`, and `.ini`, plus exact extensionless `LICENSE` and `LICENCE` basenames case-insensitively.
  • `storage.rename` requires the target path to be inside a writable shared folder for non-system AI teammates.
  • `storage.copy` requires readable source access and writable destination access for non-system AI teammates.
  • `storage.move` requires writable access to both source and destination for non-system AI teammates.
  • `storage.delete` deletes a file or recursive folder and requires write access to the parent folder for non-system AI teammates. It uses the same `.trashbin` retention behavior described above.
  • `storage.get_temp_file_url` returns a temporary HTTPS link for a readable file and is meant for downstream tool calls or external systems that need a URL instead of inline bytes.
  • Do not show temporary file URLs to end users; they are short-lived bearer-style links for tool-to-tool handoff.
  • Inside a workflow run, the link format is `{API_DOMAIN}/shared/files/{organization_id}/{run_id}/{document_id}`. It expires when the run completes or is aborted.
  • Outside a workflow run, including Hosted MCP calls, the format is `{API_DOMAIN}/shared/files/link/{link_id}`. The opaque link expires ten minutes after it is minted.
  • An expired, invalid, or unknown runless link returns `410 Temporary URL has expired`.
  • Prefer `storage.read_file` when the agent needs supported text, documents, spreadsheets, or model-viewable images, or `programming.run_code` with `storage_mounts` when the file should be processed inside a sandbox.
  • `storage.get_temp_file_url` currently returns `File not found` when the file does not yet have an `OrganizationDocument` record.
  • Temporary-link downloads support readable dotfiles and files beneath dot-prefixed folders after the path passes organization-storage containment and symlink resolution.
  • Personal Ally inherits its owning user's Storage scope when Access Group enforcement is enabled. System employees without an initiating user retain system-level Storage access; a system employee acting for an organization user follows that user's enforced group grants.

Realtime behavior

  • The backend emits `storage_file.created`, `storage_file.updated`, `storage_file.deleted`, `storage_file.renamed`, `storage_file.copied`, and `storage_file.moved`.
  • The current Storage file-list hook auto-refreshes on all six events.
  • Docs document state also listens to rename and move events so an open document can follow its new path.

Private storage API (HTTPS)

  • The canonical private API reference now lives in `private-api.md`.
  • Private storage routes are under `{api_url}/api/private/storage`.
  • Current auth is `api-key: <TOKEN>` only.

Start building your AI team