---
title: "Create Spreadsheet"
method: POST
path: "/api/v1/spreadsheets"
tags: ["spreadsheets"]
---

# Create Spreadsheet

`POST /api/v1/spreadsheets`

Create a spreadsheet as the signed-in person.

## Request body

- SpreadsheetCreate
  - `title` string, required
  - `body` SpreadsheetBodyInput, required
    - `sources` WidgetBlockInput[]
      - `id` string, required
      - `title` string, nullable
      - `viz_type` 'line' | 'bar' | 'pie' | 'number' | 'pivot' | 'logs_table' | 'heatmap', required
      - `config` WidgetConfig, required — The query half of a persisted ``Widget.config``. A widget embeds a ``ReportQuery`` rather than naming one, so this is that same query MINUS its identity (``key``/``version``) and minus ``time_dimension`` (which the referenced measures own and import validation pins — a widget never restates it). Same field TYPES throughout, so the two cannot describe different vocabularies. ``extra="allow"`` is deliberate and is the line this model draws: * The QUERY half is strict. A bad ``group_by`` or an unresolvable ref produces a wrong number silently, on a human's screen, inside a report an agent said was correct. That must fail at the boundary. * PRESENTATION keys (``show_na``, ``table_orientation``, ``bar_mode``, ``row_groups``, ``column_colors``, ``group_colors``, ``sticky_first_column``, ``legendPosition`` — note the one camelCase legacy key) ride along unmodelled. Locking them down would 422 real stored configs to protect against a wrong COLOUR, which is not the failure mode worth guarding. Near-miss metric keys (``metric_ref`` for ``metric_refs``) and catalog-shaped entries are coerced by :func:`coerce_widget_config_metric_refs` before the query half is validated, so a find_metric hit pasted verbatim is accepted. Nested ``config.metric_refs`` / ``spec.metric_refs`` hoist the same way (#2457). A config that still has no metrics after that rewrite is rejected.
        - `metric_refs` MetricRef[]
          - `key` string, required
          - `category` 'standard' | 'custom' | 'formula', required
          - `outcome` 'pass' | 'fail', nullable
          - `label` string, nullable
          - `is_after_hours` boolean, nullable
          - `population_filters` string[], nullable
          - `buckets` AttributionBucket[], nullable
          - `view_id` string, uuid, nullable
        - `aggregation` AggregationSpec — How to aggregate the metric values.
          - `type` 'count' | 'sum' | 'avg' | 'pass_rate' | 'breakdown'
          - `period` 'minute' | 'hour' | 'day' | 'week' | 'month' | 'total' | 'auto'
        - `group_by` string[]
        - `filters` WidgetIntrinsicFilters — The filters a saved query OWNS — ``WidgetFilters`` minus the globals and the date range. Widget-intrinsic filters describe what the widget *is* ("bookings only", "AI-handled conversations"); global dimensions describe what the viewer is currently looking at. Only the first kind may be persisted. Field-for-field equality with ``WidgetFilters`` (minus ``GLOBAL_DIM_KEYS`` and ``TIME_RESOLUTION_KEYS``) is asserted by ``test_intrinsic_filters_match_widget_filters`` — written out explicitly rather than derived with ``create_model`` because a dynamically built model is not statically typeable, and this file is the contract an agent reads out of the OpenAPI schema. ``extra="forbid"`` is the point of the model, not a detail. Pydantic's default would IGNORE a global dimension passed here — silently dropping it, which is the same failure mode as the client's ``console.warn`` strip: the caller believes the filter was saved and it was not. An agent especially must be told, so it learns the rule instead of re-sending it.
          - `matrix_agent_refs` string[], nullable
          - `buckets` AttributionBucket[], nullable
          - `dispositions` string[], nullable
          - `was_transferred` boolean, nullable
          - `transfer_failed` boolean, nullable
          - `has_failed_transfer_attempt` boolean, nullable
          - `was_missed` boolean, nullable
          - `was_answered` boolean, nullable
          - `is_after_hours` boolean, nullable
          - `population_filters` string[], nullable
          - `is_voicemail` boolean, nullable
          - `has_first_response` boolean, nullable
          - `has_ended` boolean, nullable
          - `transfer_destination` string, nullable
          - `transfer_desk` string, nullable
          - `failed_transfer_desk` string, nullable
          - `country` string, nullable
          - `selected_language` string, nullable
          - `language_selection_source` string, nullable
          - `countries` string[], nullable
          - `countries_exclude` string[], nullable
          - `outcome` string, nullable
          - `has_booking_intent` boolean, nullable
          - `has_booking` boolean, nullable
          - `booking_made` boolean, nullable
          - `local_hour_of_day` integer, nullable
          - `local_day_of_week` integer, nullable
          - `tag_ids` string[], nullable
          - `attribution_tiers` string[], nullable
          - `metric_filters` MetricFilter[], nullable
            - `metric_id` string, required
            - `outcome` 'pass' | 'fail' | 'any' | 'na', nullable
            - `categorical_value` string, nullable
          - `standard_filters` StandardColumnFilter[], nullable
            - `field` string, required
            - `value` string, required
        - `result_shape` 'scalar' | 'timeseries' | 'breakdown' | 'pivot' | 'matrix' | 'records', nullable
        - `metric_filters` MetricFilter[]
          - `metric_id` string, required
          - `outcome` 'pass' | 'fail' | 'any' | 'na', nullable
          - `categorical_value` string, nullable
        - `matrix_agent_refs` string[]
        - `sources` string[], nullable
        - `records_subject` 'voice' | 'text' | 'email' | 'booking', nullable
        - `column_rollups` ColumnRollup[]
          - `label` string, required
          - `buckets` AttributionBucket[], required
      - `start_date` string, date, required
      - `end_date` string, date, required
      - `property_ids` string[], nullable
      - `property_ids_exclude` string[], nullable
      - `line` integer, required
    - `sheets` SpreadsheetSheet[], required
      - `name` string, required
      - `cells` object
      - `column_widths` object
  - `folder_id` string, uuid, nullable

