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

# Create Chart

> Add a new chart to a dashboard board with a SQL query and chart type. Supports line, bar, pie, funnel, and other visualization types.

## Overview

Creates a new chart and attaches it to a board. Returns the new chart's ID on success.

The `chart_type` field controls which other request body fields are required or validated. Funnel charts are special - their SQL is **auto-generated** from the `steps` array, so pass `"SELECT 1"` as the `query` placeholder.

***

## Request Fields

| Field         | Type          | Required    | Description                                                                                     |
| ------------- | ------------- | ----------- | ----------------------------------------------------------------------------------------------- |
| `projectId`   | string        | Yes         | Project the chart belongs to                                                                    |
| `query`       | string        | Conditional | SQL query. Required for all types except `retention`. For `funnel`, pass `"SELECT 1"`           |
| `chart_type`  | string        | Yes         | `table` · `number` · `bar` · `line` · `pie` · `stacked` · `funnel` · `user_paths` · `retention` |
| `title`       | string        | Yes         | Display name (minimum 1 character)                                                              |
| `description` | string        | No          | Optional description                                                                            |
| `x_axis`      | string        | Conditional | Column for the X axis. Required for `bar`, `line`, `stacked`                                    |
| `y_axis`      | string\[]     | Conditional | Column(s) for Y axis metrics. See per-type rules below                                          |
| `group_by`    | string        | Conditional | Column to group/stack series by. Required for `stacked`                                         |
| `steps`       | FunnelStep\[] | Conditional | Ordered funnel steps. Required for `funnel` (minimum 2)                                         |
| `settings`    | ChartSettings | No          | Type-specific configuration object                                                              |

***

## Chart Types

### `table`

Renders query results in a paginated table. No axis fields needed.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT * FROM events ORDER BY timestamp DESC LIMIT 10",
  "chart_type": "table",
  "title": "Recent Events"
}
```

***

### `number`

Renders a single scalar value (KPI card). The query **must** return exactly **1 row × 1 column**.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT COUNT(DISTINCT address) FROM events WHERE type = 'connect'",
  "chart_type": "number",
  "title": "Total Connected Wallets"
}
```

***

### `bar` and `line`

Both require `x_axis` and at least **1** column in `y_axis`.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT toDate(timestamp) AS date, countDistinct(address) AS users FROM events GROUP BY date ORDER BY date DESC LIMIT 30",
  "chart_type": "line",
  "title": "Daily Active Users",
  "x_axis": "date",
  "y_axis": ["users"]
}
```

***

### `pie`

Requires exactly **1** column in `y_axis`. `x_axis` identifies the label column.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT device, COUNT(*) AS session_count FROM sessions GROUP BY device ORDER BY session_count DESC LIMIT 10",
  "chart_type": "pie",
  "title": "Sessions by Device",
  "x_axis": "device",
  "y_axis": ["session_count"]
}
```

***

### `stacked`

Requires `x_axis`, exactly **1** column in `y_axis`, and `group_by`.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT device, browser, COUNT(*) AS session_count FROM sessions GROUP BY device, browser ORDER BY session_count DESC",
  "chart_type": "stacked",
  "title": "Sessions by Device and Browser",
  "x_axis": "device",
  "y_axis": ["session_count"],
  "group_by": "browser"
}
```

***

## Funnel Charts

Funnel charts measure step-by-step user conversion. The SQL is auto-generated from `steps`, so `query` must be the placeholder `"SELECT 1"`. At least **2 steps** are required.

### The `FunnelStep` Object

Each step in the `steps` array has the following shape:

```json theme={null}
{
  "type": "event | track | decoded_log",
  "event": "<event name>",
  "<propertyKey>": { "op": "<operator>", "value": "<value>" }
}
```

| Field           | Type                  | Required | Description                                                                                                                                                                                                                                                               |
| --------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`          | string                | Yes      | `event` - built-in events (page, connect, transaction); `track` - custom tracked events; `decoded_log` - decoded smart-contract events                                                                                                                                    |
| `event`         | string                | Yes      | Event name (e.g. `page`, `connect`, `transaction`)                                                                                                                                                                                                                        |
| `<propertyKey>` | `StepFilterCondition` | No       | One or more property filters. Any extra key on the step object is treated as a filter. Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_*`) are filtered directly; all other keys are extracted from `properties` via `JSONExtractString` |

### Filter Operators (`StepFilterCondition`)

```json theme={null}
{ "op": "<operator>", "value": "<value>" }
```

| `op`         | SQL equivalent         | Notes                                                 |
| ------------ | ---------------------- | ----------------------------------------------------- |
| `equals`     | `= 'value'`            | Exact match                                           |
| `notEquals`  | `!= 'value'`           | Inverse match                                         |
| `in`         | `IN (...)`             | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` |
| `notIn`      | `NOT IN (...)`         | Pipe-delimited value                                  |
| `gt`         | `> value`              | Numeric comparison                                    |
| `gte`        | `>= value`             | Numeric comparison                                    |
| `lt`         | `< value`              | Numeric comparison                                    |
| `lte`        | `<= value`             | Numeric comparison                                    |
| `startsWith` | `startsWith(col, 'v')` | String prefix                                         |
| `endsWith`   | `endsWith(col, 'v')`   | String suffix                                         |
| `includes`   | `like '%v%'`           | Substring match                                       |

