---
title: "Collection Framework Metadata"
method: POST
path: "/v2/framework/metadata"
tags: ["Framework"]
---

# Collection Framework Metadata

`POST /v2/framework/metadata`

## Query parameters

- `poll` boolean — Opt into the job-polling contract (PRO-3420): if the operation hasn't finished within its short submission wait, respond with a QueryJobResponse envelope instead of blocking until it's done. Callers that don't pass this keep the original always-blocking behavior unchanged.
- `job_id` string, nullable — Finalize a job an earlier poll=true call already submitted, once GET /v2/query-jobs/{job_id} reports it terminal, instead of submitting a new one.

## Request body

- BodyCollectionFrameworkMetadataV2FrameworkMetadataPost
  - `frameworks` FrameworkRequest[], required
    - `entities` union, required
      - FrameworkEntityRequest[]
        - `carc_id` integer, required
        - `representation` string, required
        - `entity_name` string, nullable
      - WildCardEntityRequest
        - `carc_name` string
        - `representation` string, required
    - `insight` FrameworkInsightRequest, required
      - `insight_id` integer, required
      - `insight_name` string, nullable
    - `filters` object, required
    - `aggregate` 'sum' | 'mean' | 'count_distinct', nullable
    - `events` FrameworkEventRequest[], nullable
      - `event_id` string, nullable
      - `representation` string, required
      - `event_name` string, nullable
      - `event_category_id` union
        - integer
        - string
  - `config` object, nullable

## Response `200`

Successful Response

- union
  - FrameworkCollectionResponse
    - `total_price` number, required
    - `number_of_frameworks` integer, required
    - `frameworks` FrameworkMetadataResponse[], required
      - `id` string, uuid, nullable
      - `name` string, nullable
      - `payload` FrameworkRequest, required
        - `entities` union, required
          - FrameworkEntityRequest[]
            - `carc_id` integer, required
            - `representation` string, required
            - `entity_name` string, nullable
          - WildCardEntityRequest
            - `carc_name` string
            - `representation` string, required
        - `insight` FrameworkInsightRequest, required
          - `insight_id` integer, required
          - `insight_name` string, nullable
        - `filters` object, required
        - `aggregate` 'sum' | 'mean' | 'count_distinct', nullable
        - `events` FrameworkEventRequest[], nullable
          - `event_id` string, nullable
          - `representation` string, required
          - `event_name` string, nullable
          - `event_category_id` union
            - integer
            - string
      - `refreshed_date` string, nullable, required
      - `reinstated_date` string, nullable, required
      - `reinstatement_reason` 'Ontology Change' | 'Full Refresh' | 'Vendor Reinstatement' | 'Vendor Backfill' | 'Logic Enhancement', required
      - `ontology_version` string, required
      - `price` number, required
      - `table_max_date` string, nullable, required
      - `columns` SchemaColumn[], nullable
        - `col_name` string, required
        - `data_type` string, required
      - `data_type` string[], nullable
      - `message` string, nullable
      - `latest_framework_id` string, uuid, nullable
      - `is_superseded` boolean
  - QueryJobResponse
    - `job_id` string, required
    - `state` 'queued' | 'running' | 'done' | 'failed', required — Job lifecycle, mirroring pyca's ``QueryStatusTracker`` states. One collapsed ``queued``, with no separate "waiting for a worker" vs "waiting for the ConcurrencyGate" state (PRO-3278 decision): the two are indistinguishable to the person waiting. Be aware of where the worker actually draws that line today. It calls ``mark_running`` *before* ``async_read_query``, and the gate is acquired inside that call, so a job parked on the gate reports ``running`` — with no position, no ETA, and a ``queue_wait_ms`` frozen at pickup — rather than ``queued``. Under a saturated gate, which is the condition this whole system exists to make visible, that understates the wait by exactly the gate wait. Moving ``mark_running`` past gate admission is a change to the worker (PRO-3281), tracked separately. An enum rather than a bare ``str`` so a state pyca adds later surfaces as a loud, explicit failure in ``QueryJobService`` instead of flowing through to clients that have no branch for it.
    - `position` integer, nullable — 1-indexed place in the queue, while queued.
    - `eta_seconds` number, nullable — Estimated seconds until this job finishes, from recent completion durations. None while no completions have been recorded yet.
    - `queue_wait_ms` integer — Time spent queued. Still climbing while queued; frozen at the real wait once the job starts running.
    - `poll_after_ms` integer, nullable — How long the client should wait before polling again. Absent on a terminal state, which needs no further polling.
    - `error` string, nullable
    - `result` QueryJobResult — A finished job's rows, in the same column/row *structure* as ``SQLQueryResponse`` — columns plus a row matrix — so PRO-3283's SQL editor migration is a rename rather than a reshape. Note the ``type`` caveat above: the structure translates, the type vocabulary does not. No ``applied_limit``/``limit_injected`` here: those describe a caller's own SQL rewrite, which happens before submission, not anything the job knows about. ``truncated`` also isn't here -- see ``QueryJobResponse.truncated`` instead: a done job's *cached* result can now be a worker-applied byte-payload trim (not just a caller's own rewrite), so that flag belongs on the envelope wrapping this, not on the row/column structure itself.
      - `columns` QueryJobColumn[], required
        - `name` string, required
        - `type` string, nullable
      - `rows` array[], required
        - unknown[]
          - unknown
      - `row_count` integer, required
    - `result_json` unknown
    - `truncated` boolean — True if `result`'s rows are a prefix of the real query result -- the worker applied the same byte-payload trim run_query's own sync path uses (see trim_rows_for_payload) before caching, because the result exceeded MAX_PAYLOAD_BYTES. Lives on this envelope rather than QueryJobResult itself: QueryJobResult documents a done job's cached result as always complete, which is no longer true for a trimmed sql_editor job (bugbot finding: 'Async SQL trim hides truncation'), and callers already check SQLQueryResponse.truncated on the sync path -- this mirrors it.

## Other responses

- `404` — Not Found
- `422` — Validation Error

## Changes

- **2026-09-03** `475c0fdb4aaf` — 4 breaking, 3 info
  - the response's body type changed from `object` to no type for status `200`
  - removed the required property `frameworks` from the response with the `200` status
  - removed the required property `number_of_frameworks` from the response with the `200` status
  - removed the required property `total_price` from the response with the `200` status
  - …3 more
- **2026-07-31** `1d445d2a69b5` — 2 info
  - added the optional property `frameworks/items/is_superseded` to the response with the `200` status
  - added the optional property `frameworks/items/latest_framework_id` to the response with the `200` status
- **2026-07-26** `6ba1ca74e6c7` — 2 info
  - added the new optional request property `frameworks/items/events`
  - added the optional property `frameworks/items/payload/events` to the response with the `200` status

[Change history](https://skmtc.dev/carbonarc/apis/carbon-arc-api-documentation/changes/v2/framework/metadata/post.md)

---

[API](https://skmtc.dev/carbonarc/apis/carbon-arc-api-documentation.md) · [All operations](https://skmtc.dev/carbonarc/apis/carbon-arc-api-documentation/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/carbonarc/carbon-arc-api-documentation/revisions/475c0fdb4aaf/schema)