## Response `201`

Successful Response

- SpreadsheetRead
  - `id` string, uuid, required
  - `title` string, required
  - `state` 'streaming' | 'draft' | 'edited' | 'approved' | 'sent' | 'archived', required
  - `kind` 'report' | 'document' | 'table' | 'spreadsheet' | 'visualization', required — What a document IS, derived from its body — never declared by an author. Orthogonal to :class:`ArtifactType`, which says which product surface owns the row. For a ``document`` the kind says whether it reads as a written report, plain prose, a single table or a single chart, and :func:`app.artifacts.public.doc_body.classify_doc_kind` is the only thing that decides. ``spreadsheet`` is reserved for a real spreadsheet artifact (sheets, cells, formulas — PLAN-spreadsheet-artifact.md). A Doc that is one table is a ``table``: two different things must not share one name in the gallery.
  - `folder_id` string, uuid, nullable, required
  - `conversation_id` string, uuid, nullable, required
  - `version_number` integer, required
  - `last_author` 'agent' | 'user', required
  - `last_author_user_id` string, uuid, nullable, required
  - `created_by_user_id` string, uuid, nullable, required
  - `period` DocPeriod, required — Min/max over the doc's widget fences — the list's subtitle.
    - `start_date` string, date, required
    - `end_date` string, date, required
  - `grades` Grades, required
    - `audited` integer, required
    - `cited` integer, required
    - `unverified` integer, required
  - `published_version` integer, nullable, required
  - `preview_text` string, required
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `preview` SpreadsheetPreview, required — The top-left of the first sheet, formatted, for a card thumbnail (D18). Stored at save so a chat card or gallery card never runs a source query.
    - `cells` PreviewCell[], required
      - `ref` string, required
      - `text` string, required
      - `numeric` boolean, required
      - `style` CellStyle, required
        - `role` 'body' | 'title' | 'caption' | 'heading' | 'header' | 'note'
        - `bold` boolean
        - `italic` boolean
        - `align` 'auto' | 'left' | 'center' | 'right'
        - `wrap` boolean
        - `fill` 'none' | 'accent' | 'muted'
    - `column_widths` object, required
  - `body` SpreadsheetBodyOutput, required
    - `sources` WidgetBlockOutput[]
      - `id` string, required
      - `title` string, nullable
      - `viz_type` 'line' | 'bar' | 'pie' | 'number' | 'pivot' | 'logs_table' | 'heatmap', required
      - `config` WidgetConfig, required — The query half of a persisted ``Widget.config``. A widget embeds a ``ReportQuery`` rather than naming one, so this is that same query MINUS its identity (``key``/``version``) and minus ``time_dimension`` (which the referenced measures own and import validation pins — a widget never restates it). Same field TYPES throughout, so the two cannot describe different vocabularies. ``extra="allow"`` is deliberate and is the line this model draws: * The QUERY half is strict. A bad ``group_by`` or an unresolvable ref produces a wrong number silently, on a human's screen, inside a report an agent said was correct. That must fail at the boundary. * PRESENTATION keys (``show_na``, ``table_orientation``, ``bar_mode``, ``row_groups``, ``column_colors``, ``group_colors``, ``sticky_first_column``, ``legendPosition`` — note the one camelCase legacy key) ride along unmodelled. Locking them down would 422 real stored configs to protect against a wrong COLOUR, which is not the failure mode worth guarding. Near-miss metric keys (``metric_ref`` for ``metric_refs``) and catalog-shaped entries are coerced by :func:`coerce_widget_config_metric_refs` before the query half is validated, so a find_metric hit pasted verbatim is accepted. Nested ``config.metric_refs`` / ``spec.metric_refs`` hoist the same way (#2457). A config that still has no metrics after that rewrite is rejected.
        - `metric_refs` MetricRef[]
          - `key` string, required
          - `category` 'standard' | 'custom' | 'formula', required
          - `outcome` 'pass' | 'fail', nullable
          - `label` string, nullable
          - `is_after_hours` boolean, nullable
          - `population_filters` string[], nullable
          - `buckets` AttributionBucket[], nullable
          - `view_id` string, uuid, nullable
        - `aggregation` AggregationSpec — How to aggregate the metric values.
          - `type` 'count' | 'sum' | 'avg' | 'pass_rate' | 'breakdown'
          - `period` 'minute' | 'hour' | 'day' | 'week' | 'month' | 'total' | 'auto'
        - `group_by` string[]
        - `filters` WidgetIntrinsicFilters — The filters a saved query OWNS — ``WidgetFilters`` minus the globals and the date range. Widget-intrinsic filters describe what the widget *is* ("bookings only", "AI-handled conversations"); global dimensions describe what the viewer is currently looking at. Only the first kind may be persisted. Field-for-field equality with ``WidgetFilters`` (minus ``GLOBAL_DIM_KEYS`` and ``TIME_RESOLUTION_KEYS``) is asserted by ``test_intrinsic_filters_match_widget_filters`` — written out explicitly rather than derived with ``create_model`` because a dynamically built model is not statically typeable, and this file is the contract an agent reads out of the OpenAPI schema. ``extra="forbid"`` is the point of the model, not a detail. Pydantic's default would IGNORE a global dimension passed here — silently dropping it, which is the same failure mode as the client's ``console.warn`` strip: the caller believes the filter was saved and it was not. An agent especially must be told, so it learns the rule instead of re-sending it.
          - `matrix_agent_refs` string[], nullable
          - `buckets` AttributionBucket[], nullable
          - `dispositions` string[], nullable
          - `was_transferred` boolean, nullable
          - `transfer_failed` boolean, nullable
          - `has_failed_transfer_attempt` boolean, nullable
          - `was_missed` boolean, nullable
          - `was_answered` boolean, nullable
          - `is_after_hours` boolean, nullable
          - `population_filters` string[], nullable
          - `is_voicemail` boolean, nullable
          - `has_first_response` boolean, nullable
          - `has_ended` boolean, nullable
          - `transfer_destination` string, nullable
          - `transfer_desk` string, nullable
          - `failed_transfer_desk` string, nullable
          - `country` string, nullable
          - `selected_language` string, nullable
          - `language_selection_source` string, nullable
          - `countries` string[], nullable
          - `countries_exclude` string[], nullable
          - `outcome` string, nullable
          - `has_booking_intent` boolean, nullable
          - `has_booking` boolean, nullable
          - `booking_made` boolean, nullable
          - `local_hour_of_day` integer, nullable
          - `local_day_of_week` integer, nullable
          - `tag_ids` string[], nullable
          - `attribution_tiers` string[], nullable
          - `metric_filters` MetricFilter[], nullable
            - `metric_id` string, required
            - `outcome` 'pass' | 'fail' | 'any' | 'na', nullable
            - `categorical_value` string, nullable
          - `standard_filters` StandardColumnFilter[], nullable
            - `field` string, required
            - `value` string, required
        - `result_shape` 'scalar' | 'timeseries' | 'breakdown' | 'pivot' | 'matrix' | 'records', nullable
        - `metric_filters` MetricFilter[]
          - `metric_id` string, required
          - `outcome` 'pass' | 'fail' | 'any' | 'na', nullable
          - `categorical_value` string, nullable
        - `matrix_agent_refs` string[]
        - `sources` string[], nullable
        - `records_subject` 'voice' | 'text' | 'email' | 'booking', nullable
        - `column_rollups` ColumnRollup[]
          - `label` string, required
          - `buckets` AttributionBucket[], required
      - `start_date` string, date, required
      - `end_date` string, date, required
      - `property_ids` string[], nullable
      - `property_ids_exclude` string[], nullable
    - `sheets` SpreadsheetSheet[], required
      - `name` string, required
      - `cells` object
      - `column_widths` object
  - `sources` object, required
  - `sheets` EvaluatedSheetRead[], required
    - `name` string, required
    - `cells` object, required

