# Parameterizing a dashboard widget

Making a widget interactive (a date picker, a country dropdown, etc.) is
**two separate steps**. They live on two different objects, and the most
common mistake is conflating them.

- **Step 1 — declare the parameter on the SAVED QUERY** (`bi_save_query` /
  `bi_modify_query`). This is what `enumOptions`, `default`, and the type
  belong to.
- **Step 2 — map the parameter on the WIDGET** (`bi_update_widget`). The
  mapping says *where the value comes from*; it never re-declares the
  parameter.

If you only do Step 2, the widget has nothing to bind to. If you only do
Step 1, the query is parameterized but the widget supplies no value.

---

## Step 1 — declare parameters on the saved query

Pass a `parameters` list to `bi_save_query` (or `bi_modify_query`). Each
entry:

| field | required | notes |
|-------|----------|-------|
| `name` | yes | matches the `{{name}}` placeholder in the SQL |
| `type` | yes | one of: `text`, `text-pattern`, `number`, `enum`, `query`, `date`, `datetime-local`, `datetime-with-seconds`, `date-range`, `datetime-range`, `datetime-range-with-seconds` |
| `enumOptions` | for `enum` | list of allowed values |
| `default` | optional | pre-filled value |
| `title` | optional | label shown to the operator |
| `queryId` | for `type=query` | the saved query whose result populates the dropdown |

Rules the platform enforces (rejects with `error_code=parameter_schema_invalid`):

- Every `{{name}}` placeholder in the SQL **must** have a matching entry.
- Range types (`date-range`, `datetime-range`,
  `datetime-range-with-seconds`) use **dotted** placeholders
  `{{name.start}}` and `{{name.end}}` — not a bare `{{name}}`.
- Declaring a parameter the SQL does **not** reference is allowed (it's a
  no-op). A placeholder with **no** matching declaration is rejected.

Example:

```json
bi_save_query(
  name="Solicitudes por pais y rango de fechas",
  sql="SELECT pais, count(*) FROM solicitudes "
      "WHERE pais = {{country}} "
      "AND created_at BETWEEN {{range.start}} AND {{range.end}} "
      "GROUP BY pais",
  parameters=[
    {"name": "country", "type": "enum",
     "enumOptions": ["CO", "MX", "PE"], "default": "CO",
     "title": "Pais"},
    {"name": "range", "type": "date-range",
     "title": "Rango de fechas"}
  ]
)
```

On `bi_modify_query`: omit `parameters` to leave the existing schema
unchanged; pass `[]` to clear it.

---

## Step 2 — map the parameter on the widget

Add the widget with `bi_add_widget`, then bind its parameters with
`bi_update_widget` using `parameter_mappings`. (`bi_add_widget` does not yet
accept `parameter_mappings` directly — do the mapping in a follow-up
`bi_update_widget` call.)

`parameter_mappings` is a dict keyed by the **parameter name** (the same
`name` you declared in Step 1). Each value is **exactly one** of these three
shapes:

| mapping | meaning |
|---------|---------|
| `{"type": "widget-level"}` | the widget shows its own control; the operator sets the value per-widget |
| `{"type": "dashboard-level", "mapTo": "<slot>"}` | the value comes from a dashboard top-bar slot named `<slot>` |
| `{"type": "static-value", "value": <literal>}` | the value is pinned; no control shown |

For a `dashboard-level` mapping, the slot named in `mapTo` **must already
exist** on the dashboard (see `dashboards` help to create slots) **and** the
slot's `type` must **exactly match** the parameter's `type`.

Example — `country` reads from a shared dashboard slot, `range` is pinned:

```json
bi_update_widget(
  dashboard_id=42,
  widget_index=0,
  parameter_mappings={
    "country": {"type": "dashboard-level", "mapTo": "global_country"},
    "range":   {"type": "static-value",
                "value": {"start": "2026-01-01", "end": "2026-06-30"}}
  }
)
```

---

## Common failures

- **Mapping `type` not one of the three exact strings.** It must be
  literally `widget-level`, `dashboard-level`, or `static-value`.
- **`dashboard-level` `mapTo` slot missing or type-mismatched.** Create the
  slot first; its `type` must equal the parameter's `type`.
- **Putting `enumOptions` / `default` INSIDE the mapping.** Those belong on
  the query parameter (Step 1), never on the widget mapping (Step 2).
- **A `{{placeholder}}` with no declaration.** Every placeholder needs a
  matching `parameters` entry, or Step 1 is rejected.
