> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# folder_metadata

> Orient in a workspace directory: counts, category mix, and one page of files.

**Cost class: fast.**

Progressive by design — explore the corpus in bounded steps instead of one
dump:

1. Call with the defaults (non-recursive) on ``/``: the directory's own
   files plus a summary per subdirectory (subtree file count, top
   categories, failed-ingestion count) — enough to pick a descent target.
2. Descend by calling again with ``directory=<subdirectory>``, or narrow
   with ``category=<name>`` (an exact category from the response),
   ``ingestion_status=<status>`` (e.g. ``failed`` to list exactly the
   files the failure counts advertised), ``kind=<lane>`` (``table`` for
   every spreadsheet, ``document``, ``media``), or ``name_contains=``
   (case-insensitive filename/path substring) — counts and listing both
   narrow, and the filters compose. Because the narrowed names come from
   *subtree* censuses, any narrow implies ``recursive`` unless
   ``recursive`` is sent explicitly.
3. ``file_metadata`` on candidate files, then ``read_file`` / ``qa_file``
   to act on them.

``name_contains`` finds files by what they are *called*; the search tools
find them by what they *contain* — and only a listing can find a file
whose ingestion failed, since it was never indexed.

``include_tables=true`` adds a **table census**: one row per queryable tab
in scope (path, component, rows x columns) plus ``shared_columns`` — column
names appearing in several tables with each side's profiled stats,
**unverified join candidates** to check with one query-tables call. The one
orientation call that replaces a file-metadata call per file; slower than a
bare call (it reads ingestion profiles), so use it once and then work
per-file. Implies ``recursive`` the way the narrows do.

A ``directory`` that names a file is refused with ``invalid_request``
pointing at ``file_metadata``; an unfiltered call that matches nothing
answers with a ``hint`` naming the nearest existing ancestor and its
subdirectories, so a typo is never a dead end.

``file_entries`` is the listing — each entry carries ingestion status,
size, category, and kind (ingestion lane), so a folder can be triaged
without per-file calls. It comes one ``page_size`` page at a time
(``files_truncated`` reports a cut); resume with ``start_after=<last
path>``, or better, narrow the scope. ``recursive=true`` still returns
whole-subtree counts and listing when a full census is genuinely needed.
Everything is filtered by both the caller's access predicate and the
requested directory — the two compose as an intersection.



## OpenAPI

````yaml /openapi.json post /v1/workspaces/{workspace_id}/tools/folder-metadata
openapi: 3.1.0
info:
  description: >-
    Document intelligence: parse, split, classify, extract, and ground files, or
    build a searchable workspace. Authenticate with ``X-API-Key``.
  title: NDI Platform API
  version: 0.1.0
servers:
  - url: https://ndi-api.nace.ai
