---
title: "Preview one draft panel"
method: POST
path: "/api/v1/ackdb/observe/dashboards/preview"
tags: ["ackDB"]
---

# Preview one draft panel

`POST /api/v1/ackdb/observe/dashboards/preview`

## Request

- Base URL: `http://localhost:3009`
- URL: `http://localhost:3009/api/v1/ackdb/observe/dashboards/preview`
- Auth: HTTP bearer

Validate and RUN one panel without persisting anything. Unlike a saved-panel data read (which trusts the save-time gate) this validates HERE, including the caller's access to the pinned segment. The response is the same envelope `/data` returns; the cache is bypassed. Rate limited to 60 previews per minute per CALLER (service/static-key callers share one bucket): this door runs uncached SQL, including a `sql` panel's statement.

## Request body

- object
  - `panel` AckdbDashboardPanel, required: One panel. `segmentId` is REQUIRED for every type except `sql`, which forbids it. The pinned segment must be a BASE segment: a derived segment stores only the filter on top of its base, so a panel compiling it alone would answer for the wrong population (refused at save/preview, fails closed at read).
    - `key` string, required: Unique within the dashboard.
    - `title` string, required
    - `type` 'kpi' | 'breakdown' | 'timeseries' | 'table' | 'feed' | 'sql', required
    - `segmentId` string
    - `spec` union, required: Per type.
      - AckdbDashboardKpiSpec: One number. `count` is the segment's row count and takes NO field; every other agg requires a numeric catalog FIELD KEY (numeric traits/attributes, `sc__` score columns, `evc__` per-event-type counts, `emf__` promoted event fields). Contact-aggregate (#214) and event-aggregate (#413) columns are inline column specs, not field keys, so they cannot be named here.
        - `agg` 'count' | 'sum' | 'avg' | 'min' | 'max', required
        - `field` string: Required unless agg is `count`, forbidden when it is.
      - AckdbDashboardBreakdownSpec: Group the segment's rows by one field and reduce each group to a number. A `date` groupBy is bucketed to the day; a NULL label is a real group ("unset"), never dropped.
        - `groupBy` string, required: A groupable catalog field: string, enum, boolean, date, score label, owner, identity-link column. Numeric fields are refused.
        - `agg` 'count' | 'sum' | 'avg' | 'min' | 'max', required
        - `field` string: Required unless agg is `count`.
        - `topN` integer: Default 10.
        - `sort` 'value_desc' | 'value_asc' | 'label_asc': Default value_desc.
        - `othersBucket` boolean: Fold the remainder into one exact `others` entry. Default false.
      - AckdbDashboardTimeseriesSpec: One dense point per bucket. `bucket` x `windowDays` must yield at most 366 points (so a `day` bucket caps at 366 days), rejected at save/preview otherwise.
        - `metric` union, required: Duck-typed union: an EVENT metric, a SCORE metric, or the MEMBERSHIP metric.
          - object: The segment members' own entity_events. `eventType` is the BARE name (resolved to the stored source-prefixed form, #355). `sum` needs a promoted numeric metadata `field`; `sum`/`distinct_contacts` need an aggregatable source on the grain (company today, #413). Zero-filled server-side.
            - `source` string, required
            - `eventType` string, required
            - `agg` 'count' | 'sum' | 'distinct_contacts', required
            - `field` string
          - object: The AVG as-of score of segment members per month, read from the #411 `score.changed` stream. Month bucket only, at most 12 months, company or person grain, and the definition must exist and be ENABLED on that grain (else SCORE_NOT_FOUND). A month with no scored member is a null gap, never a fabricated 0.
            - `scoreKey` string, required
          - object: Segment size over time: anchored at the exact live count and walked BACKWARDS through the S1 enter/exit log. Honest approximation - history starts at the segment's sync bootstrap and an enter+exit inside one diff interval cancels out. SYNC_UNAVAILABLE without the sync tables.
            - `membership` true, required
        - `bucket` 'day' | 'week' | 'month', required
        - `windowDays` integer, required
      - AckdbDashboardTableSpec: The segment results rails with a columns/sort override (the pivot override contract). Omitted columns/sort inherit the segment's own; rows come back identical to GET /observe/segments/{id}/results.
        - `columns` object[]: SegmentColumn[].
        - `sort` object[]: SegmentSort[] ({ field, dir }).
        - `limit` integer: Default 10.
      - AckdbDashboardFeedSpec: Recent membership changes for the pinned segment, from the S1 change log. `reenter` is reported (and filtered) as `enter`.
        - `kinds` string[]: Default both.
        - `limit` integer: Default 20.
        - `windowDays` integer: Optional recency cut.
      - AckdbDashboardSqlSpec: Escape hatch. A single read-only SELECT on the `ackdb_readonly` role (BEGIN READ ONLY + statement timeout + row/byte caps + extended wire protocol). Segment-less by design; never cached. No bind params in this build.
        - `sql` string, required
        - `columns` object[], required: Declared by the author so the console renders without sniffing.
          - `key` string, required
          - `label` string, required
          - `shape` 'string' | 'number' | 'currency' | 'date' | 'boolean', required
        - `maxRows` integer: Default 1000.
    - `viz` union: A validated visualization mapping for a SQL panel. Omit `viz` to retain the declared-columns table.
      - AckdbDashboardSqlLineViz: Render ordered SQL rows as a line chart. `xKey` names a declared date/string/number column; `yKey` names a declared number/currency column. Row order is the query order.
        - `type` 'line', required
        - `xKey` string, required
        - `yKey` string, required
        - `valueLabel` string: Optional tooltip noun; defaults to the y column label.
      - AckdbDashboardSqlCategoryViz: Render SQL rows as horizontal bars or a pie. `labelKey` names any declared column; `valueKey` must name a number/currency column. topN defaults to all returned rows; `othersBucket` combines the remainder.
        - `type` 'bar' | 'pie', required
        - `labelKey` string, required
        - `valueKey` string, required
        - `topN` integer
        - `othersBucket` boolean
    - `degraded` boolean: SERVER-OWNED, read-only. Set by the scrub paths (segment/source/score deletion); a degraded panel returns its degraded envelope and never runs. A request that CARRIES this field (or `degradedReason`) is rejected per panel: it would otherwise let a caller store a panel that skipped the segment-access gate, or un-disarm a panel a scrub disabled. Re-point a degraded panel at a live, accessible segment to revive it; that edit re-runs the full gate and clears the flag.
    - `degradedReason` string