<Tip>
  For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal
  pipe with `\|` and a literal backslash with `\\`.
</Tip>

### `settings` for Funnel Charts

| Field              | Type                   | Default    | Description                                                                                                    |
| ------------------ | ---------------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `funnelType`       | `"closed"` \| `"open"` | `"closed"` | `closed` - strict ordering, no events between steps; `open` - ordered but other events may occur between steps |
| `conversionWindow` | `ConversionWindow`     | 1 day      | Max time from Step 1 for a user to complete all steps                                                          |
| `breakdown`        | string                 | -          | Split each funnel bar by this dimension                                                                        |

#### `ConversionWindow`

```json theme={null}
{ "value": 7, "unit": "day" }
```

`unit` must be one of: `minute` · `hour` · `day` · `week` (7 days) · `month` (30 days).

#### `breakdown` values

`device` · `browser` · `os` · `referrer` · `ref` · `utm_source` · `utm_medium` · `utm_campaign` · `utm_term` · `utm_content`

The top categories are shown individually. The rest collapse into **"Others"**.

***

### Funnel Examples

#### Basic 3-step closed funnel

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Onboarding Funnel",
  "steps": [
    { "type": "event", "event": "page" },
    { "type": "event", "event": "connect" },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 7, "unit": "day" }
  }
}
```

#### With per-step property filters

Filter step 2 to MetaMask wallet connections on Ethereum mainnet:

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "MetaMask Conversion Funnel",
  "steps": [
    { "type": "event", "event": "page" },
    {
      "type": "event",
      "event": "connect",
      "rdns": { "op": "equals", "value": "io.metamask" },
      "chain_id": { "op": "equals", "value": "1" }
    },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 7, "unit": "day" }
  }
}
```

#### With breakdown by device

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Onboarding Funnel by Device",
  "steps": [
    { "type": "event", "event": "page" },
    { "type": "event", "event": "connect" },
    { "type": "event", "event": "signature" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 30, "unit": "day" },
    "breakdown": "device"
  }
}
```

#### Open funnel with multi-value `in` filter

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Mobile Onboarding Funnel",
  "steps": [
    {
      "type": "event",
      "event": "page",
      "device": { "op": "equals", "value": "mobile" }
    },
    {
      "type": "event",
      "event": "connect",
      "provider_name": { "op": "in", "value": "metamask|rainbow|coinbase" }
    },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "open",
    "conversionWindow": { "value": 30, "unit": "day" },
    "breakdown": "device"
  }
}
```

***

## User Paths Charts

Visualize how users navigate your app after a starting event. `settings.startStep` is required.

### `settings` for User Paths Charts

| Field              | Type                   | Default | Description                                                        |
| ------------------ | ---------------------- | ------- | ------------------------------------------------------------------ |
| `startStep`        | `FunnelStep`           | -       | **Required.** Event where the flow begins                          |
| `endStep`          | `FunnelStep` \| `null` | `null`  | Optional ending event. `null` = open-ended                         |
| `maxSteps`         | integer (2–10)         | `3`     | Max steps to show in the flow. Values above 10 are clamped to 10   |
| `nodesPerStep`     | integer (2–10)         | `5`     | Max unique event nodes per step. Values above 10 are clamped to 10 |
| `conversionWindow` | `ConversionWindow`     | -       | Time window to group the flow                                      |
| `filters`          | string                 | -       | JSON-encoded string of additional path filters                     |

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT anonymous_id, type AS event, timestamp FROM events WHERE timestamp >= today() - INTERVAL 30 DAY ORDER BY anonymous_id, timestamp",
  "chart_type": "user_paths",
  "title": "Post-Connect User Flow",
  "settings": {
    "startStep": { "type": "event", "event": "connect" },
    "endStep": { "type": "event", "event": "transaction" },
    "maxSteps": 5,
    "conversionWindow": { "value": 2, "unit": "week" }
  }
}
```

***

## Retention Charts

Measures how often users return over time. The `query` field is not used - pass `""` or omit it.

### `settings` for Retention Charts

| Field                  | Type                    | Description                                                                     |
| ---------------------- | ----------------------- | ------------------------------------------------------------------------------- |
| `retentionFilter`      | `FunnelStep` \| `null`  | Event that qualifies a returning visit as "retained". `null` = any event counts |
| `retentionUserFilters` | `RetentionUserFilter[]` | User-segment filters that narrow the retention cohort                           |

Each `RetentionUserFilter`:

| Field   | Type             | Description                                                              |
| ------- | ---------------- | ------------------------------------------------------------------------ |
| `field` | string           | User property (e.g. `device`, `browser`, `os`, `utm_source`, `location`) |
| `op`    | string           | Comparison operator (same set as `StepFilterCondition.op`)               |
| `value` | string \| number | Value to compare against                                                 |

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "",
  "chart_type": "retention",
  "title": "Weekly Retention - Desktop Users",
  "settings": {
    "retentionFilter": { "type": "event", "event": "transaction" },
    "retentionUserFilters": [
      { "field": "device", "op": "equals", "value": "desktop" },
      { "field": "utm_source", "op": "notEquals", "value": "direct" }
    ]
  }
}
```