security: []
tags: []
paths:
  /v1/workspaces/{workspace_id}/tools/folder-metadata:
    post:
      tags:
        - tools
      summary: folder_metadata
      description: >-
        Orient in a workspace directory: counts, category mix, and one page of
        files.


        **Cost class: fast.**


        Progressive by design — explore the corpus in bounded steps instead of
        one

        dump:


        1. Call with the defaults (non-recursive) on ``/``: the directory's own
           files plus a summary per subdirectory (subtree file count, top
           categories, failed-ingestion count) — enough to pick a descent target.
        2. Descend by calling again with ``directory=<subdirectory>``, or narrow
           with ``category=<name>`` (an exact category from the response),
           ``ingestion_status=<status>`` (e.g. ``failed`` to list exactly the
           files the failure counts advertised), ``kind=<lane>`` (``table`` for
           every spreadsheet, ``document``, ``media``), or ``name_contains=``
           (case-insensitive filename/path substring) — counts and listing both
           narrow, and the filters compose. Because the narrowed names come from
           *subtree* censuses, any narrow implies ``recursive`` unless
           ``recursive`` is sent explicitly.
        3. ``file_metadata`` on candidate files, then ``read_file`` /
        ``qa_file``
           to act on them.

        ``name_contains`` finds files by what they are *called*; the search
        tools

        find them by what they *contain* — and only a listing can find a file

        whose ingestion failed, since it was never indexed.


        ``include_tables=true`` adds a **table census**: one row per queryable
        tab

        in scope (path, component, rows x columns) plus ``shared_columns`` —
        column

        names appearing in several tables with each side's profiled stats,

        **unverified join candidates** to check with one query-tables call. The
        one

        orientation call that replaces a file-metadata call per file; slower
        than a

        bare call (it reads ingestion profiles), so use it once and then work

        per-file. Implies ``recursive`` the way the narrows do.


        A ``directory`` that names a file is refused with ``invalid_request``

        pointing at ``file_metadata``; an unfiltered call that matches nothing

        answers with a ``hint`` naming the nearest existing ancestor and its

        subdirectories, so a typo is never a dead end.


        ``file_entries`` is the listing — each entry carries ingestion status,

        size, category, and kind (ingestion lane), so a folder can be triaged

        without per-file calls. It comes one ``page_size`` page at a time

        (``files_truncated`` reports a cut); resume with ``start_after=<last

        path>``, or better, narrow the scope. ``recursive=true`` still returns

        whole-subtree counts and listing when a full census is genuinely needed.

        Everything is filtered by both the caller's access predicate and the

        requested directory — the two compose as an intersection.
      operationId: folder_metadata_v1_workspaces__workspace_id__tools_folder_metadata_post
      parameters:
        - description: Workspace identifier.
          in: path
          name: workspace_id
          required: true
          schema:
            description: Workspace identifier.
            format: uuid
            title: Workspace Id
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FolderMetadataRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FolderMetadataResponse'
          description: Successful Response
        '504':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Gateway Timeout
        default:
          content:
            application/json:
              example:
                error:
                  code: invalid_request
                  detail: null
                  message: >-
                    Request body has extra fields that this operation does not
                    accept.
                  request_id: req-01j9k2n3p4q5r6s7t8v9
                  retryable: false
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: >-
            Typed error envelope used by every 4xx and 5xx. ``error.code``
            distinguishes the failure; HTTP status is a consequence of the code.
      security:
        - APIKeyHeader: []