## Response `200`

The panel envelope (data | degraded | error).

- union: A panel's whole answer, always HTTP 200 for a data-shaped outcome. EXACTLY one arm: `data` (it ran), `degraded` (a scrub path disarmed it, or its segment is gone: no data, no 400), or `error` (timeout, read-only guard rejection, stale field - isolated to this panel).
  - object
    - `data` union, required
      - AckdbKpiData: One number. An empty segment and a NULL aggregate both read null ("nothing to average", not 0).
        - `value` number, nullable, required
      - AckdbBreakdownData
        - `groups` object[], required: At most topN, ordered per the spec. A null label is a real group ("unset"), never dropped.
          - `label` string, nullable, required
          - `value` number, nullable, required
        - `total` number, nullable, required: The AGGREGATE across ALL groups, pre-topN, in the same unit as `value` - NOT a row count (it is a row count only when agg is `count`). This is what a percent-of-total label needs.
        - `groupCount` integer, required: How many groups exist in total.
        - `others` object, nullable, required: Present only with othersBucket:true AND more groups than were returned. Exact, derived from the two window functions.
          - `value` number
          - `groupCount` integer
      - AckdbTimeseriesData: Dense: one point per bucket, `t` is the bucket START (ISO-8601), ascending. Event and membership values are zero-filled numbers; a score month with no scored member is null.
        - `buckets` object[], required
          - `t` string, date-time, required
          - `value` number, nullable, required
      - AckdbTableData: Rows verbatim from the grain's row compiler - byte-identical to GET /observe/segments/{id}/results.
        - `rows` object[], required
        - `columns` string[], required: The projected keys in projection order (base columns included).
      - AckdbFeedData
        - `changes` object[], required
          - `seq` integer, required
          - `id` string, required: Member id: entity_id / contact_id / person_id per the segment's grain.
          - `kind` 'enter' | 'exit', required: A stored `reenter` is reported as `enter`.
          - `at` string, date-time, required
          - `name` string, nullable, required
          - `domain` string, nullable: Company grain only.
        - `asOfSeq` integer, required: The segment sync seq this page was read at.
        - `historySince` string, date-time, nullable, required: The segment's sync bootstrap time. The feed structurally cannot show anything older - caption it so an empty feed does not read as "nothing happened".
      - AckdbSqlData
        - `columns` object[], required: Echoed from the spec.
        - `rows` object[], required
  - object
    - `degraded` true, required
    - `reason` string, required
  - object
    - `error` object, required: PANEL_COMPILE | SCORE_NOT_FOUND | SYNC_UNAVAILABLE | QUERY_ERROR | QUERY_TIMEOUT | ...
      - `code` string, required
      - `message` string, required

## Other responses

- `400`: The panel is malformed, names an unknown field, or pins an invisible/derived segment.
- `429`: Preview rate limit exceeded (60/min per caller). Carries `Retry-After`.

---

[API](https://skmtc.dev/floqer/apis/floqer-public-api.md) - [All operations](https://skmtc.dev/floqer/apis/floqer-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/floqer/apis/floqer-public-api/revisions/fce50dc07b20?raw)