For all-user retention with no filters, omit `settings`:

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "",
  "chart_type": "retention",
  "title": "Overall Retention"
}
```

***

## Validation Reference

| Chart Type   | Validation Rule                                                                          |
| ------------ | ---------------------------------------------------------------------------------------- |
| `table`      | `query` must execute successfully                                                        |
| `number`     | `query` must return exactly 1 row × 1 column                                             |
| `bar`        | `x_axis` required; `y_axis` requires ≥ 1 element                                         |
| `line`       | `x_axis` required; `y_axis` requires ≥ 1 element                                         |
| `pie`        | `y_axis` requires exactly 1 element                                                      |
| `stacked`    | `x_axis` required; `y_axis` requires exactly 1 element; `group_by` required              |
| `funnel`     | `steps` requires ≥ 2 `FunnelStep` objects; `query` must be `"SELECT 1"` or any valid SQL |
| `user_paths` | `settings.startStep` required; `query` must execute and return valid path data           |
| `retention`  | No query validation - `query` is ignored                                                 |

***

## Response

**`201 Created`** - returns the new chart's ID as a bare JSON string.

```json theme={null}
"chart_1a2b3c4d"
```

Use the returned ID with the [Get Chart](/api/boards/get-chart), [Update Chart](/api/boards/update-chart), and [Delete Chart](/api/boards/delete-chart) endpoints.

**`400 Bad Request`** - validation failed (missing required fields, invalid SQL, wrong step count, etc.). Branch on `error.code`; see [Errors](/api/errors).

```json theme={null}
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Funnel chart must have at least 2 steps",
    "doc_url": "https://docs.formo.so/api/errors#bad_request"
  }
}
```

## Overview

Creates a new chart and attaches it to a board. Returns the new chart's ID on success.

The `chart_type` field controls which other request body fields are required or validated. Funnel charts are special - their SQL is **auto-generated** from the `steps` array, so pass `"SELECT 1"` as the `query` placeholder.

***

## Request Fields

| Field         | Type          | Required    | Description                                                                                     |
| ------------- | ------------- | ----------- | ----------------------------------------------------------------------------------------------- |
| `projectId`   | string        | Yes         | Project the chart belongs to                                                                    |
| `query`       | string        | Conditional | SQL query. Required for all types except `retention`. For `funnel`, pass `"SELECT 1"`           |
| `chart_type`  | string        | Yes         | `table` · `number` · `bar` · `line` · `pie` · `stacked` · `funnel` · `user_paths` · `retention` |
| `title`       | string        | Yes         | Display name (minimum 1 character)                                                              |
| `description` | string        | No          | Optional description                                                                            |
| `x_axis`      | string        | Conditional | Column for the X axis. Required for `bar`, `line`, `stacked`                                    |
| `y_axis`      | string\[]     | Conditional | Column(s) for Y axis metrics. See per-type rules below                                          |
| `group_by`    | string        | Conditional | Column to group/stack series by. Required for `stacked`                                         |
| `steps`       | FunnelStep\[] | Conditional | Ordered funnel steps. Required for `funnel` (minimum 2)                                         |
| `settings`    | ChartSettings | No          | Type-specific configuration object                                                              |

***

## Chart Types

### `table`

Renders query results in a paginated table. No axis fields needed.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT * FROM events ORDER BY timestamp DESC LIMIT 10",
  "chart_type": "table",
  "title": "Recent Events"
}
```

***

### `number`

Renders a single scalar value (KPI card). The query **must** return exactly **1 row × 1 column**.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT COUNT(DISTINCT address) FROM events WHERE type = 'connect'",
  "chart_type": "number",
  "title": "Total Connected Wallets"
}
```

***

### `bar` and `line`

Both require `x_axis` and at least **1** column in `y_axis`.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT toDate(timestamp) AS date, countDistinct(address) AS users FROM events GROUP BY date ORDER BY date DESC LIMIT 30",
  "chart_type": "line",
  "title": "Daily Active Users",
  "x_axis": "date",
  "y_axis": ["users"]
}
```

***

### `pie`

Requires exactly **1** column in `y_axis`. `x_axis` identifies the label column.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT device, COUNT(*) AS session_count FROM sessions GROUP BY device ORDER BY session_count DESC LIMIT 10",
  "chart_type": "pie",
  "title": "Sessions by Device",
  "x_axis": "device",
  "y_axis": ["session_count"]
}
```

***

### `stacked`

Requires `x_axis`, exactly **1** column in `y_axis`, and `group_by`.

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT device, browser, COUNT(*) AS session_count FROM sessions GROUP BY device, browser ORDER BY session_count DESC",
  "chart_type": "stacked",
  "title": "Sessions by Device and Browser",
  "x_axis": "device",
  "y_axis": ["session_count"],
  "group_by": "browser"
}
```

***

## Funnel Charts

Funnel charts measure step-by-step user conversion. The SQL is auto-generated from `steps`, so `query` must be the placeholder `"SELECT 1"`. At least **2 steps** are required.

### The `FunnelStep` Object

Each step in the `steps` array has the following shape:

```json theme={null}
{
  "type": "event | track | decoded_log",
  "event": "<event name>",
  "<propertyKey>": { "op": "<operator>", "value": "<value>" }
}
```