components:
  schemas:
    FolderMetadataRequest:
      additionalProperties: false
      description: >-
        Request body for ``folder-metadata``.


        Non-recursive by default: the first call on a directory orients (its own

        files plus per-subdirectory summaries), and the caller descends or
        narrows

        from there instead of receiving the whole subtree at once.


        A ``category`` or ``ingestion_status`` narrow **implies recursion**
        unless

        ``recursive`` is sent explicitly: the censuses those names come from

        (root categories, subdirectory ``top_categories`` / ``failed_files``,

        ``ingestion_summary``) count subtrees, so the natural follow-up call
        must

        scope the same subtree — otherwise narrowing by a subtree fact from the

        non-recursive root would answer an inexplicable zero. An explicit

        ``recursive=false`` alongside a narrow is honored as sent.
      properties:
        category:
          anyOf:
            - type: string
            - type: 'null'
          title: Category
        directory:
          default: /
          maxLength: 1024
          title: Directory
          type: string
        include_tables:
          default: false
          title: Include Tables
          type: boolean
        ingestion_status:
          anyOf:
            - $ref: '#/components/schemas/IngestionStatus'
            - type: 'null'
        kind:
          anyOf:
            - $ref: '#/components/schemas/IngestionLane'
            - type: 'null'
        name_contains:
          anyOf:
            - maxLength: 256
              minLength: 1
              type: string
            - type: 'null'
          title: Name Contains
        page_size:
          default: 100
          maximum: 500
          minimum: 1
          title: Page Size
          type: integer
        recursive:
          default: false
          title: Recursive
          type: boolean
        start_after:
          anyOf:
            - maxLength: 1024
              type: string
            - type: 'null'
          title: Start After
      title: FolderMetadataRequest
      type: object
    FolderMetadataResponse:
      additionalProperties: false
      description: >-
        Response body for ``folder-metadata``.


        Counts are filtered by both the caller's access predicate and the
        requested

        ``directory`` — the two compose as an intersection so the view an agent
        sees

        is neither wider than its access gate nor wider than its declared scope.


        ``file_entries`` lists the visible source files the counts cover (same

        scope and recursion as the counts), path-sorted, one page at a time

        (``page_size``); ``files_truncated`` reports a cut page — resume with

        ``start_after=<last path>``, or narrow with ``category=`` / a
        subdirectory.

        Source paths, never ingestion derivatives: derivative paths are reported
        by

        ``GET /v1/workspaces/{workspace_id}/files/{file_id}/metadata``.
      properties:
        categories:
          items:
            $ref: '#/components/schemas/CategoryCount'
          title: Categories
          type: array
        coverage:
          $ref: '#/components/schemas/Coverage'
        directory:
          title: Directory
          type: string
        file_entries:
          items:
            $ref: '#/components/schemas/FolderFileEntry'
          title: File Entries
          type: array
        files_truncated:
          title: Files Truncated
          type: boolean
        hint:
          anyOf:
            - type: string
            - type: 'null'
          title: Hint
        ingestion_summary:
          additionalProperties:
            type: integer
          propertyNames:
            $ref: '#/components/schemas/IngestionStatus'
          title: Ingestion Summary
          type: object
        overall_files:
          title: Overall Files
          type: integer
        shared_columns:
          anyOf:
            - items:
                $ref: '#/components/schemas/SharedColumn'
              maxItems: 40
              type: array
            - type: 'null'
          title: Shared Columns
        shared_columns_truncated:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Shared Columns Truncated
        subdirectories:
          items:
            $ref: '#/components/schemas/SubdirectorySummary'
          title: Subdirectories
          type: array
        tables:
          anyOf:
            - items:
                $ref: '#/components/schemas/TableCensusEntry'
              maxItems: 500
              type: array
            - type: 'null'
          title: Tables
        tables_truncated:
          anyOf:
            - type: boolean
            - type: 'null'
          title: Tables Truncated
      required:
        - directory
        - overall_files
        - categories
        - subdirectories
        - ingestion_summary
        - files_truncated
        - file_entries
        - coverage
      title: FolderMetadataResponse
      type: object
    ErrorEnvelope:
      description: Every non-2xx body on ``/v1``.
      example:
        error:
          code: invalid_request
          detail: null
          message: Request body has extra fields that this operation does not accept.
          request_id: req-01j9k2n3p4q5r6s7t8v9
          retryable: false
      properties:
        error:
          $ref: '#/components/schemas/Error'
      required:
        - error
      title: ErrorEnvelope
      type: object
    IngestionStatus:
      description: >-
        The only thing that says whether a file is searchable.


        ``expired`` cannot be derived from the hash pair: retention clears

        ``ingested_hash``, so an aged-out file and a never-ingested file look

        identical. They call for different customer actions, so the status
        carries

        the distinction the hashes cannot.
      enum:
        - discovered
        - queued
        - ingesting
        - ingested
        - stale
        - expired
        - failed
        - not_required
      title: IngestionStatus
      type: string
    IngestionLane:
      description: |-
        The processing lane a file routes through — exactly one per file.

        ``DOCUMENT`` covers everything the categorize pipeline can split into
        logical documents (PDF, Office, images, text, markdown, HTML, email);
        ``TABLE`` covers spreadsheets and CSV; ``MEDIA`` covers audio and video,
        which become one speaker-attributed timeline plus sampled frames;
        ``JSON`` preserves arbitrary nested structure and may add a table
        projection for a top-level record array. The diagram lane remains an
        explicit non-goal: its extensions map to *no*
        lane, which surfaces as a typed per-file ``unsupported_file_type``
        outcome.
      enum:
        - document
        - table
        - media
        - json
      title: IngestionLane
      type: string
    CategoryCount:
      additionalProperties: false
      description: Count of files assigned to a single document category.
      properties:
        count:
          title: Count
          type: integer
        name:
          title: Name
          type: string
      required:
        - name
        - count
      title: CategoryCount
      type: object
    Coverage:
      additionalProperties: false
      description: >-
        What the access gate withheld, in counts and label names only.


        Never a name, path, or fragment of withheld content. Naming the labels

        leaks nothing the caller cannot already read from the workspace config,
        and

        it turns "some results were withheld" into an actionable request.
      properties:
        labels_required:
          description: Which labels would have been needed to see the withheld candidates.
          items:
            type: string
          title: Labels Required
          type: array
        request_access_hint:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Access Hint
        restricted_candidates:
          default: 0
          title: Restricted Candidates
          type: integer
      title: Coverage
      type: object
    FolderFileEntry:
      additionalProperties: false
      description: >-
        One visible source file in a ``folder-metadata`` listing.


        ``path`` is the uploaded source path — what the external audience
        addresses.

        ``category`` and ``kind`` (the ingestion lane) are the triage facts:
        they say

        what a file is and which content tool family applies, without a

        ``file-metadata`` call per file. ``derived_refs`` is the host's internal

        twin, never serialized (``exclude=True``, same rule as

        :class:`~ndi_service.app.platform_api.contracts.Evidence`): the agent
        face

        expands it into the derivative paths its readers accept.
      properties:
        category:
          anyOf:
            - type: string
            - type: 'null'
          description: Ledger document category, when classified.
          title: Category
        ingestion_status:
          $ref: '#/components/schemas/IngestionStatus'
        kind:
          anyOf:
            - type: string
            - type: 'null'
          description: Ingestion lane (document / table / media), when known.
          title: Kind
        path:
          title: Path
          type: string
        size_bytes:
          title: Size Bytes
          type: integer
        uploaded_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: When this file was first registered (ISO 8601).
          title: Uploaded At
      required:
        - path
        - ingestion_status
        - size_bytes
      title: FolderFileEntry
      type: object
    SharedColumn:
      additionalProperties: false
      description: >-
        A column name appearing in several tables — an UNVERIFIED join
        candidate.


        Same name and compatible stats SUGGEST a relation (equal distinct
        counts, a

        contained value range); they do not prove one. Verify with one

        ``query-tables`` join before relying on it.
      properties:
        appears_in:
          items:
            $ref: '#/components/schemas/SharedColumnOccurrence'
          title: Appears In
          type: array
        name:
          title: Name
          type: string
        table_count:
          description: >-
            How many census tables carry the column — may exceed
            len(appears_in).
          title: Table Count
          type: integer
      required:
        - name
        - table_count
        - appears_in
      title: SharedColumn
      type: object
    SubdirectorySummary:
      additionalProperties: false
      description: >-
        Orientation summary for one immediate subdirectory, covering its whole
        subtree.


        Carries enough to pick a descent target without listing the subtree: the

        file count, the most-populous categories (capped at

        FOLDER_SUBDIR_TOP_CATEGORIES), and the failed-ingestion count when
        nonzero.
      properties:
        failed_files:
          anyOf:
            - type: integer
            - type: 'null'
          description: Files in the subtree with ingestion_status=failed; absent when none.
          title: Failed Files
        name:
          title: Name
          type: string
        overall_files:
          title: Overall Files
          type: integer
        top_categories:
          items:
            $ref: '#/components/schemas/CategoryCount'
          title: Top Categories
          type: array
      required:
        - name
        - overall_files
      title: SubdirectorySummary
      type: object
    TableCensusEntry:
      additionalProperties: false
      description: 'One queryable tab in the census: where it is and how big it is.'
      properties:
        column_count:
          anyOf:
            - type: integer
            - type: 'null'
          description: Columns, when measured.
          title: Column Count
        component:
          description: >-
            The tab's public component name — pass to
            file-metadata/query-tables.
          title: Component
          type: string
        path:
          description: Uploaded source path owning the tab.
          title: Path
          type: string
        row_count:
          anyOf:
            - type: integer
            - type: 'null'
          description: Rows in the queryable table, when ingestion measured them.
          title: Row Count
        size_bytes:
          description: The owning FILE's size — per-file, so a workbook's tabs repeat it.
          title: Size Bytes
          type: integer
        table_name:
          anyOf:
            - type: string
            - type: 'null'
          description: >-
            SQL view name the tab is registered under for run-sql, when its
            profile is readable.
          title: Table Name
      required:
        - path
        - component
        - size_bytes
      title: TableCensusEntry
      type: object
    Error:
      description: The error object, per spec 12 §1.8.
      properties:
        code:
          $ref: '#/components/schemas/ErrorCode'
        detail:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          description: Structured payload, when the code carries one.
          title: Detail
        message:
          description: Human- and model-readable; says what to do differently.
          title: Message
          type: string
        request_id:
          description: Per-request correlation id; matches the ``X-Request-Id`` header.
          title: Request Id
          type: string
        retryable:
          description: Whether retrying the identical request could succeed.
          title: Retryable
          type: boolean
      required:
        - code
        - message
        - retryable
        - request_id
      title: Error
      type: object
    SharedColumnOccurrence:
      additionalProperties: false
      description: One table carrying a shared column, with the stats its profile recorded.
      properties:
        component:
          title: Component
          type: string
        data_type:
          title: Data Type
          type: string
        distinct_count:
          anyOf:
            - type: integer
            - type: 'null'
          description: Approximate; compare to row_count for key-likeness.
          title: Distinct Count
        max_value:
          anyOf:
            - type: string
            - type: 'null'
          title: Max Value
        min_value:
          anyOf:
            - type: string
            - type: 'null'
          description: Rendered and clipped; a format exemplar for id columns.
          title: Min Value
        path:
          title: Path
          type: string
        row_count:
          anyOf:
            - type: integer
            - type: 'null'
          title: Row Count
      required:
        - path
        - component
        - data_type
      title: SharedColumnOccurrence
      type: object
    ErrorCode:
      description: |-
        Every error the ``/v1`` surface can return.

        Clients must treat this as an **open** enum: a new member is an additive
        change (spec 12 §9), and a client that crashes on an unknown code makes
        every future error a breaking change.
      enum:
        - invalid_request
        - unsupported_file_type
        - file_too_large
        - invalid_schema
        - invalid_sql
        - invalid_path_prefix
        - command_not_permitted
        - batch_too_large
        - domain_validation_failed
        - default_label_required
        - wait_required_for_zero_retention
        - workspace_not_found
        - file_not_found
        - job_not_found
        - upload_not_found
        - node_not_found
        - schema_not_found
        - domain_not_found
        - session_not_found
        - access_label_not_found
        - path_conflict
        - schema_version_conflict
        - access_label_name_taken
        - workspace_name_taken
        - access_label_in_use
        - upload_expired
        - file_not_ingested
        - workspace_deleting
        - workspace_not_deletable
        - ingestion_in_progress
        - job_not_cancellable
        - confirmation_required
        - domain_not_published
        - domain_in_use
        - reserved_domain_slug
        - kg_not_built
        - kg_build_in_progress
        - session_busy
        - range_not_satisfiable
        - result_expired
        - access_denied
        - access_backstop_violation
        - access_mode_not_implemented
        - rate_limited
        - concurrency_limit_reached
        - quota_exceeded
        - corrupt_file
        - zero_byte_file
        - encrypted_file
        - parse_failed
        - ocr_failed
        - conversion_failed
        - sheetless_workbook
        - unplayable_media
        - unclassified_pages
        - job_cancelled
        - job_timeout
        - upstream_unavailable
        - internal_error
        - unauthorized
        - not_implemented
      title: ErrorCode
      type: string
  securitySchemes:
    APIKeyHeader:
      in: header
      name: X-API-Key
      type: apiKey

````