## Other responses

- `403` — Error
- `404` — Error
- `422` — A source cannot run, or a cell is invalid

## Changes

> 28 revisions in range; 1 not diffed.

- **2026-09-24** `4df7b22a7dae` — 1 breaking, 1 info
  - removed the enum value `transfer_target` of the request property `body/sources/items/config/group_by/items/`
  - removed the `transfer_target` enum value from the `body/sources/items/config/group_by/items/` response property for the response status `201`
- **2026-09-23** `ae7627cd46da` — 2 warning, 10 info
  - added the new `booking` enum value to the `body/sources/items/config/sources/anyOf[subschema #1]/items/` response property for the response status `201`
  - added the new `transfer_desk` enum value to the `body/sources/items/config/group_by/items/` response property for the response status `201`
  - added the new optional request property `body/sources/items/config/filters/anyOf[subschema #1: WidgetIntrinsicFilters]/failed_transfer_desk`
  - added the new optional request property `body/sources/items/config/filters/anyOf[subschema #1: WidgetIntrinsicFilters]/has_failed_transfer_attempt`
  - …8 more
- …earlier changes not shown

[Full history](https://skmtc.dev/getanana/apis/cleon-api/changes/api/v1/spreadsheets/post.md)

---

[API](https://skmtc.dev/getanana/apis/cleon-api.md) · [All operations](https://skmtc.dev/getanana/apis/cleon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/getanana/apis/cleon-api/revisions/6b978a517225?raw)