| Field           | Type                  | Required | Description                                                                                                                                                                                                                                                               |
| --------------- | --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`          | string                | Yes      | `event` - built-in events (page, connect, transaction); `track` - custom tracked events; `decoded_log` - decoded smart-contract events                                                                                                                                    |
| `event`         | string                | Yes      | Event name (e.g. `page`, `connect`, `transaction`)                                                                                                                                                                                                                        |
| `<propertyKey>` | `StepFilterCondition` | No       | One or more property filters. Any extra key on the step object is treated as a filter. Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_*`) are filtered directly; all other keys are extracted from `properties` via `JSONExtractString` |

### Filter Operators (`StepFilterCondition`)

```json theme={null}
{ "op": "<operator>", "value": "<value>" }
```

| `op`         | SQL equivalent         | Notes                                                 |
| ------------ | ---------------------- | ----------------------------------------------------- |
| `equals`     | `= 'value'`            | Exact match                                           |
| `notEquals`  | `!= 'value'`           | Inverse match                                         |
| `in`         | `IN (...)`             | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` |
| `notIn`      | `NOT IN (...)`         | Pipe-delimited value                                  |
| `gt`         | `> value`              | Numeric comparison                                    |
| `gte`        | `>= value`             | Numeric comparison                                    |
| `lt`         | `< value`              | Numeric comparison                                    |
| `lte`        | `<= value`             | Numeric comparison                                    |
| `startsWith` | `startsWith(col, 'v')` | String prefix                                         |
| `endsWith`   | `endsWith(col, 'v')`   | String suffix                                         |
| `includes`   | `like '%v%'`           | Substring match                                       |

<Tip>
  For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal
  pipe with `\|` and a literal backslash with `\\`.
</Tip>

### `settings` for Funnel Charts

| Field              | Type                   | Default    | Description                                                                                                    |
| ------------------ | ---------------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `funnelType`       | `"closed"` \| `"open"` | `"closed"` | `closed` - strict ordering, no events between steps; `open` - ordered but other events may occur between steps |
| `conversionWindow` | `ConversionWindow`     | 1 day      | Max time from Step 1 for a user to complete all steps                                                          |
| `breakdown`        | string                 | -          | Split each funnel bar by this dimension                                                                        |

#### `ConversionWindow`

```json theme={null}
{ "value": 7, "unit": "day" }
```

`unit` must be one of: `minute` · `hour` · `day` · `week` (7 days) · `month` (30 days).

#### `breakdown` values

`device` · `browser` · `os` · `referrer` · `ref` · `utm_source` · `utm_medium` · `utm_campaign` · `utm_term` · `utm_content`

The top categories are shown individually. The rest collapse into **"Others"**.

***

### Funnel Examples

#### Basic 3-step closed funnel

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Onboarding Funnel",
  "steps": [
    { "type": "event", "event": "page" },
    { "type": "event", "event": "connect" },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 7, "unit": "day" }
  }
}
```

#### With per-step property filters

Filter step 2 to MetaMask wallet connections on Ethereum mainnet:

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "MetaMask Conversion Funnel",
  "steps": [
    { "type": "event", "event": "page" },
    {
      "type": "event",
      "event": "connect",
      "rdns": { "op": "equals", "value": "io.metamask" },
      "chain_id": { "op": "equals", "value": "1" }
    },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 7, "unit": "day" }
  }
}
```

#### With breakdown by device

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Onboarding Funnel by Device",
  "steps": [
    { "type": "event", "event": "page" },
    { "type": "event", "event": "connect" },
    { "type": "event", "event": "signature" }
  ],
  "settings": {
    "funnelType": "closed",
    "conversionWindow": { "value": 30, "unit": "day" },
    "breakdown": "device"
  }
}
```

#### Open funnel with multi-value `in` filter

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT 1",
  "chart_type": "funnel",
  "title": "Mobile Onboarding Funnel",
  "steps": [
    {
      "type": "event",
      "event": "page",
      "device": { "op": "equals", "value": "mobile" }
    },
    {
      "type": "event",
      "event": "connect",
      "provider_name": { "op": "in", "value": "metamask|rainbow|coinbase" }
    },
    { "type": "event", "event": "transaction" }
  ],
  "settings": {
    "funnelType": "open",
    "conversionWindow": { "value": 30, "unit": "day" },
    "breakdown": "device"
  }
}
```

***

## User Paths Charts

Visualize how users navigate your app after a starting event. `settings.startStep` is required.

### `settings` for User Paths Charts

| Field              | Type                   | Default | Description                                                        |
| ------------------ | ---------------------- | ------- | ------------------------------------------------------------------ |
| `startStep`        | `FunnelStep`           | -       | **Required.** Event where the flow begins                          |
| `endStep`          | `FunnelStep` \| `null` | `null`  | Optional ending event. `null` = open-ended                         |
| `maxSteps`         | integer (2–10)         | `3`     | Max steps to show in the flow. Values above 10 are clamped to 10   |
| `nodesPerStep`     | integer (2–10)         | `5`     | Max unique event nodes per step. Values above 10 are clamped to 10 |
| `conversionWindow` | `ConversionWindow`     | 2 weeks | Time window to group the flow                                      |
| `filters`          | string                 | -       | JSON-encoded string of additional path filters                     |

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "SELECT anonymous_id, type AS event, timestamp FROM events WHERE timestamp >= today() - INTERVAL 30 DAY ORDER BY anonymous_id, timestamp",
  "chart_type": "user_paths",
  "title": "Post-Connect User Flow",
  "settings": {
    "startStep": { "type": "event", "event": "connect" },
    "endStep": { "type": "event", "event": "transaction" },
    "maxSteps": 5,
    "conversionWindow": { "value": 2, "unit": "week" }
  }
}
```

***

## Retention Charts

Measures how often users return over time. The `query` field is not used - pass `""` or omit it.

### `settings` for Retention Charts

| Field                  | Type                    | Description                                                                     |
| ---------------------- | ----------------------- | ------------------------------------------------------------------------------- |
| `retentionFilter`      | `FunnelStep` \| `null`  | Event that qualifies a returning visit as "retained". `null` = any event counts |
| `retentionUserFilters` | `RetentionUserFilter[]` | User-segment filters that narrow the retention cohort                           |

Each `RetentionUserFilter`:

| Field   | Type             | Description                                                              |
| ------- | ---------------- | ------------------------------------------------------------------------ |
| `field` | string           | User property (e.g. `device`, `browser`, `os`, `utm_source`, `location`) |
| `op`    | string           | Comparison operator (same set as `StepFilterCondition.op`)               |
| `value` | string \| number | Value to compare against                                                 |

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "",
  "chart_type": "retention",
  "title": "Weekly Retention - Desktop Users",
  "settings": {
    "retentionFilter": { "type": "event", "event": "transaction" },
    "retentionUserFilters": [
      { "field": "device", "op": "equals", "value": "desktop" },
      { "field": "utm_source", "op": "notEquals", "value": "direct" }
    ]
  }
}
```

For all-user retention with no filters, omit `settings`:

```json theme={null}
{
  "projectId": "proj_abc",
  "query": "",
  "chart_type": "retention",
  "title": "Overall Retention"
}
```

***

## Validation Reference

| Chart Type   | Validation Rule                                                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `table`      | `query` must execute successfully                                                                                                                      |
| `number`     | `query` must return exactly 1 row × 1 column                                                                                                           |
| `bar`        | `x_axis` required; `y_axis` requires ≥ 1 element                                                                                                       |
| `line`       | `x_axis` required; `y_axis` requires ≥ 1 element                                                                                                       |
| `pie`        | `y_axis` requires exactly 1 element                                                                                                                    |
| `stacked`    | `x_axis` required; `y_axis` requires exactly 1 element; `group_by` required                                                                            |
| `funnel`     | `steps` requires ≥ 2 `FunnelStep` objects; `query` must be `"SELECT 1"` or any valid SQL                                                               |
| `user_paths` | `settings.startStep` required; `query` must execute and return data in the expected user-path format (columns: user identifier, event name, timestamp) |
| `retention`  | No query validation - `query` is ignored                                                                                                               |

***

## Response

**`201 Created`** - returns the new chart's ID as a bare JSON string.

```json theme={null}
"chart_1a2b3c4d"
```

Use the returned ID with the [Get Chart](/api/boards/get-chart), [Update Chart](/api/boards/update-chart), and [Delete Chart](/api/boards/delete-chart) endpoints.

**`400 Bad Request`** - validation failed (missing required fields, invalid SQL, wrong step count, etc.). Branch on `error.code`; see [Errors](/api/errors).

```json theme={null}
{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Funnel chart must have at least 2 steps",
    "doc_url": "https://docs.formo.so/api/errors#bad_request"
  }
}
```


## OpenAPI

````yaml POST /v0/boards/{boardId}/charts
openapi: 3.1.0
info:
  title: Formo Public API
  description: >-
    REST API for managing Formo projects, analytics, alerts, boards, charts,
    contracts, segments, and AI chat.


    **Auth.** All endpoints require a workspace API key with the appropriate
    scopes (see `x-api-scopes`).


    **Response shape.** Successful responses return the resource directly (or `{
    data: [...], total, page, size, has_more }` for paginated lists). HTTP
    status carries success/failure; there is no envelope wrapping success
    bodies.


    **Errors.** Every non-2xx response uses the `Error` envelope: `{ error: {
    code, message, doc_url, param?, details? } }`. Branch on the
    machine-readable `code` (see `ErrorCode` enum) and follow `doc_url` to the
    matching section of the [errors
    reference](https://docs.formo.so/api/errors).


    **Idempotency.** Pass an `Idempotency-Key` header on POST/PUT/PATCH/DELETE
    to make retries safe; the response is cached for 24 h and replayed on
    duplicate keys.
  version: 0.1.0
  contact:
    name: Formo
    url: https://formo.so
servers:
  - url: https://api.formo.so
    description: API Server (boards, alerts, contracts, segments, profiles, query, import)
  - url: https://events.formo.so
    description: Events Server (event ingestion)
security:
  - WorkspaceApiKey: []
tags:
  - name: Alerts
    description: Manage project alerts and notifications
  - name: Boards
    description: Manage dashboard boards
  - name: Charts
    description: Manage charts within boards
  - name: Contracts
    description: Manage blockchain contract monitoring
  - name: Segments
    description: Manage user segments
  - name: Profiles
    description: Wallet profiles and import
  - name: Query
    description: >-
      Execute SQL queries and call pre-built analytics endpoints (KPIs, top
      pages, lifecycle, retention, revenue). Requires the query:read scope.
  - name: Events
    description: Event ingestion API (events.formo.so)
paths:
  /v0/boards/{boardId}/charts:
    post:
      tags:
        - Charts
      summary: Create chart
      operationId: createChart
      parameters:
        - name: boardId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChartRequest'
            examples:
              funnelBasic:
                summary: 3-step closed funnel (7-day window)
                value:
                  projectId: proj_abc
                  query: SELECT 1
                  chart_type: funnel
                  title: Onboarding Funnel
                  steps:
                    - type: event
                      event: page
                    - type: event
                      event: connect
                    - type: event
                      event: transaction
                  settings:
                    funnelType: closed
                    conversionWindow:
                      value: 7
                      unit: day
              funnelWithPropertyFilters:
                summary: Funnel with per-step property filters (MetaMask on mainnet)
                value:
                  projectId: proj_abc
                  query: SELECT 1
                  chart_type: funnel
                  title: MetaMask Conversion Funnel
                  steps:
                    - type: event
                      event: page
                    - type: event
                      event: connect
                      rdns:
                        op: equals
                        value: io.metamask
                      chain_id:
                        op: equals
                        value: '1'
                    - type: event
                      event: transaction
                  settings:
                    funnelType: closed
                    conversionWindow:
                      value: 7
                      unit: day
              funnelWithBreakdown:
                summary: Funnel with device breakdown
                value:
                  projectId: proj_abc
                  query: SELECT 1
                  chart_type: funnel
                  title: Onboarding Funnel by Device
                  steps:
                    - type: event
                      event: page
                    - type: event
                      event: connect
                    - type: event
                      event: signature
                  settings:
                    funnelType: closed
                    conversionWindow:
                      value: 30
                      unit: day
                    breakdown: device
              funnelOpenMultiValue:
                summary: Open funnel with multi-value `in` filter and breakdown
                value:
                  projectId: proj_abc
                  query: SELECT 1
                  chart_type: funnel
                  title: Mobile Onboarding Funnel
                  steps:
                    - type: event
                      event: page
                      device:
                        op: equals
                        value: mobile
                    - type: event
                      event: connect
                      provider_name:
                        op: in
                        value: metamask|rainbow|coinbase
                    - type: event
                      event: transaction
                  settings:
                    funnelType: open
                    conversionWindow:
                      value: 30
                      unit: day
                    breakdown: device
              barChart:
                summary: 'Daily active users: bar chart'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT toDate(timestamp) AS date, countDistinct(address) AS
                    users FROM events GROUP BY date ORDER BY date
                  chart_type: bar
                  title: Daily Active Users
                  x_axis: date
                  y_axis:
                    - users
              lineChart:
                summary: 'DAU last 30 days: line chart'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT toDate(timestamp) AS date, countDistinct(address) AS
                    daily_active_users FROM events GROUP BY date ORDER BY date
                    DESC LIMIT 30
                  chart_type: line
                  title: Daily Active Users
                  x_axis: date
                  y_axis:
                    - daily_active_users
              pieChart:
                summary: 'Sessions by device: pie chart'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT device, COUNT(*) AS session_count FROM sessions GROUP
                    BY device ORDER BY session_count DESC LIMIT 10
                  chart_type: pie
                  title: Sessions by Device
                  x_axis: device
                  y_axis:
                    - session_count
              stackedChart:
                summary: 'Sessions by device grouped by browser: stacked chart'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT device, browser, COUNT(*) AS session_count FROM
                    sessions GROUP BY device, browser ORDER BY session_count
                    DESC
                  chart_type: stacked
                  title: Sessions by Device and Browser
                  x_axis: device
                  y_axis:
                    - session_count
                  group_by: browser
              numberChart:
                summary: 'Total connected wallets: number / KPI card'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT COUNT(DISTINCT address) FROM events WHERE type =
                    'connect'
                  chart_type: number
                  title: Total Connected Wallets
              tableChart:
                summary: 'Recent events: table'
                value:
                  projectId: proj_abc
                  query: SELECT * FROM events ORDER BY timestamp DESC LIMIT 10
                  chart_type: table
                  title: Recent Events
              userPathsChart:
                summary: 'User flow from connect: max 5 steps'
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT anonymous_id, type AS event, timestamp FROM events
                    WHERE timestamp >= today() - INTERVAL 30 DAY ORDER BY
                    anonymous_id, timestamp
                  chart_type: user_paths
                  title: Post-Connect User Flow
                  settings:
                    startStep:
                      type: event
                      event: connect
                    endStep:
                      type: event
                      event: transaction
                    maxSteps: 5
                    conversionWindow:
                      value: 2
                      unit: week
              userPathsOpenEnded:
                summary: Open-ended user flow from page view
                value:
                  projectId: proj_abc
                  query: >-
                    SELECT anonymous_id, type AS event, timestamp FROM events
                    WHERE timestamp >= today() - INTERVAL 14 DAY ORDER BY
                    anonymous_id, timestamp
                  chart_type: user_paths
                  title: User Discovery Paths
                  settings:
                    startStep:
                      type: event
                      event: page
                    maxSteps: 8
                    nodesPerStep: 10
              retentionFiltered:
                summary: 'Weekly retention: desktop users, transaction event'
                value:
                  projectId: proj_abc
                  query: ''
                  chart_type: retention
                  title: 'Weekly Retention: Desktop'
                  settings:
                    retentionFilter:
                      type: event
                      event: transaction
                    retentionUserFilters:
                      - field: device
                        op: equals
                        value: desktop
              retentionUnfiltered:
                summary: Overall retention (no filters)
                value:
                  projectId: proj_abc
                  query: ''
                  chart_type: retention
                  title: Overall Retention
      responses:
        '201':
          description: Chart created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Chart'
components:
  schemas:
    CreateChartRequest:
      type: object
      description: Request body for creating a chart.
      properties:
        projectId:
          type: string
          description: Project the chart belongs to.
        query:
          type: string
          minLength: 1
          description: >-
            SQL query that powers the chart.


            - **`funnel`**: pass `"SELECT 1"`; the actual query is
            auto-generated from `steps`.

            - **`retention`**: can be omitted or pass `""`; data is fetched from
            the retention pipe directly.

            - **All other types**: required; must be a valid SQL string.
        chart_type:
          type: string
          enum:
            - table
            - number
            - funnel
            - bar
            - line
            - pie
            - stacked
            - user_paths
            - retention
          description: |-
            Visualization type. Determines which other fields are required:

            | `chart_type` | Extra required fields |
            |---|---|
            | `table` | `query` |
            | `number` | `query` (must return 1 row × 1 column) |
            | `bar` | `query`, `x_axis`, `y_axis` (≥ 1) |
            | `line` | `query`, `x_axis`, `y_axis` (≥ 1) |
            | `pie` | `query`, `y_axis` (exactly 1) |
            | `stacked` | `query`, `x_axis`, `y_axis` (exactly 1), `group_by` |
            | `funnel` | `steps` (≥ 2), `query` placeholder `"SELECT 1"` |
            | `user_paths` | `query`, `settings.startStep` |
            | `retention` | none (`query` ignored) |
        title:
          type: string
          minLength: 1
          description: Display name shown on the chart and board.
        description:
          type: string
          description: Optional description.
        x_axis:
          type: string
          description: >-
            Column name for the X axis. Required for `bar`, `line`, and
            `stacked`.
        y_axis:
          type: array
          items:
            type: string
          description: |-
            Column name(s) used as Y axis metrics.

            - `bar` / `line`: at least 1 element required.
            - `pie` / `stacked`: exactly 1 element required.
        group_by:
          type: string
          description: Column to group / stack series by. Required for `stacked`.
        steps:
          type: array
          items:
            $ref: '#/components/schemas/FunnelStep'
          minItems: 2
          description: >-
            Ordered list of funnel steps. Required for `funnel` (minimum 2
            steps).


            Each element is a `FunnelStep`; add property filters as extra keys
            on the step object (e.g. `"rdns": { "op": "equals", "value":
            "io.metamask" }`).
        settings:
          $ref: '#/components/schemas/ChartSettings'
      required:
        - projectId
        - chart_type
        - title
    Chart:
      type: object
      description: A saved chart attached to a board.
      properties:
        id:
          type: string
        chart_type:
          type: string
          enum:
            - table
            - number
            - funnel
            - bar
            - line
            - pie
            - stacked
            - user_paths
            - retention
          description: Visualization type.
        title:
          type: string
        description:
          type: string
          nullable: true
        query:
          type: string
          description: >-
            SQL query powering the chart. For `funnel` and `retention` charts
            this is a system-managed placeholder.
        project_id:
          type: string
        board_id:
          type: string
        x_axis:
          type: string
          nullable: true
          description: Column used as the X axis.
        y_axis:
          type: array
          items:
            type: string
          nullable: true
          description: Column(s) used as Y axis metric(s).
        group_by:
          type: string
          nullable: true
          description: Column used to group/stack series.
        steps:
          type: array
          items:
            $ref: '#/components/schemas/FunnelStep'
          nullable: true
          description: >-
            Ordered list of funnel steps. Only present when `chart_type` is
            `funnel`.
        settings:
          oneOf:
            - $ref: '#/components/schemas/ChartSettings'
            - type: 'null'
          description: Type-specific configuration. See `ChartSettings` for all fields.
      required:
        - id
        - chart_type
        - title
        - query
        - project_id
        - board_id
    FunnelStep:
      type: object
      description: >-
        A single step in a funnel or user-path flow.


        `type` and `event` are required. Any additional property key becomes a
        filter condition using the `StepFilterCondition` schema (e.g. `"rdns": {
        "op": "equals", "value": "io.metamask" }`).


        Standard filterable columns: `origin`, `device`, `browser`, `os`,
        `location`, `referrer`, `ref`, `utm_source`, `utm_medium`,
        `utm_campaign`, `utm_content`, `utm_term`. Any other key is treated as a
        JSON event property and extracted via `JSONExtractString(properties,
        '<key>')`.
      properties:
        type:
          type: string
          enum:
            - event
            - track
            - decoded_log
          description: >-
            `event`: built-in page/connect/transaction events; `track`: custom
            tracked events; `decoded_log`: decoded smart-contract events.
        event:
          type: string
          minLength: 1
          description: >-
            Event name (e.g. `page`, `connect`, `transaction`, or a custom track
            event name).
      required:
        - type
        - event
      additionalProperties:
        $ref: '#/components/schemas/StepFilterCondition'
    ChartSettings:
      type: object
      description: >-
        Chart-type-specific configuration. The fields that apply depend on
        `chart_type`:


        - **funnel**: `funnelType`, `conversionWindow`, `breakdown`

        - **user_paths**: `startStep`, `endStep`, `maxSteps`, `nodesPerStep`,
        `conversionWindow`, `filters`

        - **retention**: `retentionFilter`, `retentionUserFilters`


        All fields are optional at the schema level; see per-type validation
        rules for which are functionally required.
      properties:
        funnelType:
          type: string
          enum:
            - closed
            - open
          default: closed
          description: >-
            **Funnel only.** `closed`: users must complete steps in strict order
            with no intervening events. `open`: users may complete steps in
            order but other events may occur between steps.
        conversionWindow:
          $ref: '#/components/schemas/ConversionWindow'
          description: >-
            **Funnel & user_paths.** Maximum time from Step 1 for a user to
            complete all steps.
        breakdown:
          type: string
          enum:
            - device
            - browser
            - os
            - referrer
            - ref
            - utm_source
            - utm_medium
            - utm_campaign
            - utm_term
            - utm_content
          description: >-
            **Funnel only.** Split each funnel bar by this dimension. The top
            categories are shown individually; the rest are collapsed into
            'Others'.
        startStep:
          $ref: '#/components/schemas/FunnelStep'
          description: '**user_paths (required).** The event where the user flow begins.'
        endStep:
          oneOf:
            - $ref: '#/components/schemas/FunnelStep'
            - type: 'null'
          description: >-
            **user_paths (optional).** The event where the user flow ends. If
            `null` the flow is open-ended.
        maxSteps:
          type: integer
          minimum: 2
          maximum: 10
          default: 3
          description: >-
            **user_paths.** Maximum number of steps to show in the flow (2–10).
            Values above 10 are clamped to 10.
        nodesPerStep:
          type: integer
          minimum: 2
          maximum: 10
          default: 5
          description: >-
            **user_paths.** Maximum number of unique event nodes visible per
            step (2–10). Values above 10 are clamped to 10.
        filters:
          type: string
          description: >-
            **user_paths.** JSON-encoded string of additional filters applied to
            the path query.
        retentionFilter:
          oneOf:
            - $ref: '#/components/schemas/FunnelStep'
            - type: 'null'
          description: >-
            **retention.** Event that qualifies a returning visit as 'retained'.
            If `null`, any event counts as a return.
        retentionUserFilters:
          type: array
          items:
            $ref: '#/components/schemas/RetentionUserFilter'
          description: >-
            **retention.** Zero or more user-segment filters that narrow the
            cohort (e.g. only desktop users, only users from a specific UTM
            source).
    StepFilterCondition:
      type: object
      description: >-
        A filter condition for an event property on a funnel step. The `op`
        field specifies the comparison operator and `value` is the value to
        compare against. For `in` and `notIn` operators the value is a
        pipe-delimited string (e.g. `"metamask|rainbow|coinbase"`).
      properties:
        op:
          type: string
          enum:
            - equals
            - notEquals
            - in
            - notIn
            - gt
            - gte
            - lt
            - lte
            - startsWith
            - endsWith
            - includes
          description: >-
            Comparison operator. Use `in` / `notIn` for multi-value matching
            (pipe-delimited value string).
        value:
          oneOf:
            - type: string
            - type: number
          description: >-
            The comparison value. For `in` / `notIn`, use a pipe-delimited
            string: `"metamask|rainbow|coinbase"`.
      required:
        - op
        - value
    ConversionWindow:
      type: object
      description: >-
        Time window within which a user must complete all funnel steps (measured
        from Step 1). Defaults to 1 day if omitted.
      properties:
        value:
          type: integer
          minimum: 1
          description: Number of time units.
        unit:
          type: string
          enum:
            - minute
            - hour
            - day
            - week
            - month
          description: Time unit. `month` = 30 days, `week` = 7 days.
      required:
        - value
        - unit
    RetentionUserFilter:
      type: object
      description: A user-level segment filter applied to the retention chart cohort.
      properties:
        field:
          type: string
          description: >-
            The user property to filter on (e.g. `device`, `browser`, `os`,
            `location`, `utm_source`, `utm_medium`, `utm_campaign`).
        op:
          type: string
          enum:
            - equals
            - notEquals
            - in
            - notIn
            - gt
            - gte
            - lt
            - lte
            - startsWith
            - endsWith
            - includes
          description: Comparison operator.
        value:
          oneOf:
            - type: string
            - type: number
          description: The value to compare against.
      required:
        - field
        - op
        - value
  securitySchemes:
    WorkspaceApiKey:
      type: http
      scheme: bearer
      description: >-
        Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard
        under Team Settings > API Keys.

````