From 6c55f524589cee99384a03d2b88b8648c118c2db Mon Sep 17 00:00:00 2001 From: Yos Riady Date: Fri, 24 Jul 2026 12:20:24 +0700 Subject: [PATCH 1/3] docs: canonical filter operators + isEmpty/notEmpty (v0/profiles alias retirement) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Formo API retired the legacy operator aliases (eq/neq/gt/gte/lt/lte/ nin/contains) on POST /v0/profiles filters — they now return 400. Bring the docs in line with the canonical vocabulary: - api/openapi.json: synced from the backend spec — FilterCondition (and the analytics/step/retention filter schemas) now use canonical tokens and add the value-less existence ops notEmpty / isEmpty; value optional. - api/profiles/search.mdx: operator table rewritten to canonical (equals/notEquals/greater/greaterOrEqual/less/lessOrEqual/in/notIn/ includes) + notEmpty/isEmpty rows and a compatibility note; all JSON examples canonicalized; social presence checks now use `notEmpty` instead of the old `equals ""` idiom. - cli/profiles.mdx: canonicalized the example op tokens. --- api/openapi.json | 76 +++++++++++++++++++------------- api/profiles/search.mdx | 98 ++++++++++++++++++++++------------------- cli/profiles.mdx | 10 ++--- 3 files changed, 103 insertions(+), 81 deletions(-) diff --git a/api/openapi.json b/api/openapi.json index 8ce66bd..49ae460 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -29,7 +29,7 @@ "WorkspaceApiKey": { "type": "http", "scheme": "bearer", - "description": "Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard under Team Settings > API." + "description": "Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard under Team Settings > API Keys." }, "X402Payment": { "type": "apiKey", @@ -168,7 +168,8 @@ "type": "string" }, "operator": { - "type": "string" + "type": "string", + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `in`, `notIn`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty`) and their legacy aliases (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -275,11 +276,17 @@ "gte", "lt", "lte", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", "startsWith", "endsWith", - "includes" + "includes", + "notEmpty", + "isEmpty" ], - "description": "Comparison operator. Use `in` / `notIn` for multi-value matching (pipe-delimited value string)." + "description": "Comparison operator. Both the abbreviated comparison aliases (`gt`, `gte`, `lt`, `lte`) and the canonical long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are accepted and equivalent. Use `in` / `notIn` for multi-value matching (pipe-delimited value string). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on the property; the `value` is ignored. Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points` — a number has no emptiness semantics." }, "value": { "oneOf": [ @@ -370,9 +377,11 @@ "notIn", "startsWith", "endsWith", - "includes" + "includes", + "notEmpty", + "isEmpty" ], - "description": "Comparison operator. `startsWith` / `endsWith` / `includes` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`)." + "description": "Comparison operator. `startsWith` / `endsWith` / `includes` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`). `notEmpty` matches rows where the column has any non-empty value, and `isEmpty` is its complement (the column is empty/unset); both ignore `value` (pass any placeholder). Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points`." }, "value": { "oneOf": [ @@ -414,11 +423,14 @@ "gte", "lt", "lte", - "startsWith", - "endsWith", - "includes" + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", + "notEmpty", + "isEmpty" ], - "description": "Comparison operator." + "description": "Comparison operator. Both the abbreviated numeric aliases (`gt`, `gte`, `lt`, `lte`) and the canonical long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are accepted and equivalent. `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on a string user property; the `value` is ignored. Substring operators (`startsWith` / `endsWith` / `includes`) are not supported on retention user filters." }, "value": { "oneOf": [ @@ -1631,8 +1643,7 @@ "type": "object", "required": [ "field", - "op", - "value" + "op" ], "properties": { "field": { @@ -1642,18 +1653,22 @@ "op": { "type": "string", "enum": [ - "eq", - "neq", - "gt", - "gte", - "lt", - "lte", + "equals", + "notEquals", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", "in", - "nin" - ] + "notIn", + "includes", + "notEmpty", + "isEmpty" + ], + "description": "Comparison operator (canonical spellings only). The legacy abbreviated dialect (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) is no longer accepted — sending one returns 400; use the canonical form (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`). `includes` (case-insensitive substring) is only supported on social fields (e.g. `users.twitter`, `users.email`). `notEmpty` (\"is not empty\" / \"is set\") is a string existence check for user string attributes (device/os/utm_*/referrer/…) and social fields; `isEmpty` (\"is empty\" / \"is not set\") is its complement, supported for user string attributes only (not social fields). Both ignore `value`." }, "value": { - "description": "Filter value (string, number, boolean, or array)" + "description": "Filter value (string, number, boolean, or array). Required for every operator except the existence checks `notEmpty` and `isEmpty`, which ignore it." }, "scope": { "type": "string", @@ -2221,7 +2236,7 @@ } }, "x-api-scopes": { - "description": "API key scopes control access to endpoints. Create keys with the required scopes in Team Settings > API.", + "description": "API key scopes control access to endpoints. Create keys with the required scopes in Team Settings > API Keys.", "scopes": { "alerts:read": "List and get alerts", "alerts:write": "Create, update, delete alerts (requires alerts:read)", @@ -2351,7 +2366,8 @@ "type": "string" }, "operator": { - "type": "string" + "type": "string", + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `in`, `notIn`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty`) and their legacy aliases (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -4685,7 +4701,7 @@ "conditions": [ { "field": "users.net_worth_usd", - "op": "gt", + "op": "greater", "value": 10000 } ], @@ -4698,7 +4714,7 @@ "conditions": [ { "field": "chains.1.balance", - "op": "gt", + "op": "greater", "value": 1000 } ], @@ -4711,7 +4727,7 @@ "conditions": [ { "field": "labels.coinbase.verified_account", - "op": "eq", + "op": "equals", "value": "true" } ], @@ -4724,7 +4740,7 @@ "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "gt", + "op": "greater", "value": 0, "scope": "any" } @@ -7407,7 +7423,7 @@ "get": { "operationId": "getAnalyticsFunnel", "summary": "Get multi-step conversion funnel", - "description": "Multi-step conversion funnel. For an ordered list of step specs, returns one row per step with the unique-user count, conversion ratios against step 1 and the previous step, drop-off ratio, and median time-to-convert (from-previous and from-start, in seconds).\n\n**Closed vs open:** `funnel_type=closed` (default) requires the user to fire the steps in order within `window_seconds`. `funnel_type=open` counts whoever fired step k regardless of order, and adds a `dropped_off_users` column for each step.\n\n**Breakdown:** when `group_by` is set to a column from the breakdown allowlist (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`), the response gains a `breakdown` column and is grouped per (step, top-N category + 'Others', up to `limit` which defaults to 5). First-touch attribution is used by default; pass `attribution=last_touch` to bucket by each user's latest value.\n\n**Steps:** the `steps` query param is a JSON-encoded array of 2 to 10 step specs of shape `{type, event, name, filters?: [{operand, operator, value}]}`. Use `name` (e.g. `\"page::0\"`) as a unique step id so the same event re-used at different steps can be disambiguated in the response. `operator` accepts `equals | notEquals | in | notIn | gt | lt | gte | lte | startsWith | endsWith | includes`. `operand` may target a standard event column or a JSON property on `properties`.", + "description": "Multi-step conversion funnel. For an ordered list of step specs, returns one row per step with the unique-user count, conversion ratios against step 1 and the previous step, drop-off ratio, and median time-to-convert (from-previous and from-start, in seconds).\n\n**Closed vs open:** `funnel_type=closed` (default) requires the user to fire the steps in order within `window_seconds`. `funnel_type=open` counts whoever fired step k regardless of order, and adds a `dropped_off_users` column for each step.\n\n**Breakdown:** when `group_by` is set to a column from the breakdown allowlist (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`), the response gains a `breakdown` column and is grouped per (step, top-N category + 'Others', up to `limit` which defaults to 5). First-touch attribution is used by default; pass `attribution=last_touch` to bucket by each user's latest value.\n\n**Steps:** the `steps` query param is a JSON-encoded array of 2 to 10 step specs of shape `{type, event, name, filters?: [{operand, operator, value}]}`. Use `name` (e.g. `\"page::0\"`) as a unique step id so the same event re-used at different steps can be disambiguated in the response. `operator` accepts `equals | notEquals | in | notIn | gt | lt | gte | lte | greater | greaterOrEqual | less | lessOrEqual | startsWith | endsWith | includes | notEmpty | isEmpty` (the short numeric aliases `gt`/`gte`/`lt`/`lte` and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on the numeric event columns `volume`/`revenue`/`points`). `operand` may target a standard event column or a JSON property on `properties`.", "tags": [ "Query" ], @@ -7442,7 +7458,7 @@ "schema": { "type": "string" }, - "description": "JSON-encoded array of 2 to 10 step specs. Each step is `{type, event, name, filters?: [{operand, operator, value}]}`. `type` is the event type (e.g. `event` for page views, `track` for custom events, `transaction`, `signature`, `decoded_log`). `event` is the event name. `name` is the unique step id (use `\"::\"` to disambiguate repeated events).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `startsWith`, `endsWith`, `includes`. For `in`/`notIn`, pass the values as a `|`-separated string in `value` (e.g. `\"ethereum|polygon|base\"`).\n\n**Standard columns** (rendered via direct column access): `origin`, `device`, `browser`, `os`, `location`, `referrer`, `direct`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`, `version`, `locale`, `timezone`, `page_path`. Anything else is treated as a JSON property and read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", + "description": "JSON-encoded array of 2 to 10 step specs. Each step is `{type, event, name, filters?: [{operand, operator, value}]}`. `type` is the event type (e.g. `event` for page views, `track` for custom events, `transaction`, `signature`, `decoded_log`). `event` is the event name. `name` is the unique step id (use `\"::\"` to disambiguate repeated events).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty` (the short numeric aliases and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on numeric event columns `volume`/`revenue`/`points`). For `in`/`notIn`, pass the values as a `|`-separated string in `value` (e.g. `\"ethereum|polygon|base\"`).\n\n**Standard columns** (rendered via direct column access): `origin`, `device`, `browser`, `os`, `location`, `referrer`, `direct`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`, `version`, `locale`, `timezone`, `page_path`. Anything else is treated as a JSON property and read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", "examples": { "simple": { "summary": "Simple 3-step funnel: page → connect → swap", @@ -7678,7 +7694,7 @@ "schema": { "type": "string" }, - "description": "JSON-encoded start-step spec of shape `{type, event, resolved_event, filters?: [...], status_type?, status_value?}`. Use `\"resolved_event\":\"__ALL_PAGE_VIEWS__\"` to match any page view; pass a page path for a specific landing page; or pass an event name for `track`/`decoded_log` start events. `filters` use `{operand, operator, value, values?}` (use `values` for `in`/`notIn`).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `startsWith`, `endsWith`, `includes`. Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `utm_*`, `page_path`, etc.) are read directly; everything else is read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", + "description": "JSON-encoded start-step spec of shape `{type, event, resolved_event, filters?: [...], status_type?, status_value?}`. Use `\"resolved_event\":\"__ALL_PAGE_VIEWS__\"` to match any page view; pass a page path for a specific landing page; or pass an event name for `track`/`decoded_log` start events. `filters` use `{operand, operator, value, values?}` (use `values` for `in`/`notIn`).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty` (the short numeric aliases and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on numeric event columns `volume`/`revenue`/`points`). Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `utm_*`, `page_path`, etc.) are read directly; everything else is read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", "examples": { "anyPageView": { "summary": "Match any page view as the start step", diff --git a/api/profiles/search.mdx b/api/profiles/search.mdx index b9eb2b5..adde22d 100644 --- a/api/profiles/search.mdx +++ b/api/profiles/search.mdx @@ -108,7 +108,7 @@ The search endpoint supports rich filtering capabilities through a JSON request "conditions": [ { "field": "users.net_worth_usd", - "op": "gt", + "op": "greater", "value": 1000 } ], @@ -137,15 +137,21 @@ Each condition in the `conditions` array has: | Operator | Description | Example | |----------|-------------|---------| -| `eq` | Equals | `{"field": "users.device", "op": "eq", "value": "Desktop"}` | -| `neq` | Not equals | `{"field": "users.os", "op": "neq", "value": "Windows"}` | -| `gt` | Greater than | `{"field": "users.net_worth_usd", "op": "gt", "value": 1000}` | -| `gte` | Greater than or equal | `{"field": "users.volume", "op": "gte", "value": 100}` | -| `lt` | Less than | `{"field": "users.net_worth_usd", "op": "lt", "value": 50000}` | -| `lte` | Less than or equal | `{"field": "chains.1.balance", "op": "lte", "value": 10000}` | +| `equals` | Equals | `{"field": "users.device", "op": "equals", "value": "Desktop"}` | +| `notEquals` | Not equals | `{"field": "users.os", "op": "notEquals", "value": "Windows"}` | +| `greater` | Greater than | `{"field": "users.net_worth_usd", "op": "greater", "value": 1000}` | +| `greaterOrEqual` | Greater than or equal | `{"field": "users.volume", "op": "greaterOrEqual", "value": 100}` | +| `less` | Less than | `{"field": "users.net_worth_usd", "op": "less", "value": 50000}` | +| `lessOrEqual` | Less than or equal | `{"field": "chains.1.balance", "op": "lessOrEqual", "value": 10000}` | | `in` | In array | `{"field": "users.lifecycle", "op": "in", "value": ["New", "Power user"]}` | -| `nin` | Not in array | `{"field": "users.location", "op": "nin", "value": ["US", "UK"]}` | -| `contains` | Case-insensitive substring match for social fields only | `{"field": "users.twitter", "op": "contains", "value": "vitalik"}` | +| `notIn` | Not in array | `{"field": "users.location", "op": "notIn", "value": ["US", "UK"]}` | +| `includes` | Case-insensitive substring match for social fields only | `{"field": "users.twitter", "op": "includes", "value": "vitalik"}` | +| `notEmpty` | Field is set (non-empty). String fields only — value is ignored | `{"field": "users.email", "op": "notEmpty"}` | +| `isEmpty` | Field is not set (empty). String user attributes only — value is ignored | `{"field": "users.utm_source", "op": "isEmpty"}` | + + +**Operator compatibility.** The short aliases `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, and `contains` are **no longer accepted** and return `400`. Use the canonical operators above. `notEmpty` / `isEmpty` are value-less existence checks (any `value` is ignored) and are supported on string user/profile attributes only — not on numeric metrics (`net_worth_usd`, `volume`, `revenue`, `points`), the `lifecycle` enum, or chain/app/token/label fields. + --- @@ -169,7 +175,7 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.net_worth_usd", "op": "gt", "value": 10000 } + { "field": "users.net_worth_usd", "op": "greater", "value": 10000 } ], "logic": "and" } @@ -180,7 +186,7 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.volume", "op": "gt", "value": 5000 } + { "field": "users.volume", "op": "greater", "value": 5000 } ], "logic": "and" } @@ -200,8 +206,8 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.device", "op": "eq", "value": "Mobile" }, - { "field": "users.location", "op": "eq", "value": "US" } + { "field": "users.device", "op": "equals", "value": "Mobile" }, + { "field": "users.location", "op": "equals", "value": "US" } ], "logic": "and" } @@ -233,8 +239,8 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.first_utm_source", "op": "eq", "value": "google" }, - { "field": "users.first_utm_medium", "op": "eq", "value": "cpc" } + { "field": "users.first_utm_source", "op": "equals", "value": "google" }, + { "field": "users.first_utm_medium", "op": "equals", "value": "cpc" } ], "logic": "and" } @@ -263,8 +269,8 @@ Filter by user lifecycle stage. Valid values: `At Risk`, `Churned`, `New`, `Powe Social fields support two modes: -- Presence checks: use an empty string value to match profiles where the field is present. -- Value matching: use `eq`, `neq`, or `contains` with a non-empty string to match the actual social value. +- Presence checks: use `notEmpty` to match profiles where the field is set (or `isEmpty` for the opposite). The value is ignored. +- Value matching: use `equals`, `notEquals`, or `includes` (case-insensitive substring) with a non-empty string to match the actual social value. | Field | Description | |-------|-------------| @@ -291,7 +297,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.email", "op": "eq", "value": "" } + { "field": "users.email", "op": "notEmpty" } ], "logic": "and" } @@ -302,8 +308,8 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.ens", "op": "eq", "value": "" }, - { "field": "users.farcaster", "op": "eq", "value": "" } + { "field": "users.ens", "op": "notEmpty" }, + { "field": "users.farcaster", "op": "notEmpty" } ], "logic": "and" } @@ -314,7 +320,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.twitter", "op": "contains", "value": "bob" } + { "field": "users.twitter", "op": "includes", "value": "bob" } ], "logic": "and" } @@ -325,7 +331,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.email", "op": "eq", "value": "alice@formo.so" } + { "field": "users.email", "op": "equals", "value": "alice@formo.so" } ], "logic": "and" } @@ -346,7 +352,7 @@ Use `chains.balance` to filter across all chains (returns profiles where **any** ```json { "conditions": [ - { "field": "chains.balance", "op": "gt", "value": 1000 } + { "field": "chains.balance", "op": "greater", "value": 1000 } ], "logic": "and" } @@ -370,7 +376,7 @@ Common chain IDs: ```json { "conditions": [ - { "field": "chains.1.balance", "op": "gt", "value": 5000 } + { "field": "chains.1.balance", "op": "greater", "value": 5000 } ], "logic": "and" } @@ -381,8 +387,8 @@ Common chain IDs: ```json { "conditions": [ - { "field": "chains.1.balance", "op": "gt", "value": 1000 }, - { "field": "chains.137.balance", "op": "gt", "value": 1000 } + { "field": "chains.1.balance", "op": "greater", "value": 1000 }, + { "field": "chains.137.balance", "op": "greater", "value": 1000 } ], "logic": "and" } @@ -403,7 +409,7 @@ Use `apps.{app_id}.balance` to filter by app balance across all chains. ```json { "conditions": [ - { "field": "apps.uniswap.balance", "op": "gt", "value": 1000 } + { "field": "apps.uniswap.balance", "op": "greater", "value": 1000 } ], "logic": "and" } @@ -418,7 +424,7 @@ Use `apps.{chain_id}.{app_id}.balance` to filter by app balance on a specific ch ```json { "conditions": [ - { "field": "apps.1.aave.balance", "op": "gt", "value": 500 } + { "field": "apps.1.aave.balance", "op": "greater", "value": 500 } ], "logic": "and" } @@ -448,7 +454,7 @@ Use `tokens.{token_address}.balance` to filter by token balance across all chain "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "gt", + "op": "greater", "value": 1000 } ], @@ -467,7 +473,7 @@ Use `tokens.{chain_id}.{token_address}.balance` to filter by token balance on a "conditions": [ { "field": "tokens.1.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "gt", + "op": "greater", "value": 500 } ], @@ -486,7 +492,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "gt", + "op": "greater", "value": 0, "scope": "protocol" } @@ -502,7 +508,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "gt", + "op": "greater", "value": 1000, "scope": "protocol", "appId": "aave-v3" @@ -519,7 +525,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0x0000000000000000000000000000000000000000.balance", - "op": "gt", + "op": "greater", "value": 1, "scope": "protocol", "appId": "lido" @@ -548,7 +554,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.coinbase.verified_account", "op": "eq", "value": "true" } + { "field": "labels.coinbase.verified_account", "op": "equals", "value": "true" } ], "logic": "and" } @@ -559,7 +565,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.coinbase.verified_country", "op": "eq", "value": "US" } + { "field": "labels.coinbase.verified_country", "op": "equals", "value": "US" } ], "logic": "and" } @@ -570,7 +576,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.passport.models_aggregate_score", "op": "gte", "value": "50" } + { "field": "labels.passport.models_aggregate_score", "op": "greaterOrEqual", "value": "50" } ], "logic": "and" } @@ -590,9 +596,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }, - { "field": "users.ens", "op": "eq", "value": "" }, - { "field": "chains.1.balance", "op": "gt", "value": 0 } + { "field": "users.net_worth_usd", "op": "greater", "value": 10000 }, + { "field": "users.ens", "op": "notEmpty" }, + { "field": "chains.1.balance", "op": "greater", "value": 0 } ], "logic": "and" }' \ @@ -609,8 +615,8 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "apps.uniswap.balance", "op": "gt", "value": 0 }, - { "field": "apps.aave.balance", "op": "gt", "value": 0 } + { "field": "apps.uniswap.balance", "op": "greater", "value": 0 }, + { "field": "apps.aave.balance", "op": "greater", "value": 0 } ], "logic": "or" }' \ @@ -627,9 +633,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "labels.coinbase.verified_account", "op": "eq", "value": "true" }, + { "field": "labels.coinbase.verified_account", "op": "equals", "value": "true" }, { "field": "labels.coinbase.verified_country", "op": "in", "value": ["US", "UK", "DE"] }, - { "field": "users.lifecycle", "op": "eq", "value": "Power user" } + { "field": "users.lifecycle", "op": "equals", "value": "Power user" } ], "logic": "and" }' \ @@ -646,9 +652,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "chains.1.balance", "op": "gt", "value": 10000 }, - { "field": "chains.137.balance", "op": "gt", "value": 5000 }, - { "field": "chains.42161.balance", "op": "gt", "value": 5000 } + { "field": "chains.1.balance", "op": "greater", "value": 10000 }, + { "field": "chains.137.balance", "op": "greater", "value": 5000 }, + { "field": "chains.42161.balance", "op": "greater", "value": 5000 } ], "logic": "and" }' \ diff --git a/cli/profiles.mdx b/cli/profiles.mdx index 8834b1b..2e29004 100644 --- a/cli/profiles.mdx +++ b/cli/profiles.mdx @@ -91,18 +91,18 @@ formo profiles search --order-by net_worth_usd --order-dir desc --size 5 # Search with a typed filter condition formo profiles search \ - --conditions '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' \ + --conditions '[{"field":"users.net_worth_usd","op":"greater","value":10000}]' \ --size 20 # Search profiles matching either condition formo profiles search \ - --conditions '[{"field":"users.net_worth_usd","op":"gt","value":10000},{"field":"users.volume","op":"gt","value":1000}]' \ + --conditions '[{"field":"users.net_worth_usd","op":"greater","value":10000},{"field":"users.volume","op":"greater","value":1000}]' \ --logic or \ --size 20 # Filter by onchain balance on Ethereum formo profiles search \ - --conditions '[{"field":"chains.1.balance","op":"gt","value":1000}]' \ + --conditions '[{"field":"chains.1.balance","op":"greater","value":1000}]' \ --size 20 ``` @@ -114,8 +114,8 @@ The response is a paginated envelope: `{ data, total, page, size, has_more }`. ```json [ - { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }, - { "field": "chains.1.balance", "op": "gte", "value": 1000 } + { "field": "users.net_worth_usd", "op": "greater", "value": 10000 }, + { "field": "chains.1.balance", "op": "greaterOrEqual", "value": 1000 } ] ``` From 32da1e6a48584f018a771ecf27dea741d783dbf2 Mon Sep 17 00:00:00 2001 From: Yos Riady Date: Fri, 24 Jul 2026 16:29:57 +0700 Subject: [PATCH 2/3] Terse canonical operators; long forms deprecated-but-accepted on /v0/profiles The backend flipped canonical filter operators to the terse set (eq, neq, in, nin, gt, gte, lt, lte, contains, startsWith, endsWith, notEmpty, isEmpty). Sync openapi.json from the backend spec (semantically identical), rewrite the profiles operator tables/examples, and correct the earlier "aliases return 400" framing: the 8 long-form spellings remain accepted on /v0/profiles as deprecated aliases folded at the boundary (retirement deferred to P-2373 Phase B). Also sweeps create-chart, analytics CLI, event-timeseries, funnel, and idempotency pages that disagreed with the terse-only analytics endpoints. Co-Authored-By: Claude Fable 5 --- api/boards/create-chart.mdx | 60 ++++++----- api/idempotency.mdx | 2 +- api/openapi.json | 186 ++++++++++++++++----------------- api/profiles/search.mdx | 84 +++++++-------- api/query/event-timeseries.mdx | 2 +- api/query/funnel.mdx | 2 + cli/analytics.mdx | 4 +- cli/profiles.mdx | 14 +-- 8 files changed, 185 insertions(+), 169 deletions(-) diff --git a/api/boards/create-chart.mdx b/api/boards/create-chart.mdx index fa2a326..9ef1297 100644 --- a/api/boards/create-chart.mdx +++ b/api/boards/create-chart.mdx @@ -143,21 +143,27 @@ Each step in the `steps` array has the following shape: | `op` | SQL equivalent | Notes | | ------------ | ---------------------- | ----------------------------------------------------- | -| `equals` | `= 'value'` | Exact match | -| `notEquals` | `!= 'value'` | Inverse match | +| `eq` | `= 'value'` | Exact match | +| `neq` | `!= 'value'` | Inverse match | | `in` | `IN (...)` | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` | -| `notIn` | `NOT IN (...)` | Pipe-delimited value | +| `nin` | `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 | +| `contains` | `like '%v%'` | Substring match | +| `notEmpty` | `col != ''` | Value-less existence check; `value` is ignored | +| `isEmpty` | `col = ''` | Value-less existence check; `value` is ignored | - For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal - pipe with `\|` and a literal backslash with `\\`. + For `in` and `nin`, join multiple values with a pipe `|`. Escape a literal + pipe with `\|` and a literal backslash with `\\`. The legacy numeric long + forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are deprecated + aliases for `gt` / `gte` / `lt` / `lte` and remain accepted. `notEmpty` and + `isEmpty` are rejected on the numeric event columns `volume` / `revenue` / + `points`. ### `settings` for Funnel Charts @@ -221,8 +227,8 @@ Filter step 2 to MetaMask wallets on Ethereum mainnet: { "type": "event", "event": "connect", - "rdns": { "op": "equals", "value": "io.metamask" }, - "chain_id": { "op": "equals", "value": "1" } + "rdns": { "op": "eq", "value": "io.metamask" }, + "chain_id": { "op": "eq", "value": "1" } }, { "type": "event", "event": "transaction" } ], @@ -266,7 +272,7 @@ Filter step 2 to MetaMask wallets on Ethereum mainnet: { "type": "event", "event": "page", - "device": { "op": "equals", "value": "mobile" } + "device": { "op": "eq", "value": "mobile" } }, { "type": "event", @@ -333,7 +339,7 @@ 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`) | +| `op` | string | Comparison operator (same set as `StepFilterCondition.op`, minus the substring operators `startsWith` / `endsWith` / `contains`) | | `value` | string \| number | Value to compare against | ```json @@ -345,8 +351,8 @@ Each `RetentionUserFilter`: "settings": { "retentionFilter": { "type": "event", "event": "transaction" }, "retentionUserFilters": [ - { "field": "device", "op": "equals", "value": "desktop" }, - { "field": "utm_source", "op": "notEquals", "value": "direct" } + { "field": "device", "op": "eq", "value": "desktop" }, + { "field": "utm_source", "op": "neq", "value": "direct" } ] } } @@ -550,21 +556,27 @@ Each step in the `steps` array has the following shape: | `op` | SQL equivalent | Notes | | ------------ | ---------------------- | ----------------------------------------------------- | -| `equals` | `= 'value'` | Exact match | -| `notEquals` | `!= 'value'` | Inverse match | +| `eq` | `= 'value'` | Exact match | +| `neq` | `!= 'value'` | Inverse match | | `in` | `IN (...)` | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` | -| `notIn` | `NOT IN (...)` | Pipe-delimited value | +| `nin` | `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 | +| `contains` | `like '%v%'` | Substring match | +| `notEmpty` | `col != ''` | Value-less existence check; `value` is ignored | +| `isEmpty` | `col = ''` | Value-less existence check; `value` is ignored | - For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal - pipe with `\|` and a literal backslash with `\\`. + For `in` and `nin`, join multiple values with a pipe `|`. Escape a literal + pipe with `\|` and a literal backslash with `\\`. The legacy numeric long + forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are deprecated + aliases for `gt` / `gte` / `lt` / `lte` and remain accepted. `notEmpty` and + `isEmpty` are rejected on the numeric event columns `volume` / `revenue` / + `points`. ### `settings` for Funnel Charts @@ -628,8 +640,8 @@ Filter step 2 to MetaMask wallet connections on Ethereum mainnet: { "type": "event", "event": "connect", - "rdns": { "op": "equals", "value": "io.metamask" }, - "chain_id": { "op": "equals", "value": "1" } + "rdns": { "op": "eq", "value": "io.metamask" }, + "chain_id": { "op": "eq", "value": "1" } }, { "type": "event", "event": "transaction" } ], @@ -673,7 +685,7 @@ Filter step 2 to MetaMask wallet connections on Ethereum mainnet: { "type": "event", "event": "page", - "device": { "op": "equals", "value": "mobile" } + "device": { "op": "eq", "value": "mobile" } }, { "type": "event", @@ -740,7 +752,7 @@ 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`) | +| `op` | string | Comparison operator (same set as `StepFilterCondition.op`, minus the substring operators `startsWith` / `endsWith` / `contains`) | | `value` | string \| number | Value to compare against | ```json @@ -752,8 +764,8 @@ Each `RetentionUserFilter`: "settings": { "retentionFilter": { "type": "event", "event": "transaction" }, "retentionUserFilters": [ - { "field": "device", "op": "equals", "value": "desktop" }, - { "field": "utm_source", "op": "notEquals", "value": "direct" } + { "field": "device", "op": "eq", "value": "desktop" }, + { "field": "utm_source", "op": "neq", "value": "direct" } ] } } diff --git a/api/idempotency.mdx b/api/idempotency.mdx index c77d7e3..541a095 100644 --- a/api/idempotency.mdx +++ b/api/idempotency.mdx @@ -72,7 +72,7 @@ curl -X POST https://api.formo.so/v0/alerts \ "name": "Daily revenue drop", "trigger_type": "event", "trigger_filters": [ - { "name": "event", "operator": "equals", "value": "transaction" } + { "name": "event", "operator": "eq", "value": "transaction" } ], "recipient": [{ "type": "webhook", "value": ["https://myapp.com/formo-webhook"] }] }' diff --git a/api/openapi.json b/api/openapi.json index 49ae460..3652dce 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -169,7 +169,7 @@ }, "operator": { "type": "string", - "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `in`, `notIn`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty`) and their legacy aliases (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `startsWith`, `endsWith`, `contains`, `notEmpty`, `isEmpty`) and their legacy aliases (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`, `like`, `=`, `!=`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -263,30 +263,26 @@ }, "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\"`).", + "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 `nin` operators the value is a pipe-delimited string (e.g. `\"metamask|rainbow|coinbase\"`).", "properties": { "op": { "type": "string", "enum": [ - "equals", - "notEquals", + "eq", + "neq", "in", - "notIn", + "nin", "gt", "gte", "lt", "lte", - "greater", - "greaterOrEqual", - "less", - "lessOrEqual", "startsWith", "endsWith", - "includes", + "contains", "notEmpty", "isEmpty" ], - "description": "Comparison operator. Both the abbreviated comparison aliases (`gt`, `gte`, `lt`, `lte`) and the canonical long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are accepted and equivalent. Use `in` / `notIn` for multi-value matching (pipe-delimited value string). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on the property; the `value` is ignored. Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points` — a number has no emptiness semantics." + "description": "Comparison operator. The terse comparison tokens (`gt`, `gte`, `lt`, `lte`) are canonical; the legacy long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) remain accepted and equivalent (deprecated). Use `in` / `nin` for multi-value matching (pipe-delimited value string). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on the property; the `value` is ignored. Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points` — a number has no emptiness semantics." }, "value": { "oneOf": [ @@ -297,7 +293,7 @@ "type": "number" } ], - "description": "The comparison value. For `in` / `notIn`, use a pipe-delimited string: `\"metamask|rainbow|coinbase\"`." + "description": "The comparison value. For `in` / `nin`, use a pipe-delimited string: `\"metamask|rainbow|coinbase\"`." } }, "required": [ @@ -307,7 +303,7 @@ }, "FunnelStep": { "type": "object", - "description": "A single step in a funnel or user-path flow.\n\n`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\" }`).\n\nStandard 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, '')`.", + "description": "A single step in a funnel or user-path flow.\n\n`type` and `event` are required. Any additional property key becomes a filter condition using the `StepFilterCondition` schema (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`).\n\nStandard 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, '')`.", "properties": { "type": { "type": "string", @@ -358,7 +354,7 @@ }, "AnalyticsFilterCondition": { "type": "object", - "description": "A single analytics filter condition. Use `in` / `notIn` with a pipe-delimited string value (e.g. `\"a|b|c\"`) for multi-value matches.", + "description": "A single analytics filter condition. Use `in` / `nin` with a pipe-delimited string value (e.g. `\"a|b|c\"`) for multi-value matches.", "properties": { "field": { "type": "string", @@ -367,21 +363,21 @@ "op": { "type": "string", "enum": [ - "equals", - "notEquals", - "greater", - "less", - "greaterOrEqual", - "lessOrEqual", + "eq", + "neq", + "gt", + "lt", + "gte", + "lte", "in", - "notIn", + "nin", "startsWith", "endsWith", - "includes", + "contains", "notEmpty", "isEmpty" ], - "description": "Comparison operator. `startsWith` / `endsWith` / `includes` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`). `notEmpty` matches rows where the column has any non-empty value, and `isEmpty` is its complement (the column is empty/unset); both ignore `value` (pass any placeholder). Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points`." + "description": "Comparison operator. `startsWith` / `endsWith` / `contains` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`). `notEmpty` matches rows where the column has any non-empty value, and `isEmpty` is its complement (the column is empty/unset); both ignore `value` (pass any placeholder). Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points`." }, "value": { "oneOf": [ @@ -395,7 +391,7 @@ "type": "boolean" } ], - "description": "Value to compare against. For `in` / `notIn`, pass a pipe-delimited string (e.g. `\"chrome|firefox\"`)." + "description": "Value to compare against. For `in` / `nin`, pass a pipe-delimited string (e.g. `\"chrome|firefox\"`)." } }, "required": [ @@ -415,22 +411,18 @@ "op": { "type": "string", "enum": [ - "equals", - "notEquals", + "eq", + "neq", "in", - "notIn", + "nin", "gt", "gte", "lt", "lte", - "greater", - "greaterOrEqual", - "less", - "lessOrEqual", "notEmpty", "isEmpty" ], - "description": "Comparison operator. Both the abbreviated numeric aliases (`gt`, `gte`, `lt`, `lte`) and the canonical long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are accepted and equivalent. `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on a string user property; the `value` is ignored. Substring operators (`startsWith` / `endsWith` / `includes`) are not supported on retention user filters." + "description": "Comparison operator. The terse numeric tokens (`gt`, `gte`, `lt`, `lte`) are canonical; the legacy long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) remain accepted and equivalent (deprecated). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on a string user property; the `value` is ignored. Substring operators (`startsWith` / `endsWith` / `contains`) are not supported on retention user filters." }, "value": { "oneOf": [ @@ -461,13 +453,13 @@ "op": { "type": "string", "enum": [ - "greater", - "greaterOrEqual", - "less", - "lessOrEqual", - "equals" + "gt", + "gte", + "lt", + "lte", + "eq" ], - "default": "greater", + "default": "gt", "description": "Comparison operator. Numeric operators coerce both sides via toFloat64OrZero, so a numeric op on a non-numeric value yields no match (not an error)." }, "value": { @@ -646,7 +638,7 @@ "$ref": "#/components/schemas/FunnelStep" }, "minItems": 2, - "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"equals\", \"value\": \"io.metamask\" }`)." + "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`)." }, "settings": { "$ref": "#/components/schemas/ChartSettings" @@ -721,7 +713,7 @@ "$ref": "#/components/schemas/FunnelStep" }, "minItems": 2, - "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"equals\", \"value\": \"io.metamask\" }`)." + "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`)." }, "settings": { "$ref": "#/components/schemas/ChartSettings" @@ -1653,19 +1645,27 @@ "op": { "type": "string", "enum": [ + "eq", + "neq", + "gt", + "gte", + "lt", + "lte", + "in", + "nin", + "contains", + "notEmpty", + "isEmpty", "equals", "notEquals", "greater", "greaterOrEqual", "less", "lessOrEqual", - "in", "notIn", - "includes", - "notEmpty", - "isEmpty" + "includes" ], - "description": "Comparison operator (canonical spellings only). The legacy abbreviated dialect (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) is no longer accepted — sending one returns 400; use the canonical form (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`). `includes` (case-insensitive substring) is only supported on social fields (e.g. `users.twitter`, `users.email`). `notEmpty` (\"is not empty\" / \"is set\") is a string existence check for user string attributes (device/os/utm_*/referrer/…) and social fields; `isEmpty` (\"is empty\" / \"is not set\") is its complement, supported for user string attributes only (not social fields). Both ignore `value`." + "description": "Comparison operator. The terse tokens (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `notEmpty`, `isEmpty`) are canonical; the long-form spellings (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`) are deprecated aliases that remain accepted and are folded to canonical at the boundary. `contains` (case-insensitive substring) is only supported on social fields (e.g. `users.twitter`, `users.email`). `notEmpty` (\"is not empty\" / \"is set\") is a string existence check for user string attributes (device/os/utm_*/referrer/…) and social fields; `isEmpty` (\"is empty\" / \"is not set\") is its complement, supported for user string attributes only (not social fields). Both ignore `value`. `users.lifecycle` supports only `eq` (a single stage) and `in` (a list of stages)." }, "value": { "description": "Filter value (string, number, boolean, or array). Required for every operator except the existence checks `notEmpty` and `isEmpty`, which ignore it." @@ -1781,7 +1781,7 @@ "AnalyticsFilters": { "name": "filters", "in": "query", - "description": "Array of filter conditions, JSON-encoded in the query string. Each entry is `{ field, op, value }`. Use `in` / `notIn` with a pipe-delimited `value` (e.g. `\"chrome|firefox\"`) for multi-value matching.", + "description": "Array of filter conditions, JSON-encoded in the query string. Each entry is `{ field, op, value }`. Use `in` / `nin` with a pipe-delimited `value` (e.g. `\"chrome|firefox\"`) for multi-value matching.", "content": { "application/json": { "schema": { @@ -1793,7 +1793,7 @@ "example": [ { "field": "location", - "op": "equals", + "op": "eq", "value": "US" }, { @@ -1908,7 +1908,7 @@ "example": [ { "field": "net_worth_usd", - "op": "greater", + "op": "gt", "value": "1000" } ] @@ -1951,7 +1951,7 @@ "protocol", 1, "uniswap-v3", - "greater", + "gt", "0" ] ] @@ -1976,7 +1976,7 @@ 1, "", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "greater", + "gt", "100" ] ] @@ -1998,7 +1998,7 @@ "example": [ [ 1, - "greater", + "gt", "0" ] ] @@ -2021,7 +2021,7 @@ { "event": "swap", "event_type": "track", - "op": "greaterOrEqual", + "op": "gte", "times": 1, "relative_to": "first_seen", "window": { "value": 24, "unit": "hour" } @@ -2033,7 +2033,7 @@ "AnalyticsSourceFilter": { "name": "source_filter", "in": "query", - "description": "JSON object filtering users by raw event source; which SDK/integration the event was sent from (the raw `channel` column). Format: `{ op, value }` where `op` is one of `equals`, `notEquals`, `in`, `notIn` and `value` is one of `web`, `api`, `import`, `mobile`, `server`, `onchain` (pipe-delimited for `in`/`notIn`). Distinct from `channel_filter`, which matches the classified acquisition channel.", + "description": "JSON object filtering users by raw event source; which SDK/integration the event was sent from (the raw `channel` column). Format: `{ op, value }` where `op` is one of `eq`, `neq`, `in`, `nin` and `value` is one of `web`, `api`, `import`, `mobile`, `server`, `onchain` (pipe-delimited for `in`/`nin`). Distinct from `channel_filter`, which matches the classified acquisition channel.", "content": { "application/json": { "schema": { @@ -2049,7 +2049,7 @@ "AnalyticsChannelFilter": { "name": "channel_filter", "in": "query", - "description": "JSON object filtering users by classified acquisition channel; the precomputed `channel_type` column (GA4-style attribution). Format: `{ op, value }` where `op` is one of `equals`, `notEquals`, `in`, `notIn` and `value` is one of `Direct`, `Organic Search`, `Organic Social`, `Organic Video`, `Paid Search`, `Paid Social`, `Paid Video`, `Email`, `Referrals`, `Display`, `AI`, `Referrers` (pipe-delimited for `in`/`notIn`). Distinct from `source_filter`, which matches the raw SDK source.", + "description": "JSON object filtering users by classified acquisition channel; the precomputed `channel_type` column (GA4-style attribution). Format: `{ op, value }` where `op` is one of `eq`, `neq`, `in`, `nin` and `value` is one of `Direct`, `Organic Search`, `Organic Social`, `Organic Video`, `Paid Search`, `Paid Social`, `Paid Video`, `Email`, `Referrals`, `Display`, `AI`, `Referrers` (pipe-delimited for `in`/`nin`). Distinct from `source_filter`, which matches the raw SDK source.", "content": { "application/json": { "schema": { @@ -2078,7 +2078,7 @@ [ "whale", 1, - "equals", + "eq", "true" ] ] @@ -2296,7 +2296,7 @@ "trigger_filters": [ { "name": "event", - "operator": "equals", + "operator": "eq", "value": "transaction" }, { @@ -2367,7 +2367,7 @@ }, "operator": { "type": "string", - "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `in`, `notIn`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty`) and their legacy aliases (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, `contains`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `startsWith`, `endsWith`, `contains`, `notEmpty`, `isEmpty`) and their legacy aliases (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`, `like`, `=`, `!=`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -2425,17 +2425,17 @@ { "name": "type", "value": "swap", - "operator": "equals" + "operator": "eq" }, { "name": "device", "value": "mobile", - "operator": "equals" + "operator": "eq" }, { "name": "volume", "value": "1000", - "operator": "greater", + "operator": "gt", "numericThreshold": "1000" } ], @@ -2469,12 +2469,12 @@ { "name": "utm_source", "value": "google", - "operator": "equals" + "operator": "eq" }, { "name": "utm_medium", "value": "cpc", - "operator": "equals" + "operator": "eq" } ], "recipient": [ @@ -2496,12 +2496,12 @@ { "name": "location", "value": "US", - "operator": "equals" + "operator": "eq" }, { "name": "net_worth_usd", "value": "10000", - "operator": "greaterOrEqual", + "operator": "gte", "numericThreshold": "10000" } ], @@ -2524,13 +2524,13 @@ { "name": "apps", "value": "uniswap", - "operator": "greater", + "operator": "gt", "numericThreshold": "5" }, { "name": "chains", "value": "1", - "operator": "greaterOrEqual", + "operator": "gte", "numericThreshold": "3" } ], @@ -2596,7 +2596,7 @@ "trigger_filters": [ { "name": "event", - "operator": "equals", + "operator": "eq", "value": "transaction" }, { @@ -3261,11 +3261,11 @@ "type": "event", "event": "connect", "rdns": { - "op": "equals", + "op": "eq", "value": "io.metamask" }, "chain_id": { - "op": "equals", + "op": "eq", "value": "1" } }, @@ -3326,7 +3326,7 @@ "type": "event", "event": "page", "device": { - "op": "equals", + "op": "eq", "value": "mobile" } }, @@ -3480,7 +3480,7 @@ "retentionUserFilters": [ { "field": "device", - "op": "equals", + "op": "eq", "value": "desktop" } ] @@ -4116,7 +4116,7 @@ "projectId": "proj_abc123", "title": "High net worth desktop users", "filterSet": [ - "{\"field\":\"device\",\"op\":\"equals\",\"value\":\"desktop\"}", + "{\"field\":\"device\",\"op\":\"eq\",\"value\":\"desktop\"}", "{\"field\":\"net_worth_usd\",\"op\":\"greaterOrEqual\",\"value\":100000}" ] } @@ -4168,9 +4168,9 @@ "value": { "title": "High-value mobile users", "filterSets": [ - "device::equals::mobile", - "net_worth_usd::greaterOrEqual::10000", - "utm_source::equals::paid_ads" + "device::eq::mobile", + "net_worth_usd::gte::10000", + "utm_source::eq::paid_ads" ] } }, @@ -4188,7 +4188,7 @@ "value": { "title": "Non-US users", "filterSets": [ - "location::notEquals::US" + "location::neq::US" ] } }, @@ -4197,9 +4197,9 @@ "value": { "title": "Organic power users", "filterSets": [ - "net_worth_usd::greater::1000", - "utm_source::notIn::google_ads|facebook_ads", - "lifecycle::equals::power" + "net_worth_usd::gt::1000", + "utm_source::nin::google_ads|facebook_ads", + "lifecycle::eq::power" ] } }, @@ -4208,19 +4208,19 @@ "value": { "title": "Multi-chain whales", "filterSets": [ - "chains::greaterOrEqual::3", - "net_worth_usd::greater::100000", - "apps::greater::5" + "chains::gte::3", + "net_worth_usd::gt::100000", + "apps::gt::5" ] } }, "behavioralFilterSimple": { "summary": "Users who performed 'signature' event at least once in last 30 days", - "description": "Behavioural filters use the 'events' filter key with a base64-encoded JSON array as the value. The JSON contains event name, frequency operator (greater/greaterOrEqual/equals/lessOrEqual/less), times count, and date range.", + "description": "Behavioural filters use the 'events' filter key with a base64-encoded JSON array as the value. The JSON contains event name, frequency operator (gt/gte/eq/lte/lt), times count, and date range.", "value": { "title": "Recent signers", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAic2lnbmF0dXJlIiwgIm9wZXJhdG9yIjogImdyZWF0ZXJPckVxdWFsIiwgInRpbWVzIjogMSwgInByZXNldCI6ICJsYXN0XzMwZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0wOS0xOSIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifV0=" + "events::eq::W3siZXZlbnQiOiAic2lnbmF0dXJlIiwgIm9wZXJhdG9yIjogImdyZWF0ZXJPckVxdWFsIiwgInRpbWVzIjogMSwgInByZXNldCI6ICJsYXN0XzMwZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0wOS0xOSIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifV0=" ] } }, @@ -4230,8 +4230,8 @@ "value": { "title": "Indian MetaMask users", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAiY29ubmVjdCIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5IiwgInJkbnMiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICJpby5tZXRhbWFzayJ9LCAiY2hhaW5faWQiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICIxIn19XQ==", - "location::equals::IN" + "events::eq::W3siZXZlbnQiOiAiY29ubmVjdCIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5IiwgInJkbnMiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICJpby5tZXRhbWFzayJ9LCAiY2hhaW5faWQiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICIxIn19XQ==", + "location::eq::IN" ] } }, @@ -4241,8 +4241,8 @@ "value": { "title": "Active transactors", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAicGFnZSIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDUsICJwcmVzZXQiOiAibGFzdF83ZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0xMC0xMiIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifSwgeyJldmVudCI6ICJ0cmFuc2FjdGlvbiIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5In1d", - "device::equals::desktop" + "events::eq::W3siZXZlbnQiOiAicGFnZSIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDUsICJwcmVzZXQiOiAibGFzdF83ZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0xMC0xMiIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifSwgeyJldmVudCI6ICJ0cmFuc2FjdGlvbiIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5In1d", + "device::eq::desktop" ] } } @@ -4701,7 +4701,7 @@ "conditions": [ { "field": "users.net_worth_usd", - "op": "greater", + "op": "gt", "value": 10000 } ], @@ -4714,7 +4714,7 @@ "conditions": [ { "field": "chains.1.balance", - "op": "greater", + "op": "gt", "value": 1000 } ], @@ -4727,7 +4727,7 @@ "conditions": [ { "field": "labels.coinbase.verified_account", - "op": "equals", + "op": "eq", "value": "true" } ], @@ -4740,7 +4740,7 @@ "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "greater", + "op": "gt", "value": 0, "scope": "any" } @@ -7290,7 +7290,7 @@ "example": [ { "field": "device", - "op": "equals", + "op": "eq", "value": "desktop" } ] diff --git a/api/profiles/search.mdx b/api/profiles/search.mdx index adde22d..d93eb5d 100644 --- a/api/profiles/search.mdx +++ b/api/profiles/search.mdx @@ -108,7 +108,7 @@ The search endpoint supports rich filtering capabilities through a JSON request "conditions": [ { "field": "users.net_worth_usd", - "op": "greater", + "op": "gt", "value": 1000 } ], @@ -137,20 +137,20 @@ Each condition in the `conditions` array has: | Operator | Description | Example | |----------|-------------|---------| -| `equals` | Equals | `{"field": "users.device", "op": "equals", "value": "Desktop"}` | -| `notEquals` | Not equals | `{"field": "users.os", "op": "notEquals", "value": "Windows"}` | -| `greater` | Greater than | `{"field": "users.net_worth_usd", "op": "greater", "value": 1000}` | -| `greaterOrEqual` | Greater than or equal | `{"field": "users.volume", "op": "greaterOrEqual", "value": 100}` | -| `less` | Less than | `{"field": "users.net_worth_usd", "op": "less", "value": 50000}` | -| `lessOrEqual` | Less than or equal | `{"field": "chains.1.balance", "op": "lessOrEqual", "value": 10000}` | +| `eq` | Equals | `{"field": "users.device", "op": "eq", "value": "Desktop"}` | +| `neq` | Not equals | `{"field": "users.os", "op": "neq", "value": "Windows"}` | +| `gt` | Greater than | `{"field": "users.net_worth_usd", "op": "gt", "value": 1000}` | +| `gte` | Greater than or equal | `{"field": "users.volume", "op": "gte", "value": 100}` | +| `lt` | Less than | `{"field": "users.net_worth_usd", "op": "lt", "value": 50000}` | +| `lte` | Less than or equal | `{"field": "chains.1.balance", "op": "lte", "value": 10000}` | | `in` | In array | `{"field": "users.lifecycle", "op": "in", "value": ["New", "Power user"]}` | -| `notIn` | Not in array | `{"field": "users.location", "op": "notIn", "value": ["US", "UK"]}` | -| `includes` | Case-insensitive substring match for social fields only | `{"field": "users.twitter", "op": "includes", "value": "vitalik"}` | +| `nin` | Not in array | `{"field": "users.location", "op": "nin", "value": ["US", "UK"]}` | +| `contains` | Case-insensitive substring match for social fields only | `{"field": "users.twitter", "op": "contains", "value": "vitalik"}` | | `notEmpty` | Field is set (non-empty). String fields only — value is ignored | `{"field": "users.email", "op": "notEmpty"}` | | `isEmpty` | Field is not set (empty). String user attributes only — value is ignored | `{"field": "users.utm_source", "op": "isEmpty"}` | -**Operator compatibility.** The short aliases `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `nin`, and `contains` are **no longer accepted** and return `400`. Use the canonical operators above. `notEmpty` / `isEmpty` are value-less existence checks (any `value` is ignored) and are supported on string user/profile attributes only — not on numeric metrics (`net_worth_usd`, `volume`, `revenue`, `points`), the `lifecycle` enum, or chain/app/token/label fields. +**Deprecated aliases.** The long-form spellings `equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, and `includes` are deprecated but still accepted on this endpoint — they are folded to the canonical operators above at the input boundary, so existing integrations keep working. Use the canonical operators for new integrations; the aliases may be retired in a future release. `notEmpty` / `isEmpty` are value-less existence checks (any `value` is ignored) and are supported on string user/profile attributes only — not on numeric metrics (`net_worth_usd`, `volume`, `revenue`, `points`), the `lifecycle` enum, or chain/app/token/label fields. --- @@ -175,7 +175,7 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.net_worth_usd", "op": "greater", "value": 10000 } + { "field": "users.net_worth_usd", "op": "gt", "value": 10000 } ], "logic": "and" } @@ -186,7 +186,7 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.volume", "op": "greater", "value": 5000 } + { "field": "users.volume", "op": "gt", "value": 5000 } ], "logic": "and" } @@ -206,8 +206,8 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.device", "op": "equals", "value": "Mobile" }, - { "field": "users.location", "op": "equals", "value": "US" } + { "field": "users.device", "op": "eq", "value": "Mobile" }, + { "field": "users.location", "op": "eq", "value": "US" } ], "logic": "and" } @@ -239,8 +239,8 @@ Filter by user engagement and profile data. Use the format `users.{attribute}`. ```json { "conditions": [ - { "field": "users.first_utm_source", "op": "equals", "value": "google" }, - { "field": "users.first_utm_medium", "op": "equals", "value": "cpc" } + { "field": "users.first_utm_source", "op": "eq", "value": "google" }, + { "field": "users.first_utm_medium", "op": "eq", "value": "cpc" } ], "logic": "and" } @@ -270,7 +270,7 @@ Filter by user lifecycle stage. Valid values: `At Risk`, `Churned`, `New`, `Powe Social fields support two modes: - Presence checks: use `notEmpty` to match profiles where the field is set (or `isEmpty` for the opposite). The value is ignored. -- Value matching: use `equals`, `notEquals`, or `includes` (case-insensitive substring) with a non-empty string to match the actual social value. +- Value matching: use `eq`, `neq`, or `contains` (case-insensitive substring) with a non-empty string to match the actual social value. | Field | Description | |-------|-------------| @@ -320,7 +320,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.twitter", "op": "includes", "value": "bob" } + { "field": "users.twitter", "op": "contains", "value": "bob" } ], "logic": "and" } @@ -331,7 +331,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.email", "op": "equals", "value": "alice@formo.so" } + { "field": "users.email", "op": "eq", "value": "alice@formo.so" } ], "logic": "and" } @@ -352,7 +352,7 @@ Use `chains.balance` to filter across all chains (returns profiles where **any** ```json { "conditions": [ - { "field": "chains.balance", "op": "greater", "value": 1000 } + { "field": "chains.balance", "op": "gt", "value": 1000 } ], "logic": "and" } @@ -376,7 +376,7 @@ Common chain IDs: ```json { "conditions": [ - { "field": "chains.1.balance", "op": "greater", "value": 5000 } + { "field": "chains.1.balance", "op": "gt", "value": 5000 } ], "logic": "and" } @@ -387,8 +387,8 @@ Common chain IDs: ```json { "conditions": [ - { "field": "chains.1.balance", "op": "greater", "value": 1000 }, - { "field": "chains.137.balance", "op": "greater", "value": 1000 } + { "field": "chains.1.balance", "op": "gt", "value": 1000 }, + { "field": "chains.137.balance", "op": "gt", "value": 1000 } ], "logic": "and" } @@ -409,7 +409,7 @@ Use `apps.{app_id}.balance` to filter by app balance across all chains. ```json { "conditions": [ - { "field": "apps.uniswap.balance", "op": "greater", "value": 1000 } + { "field": "apps.uniswap.balance", "op": "gt", "value": 1000 } ], "logic": "and" } @@ -424,7 +424,7 @@ Use `apps.{chain_id}.{app_id}.balance` to filter by app balance on a specific ch ```json { "conditions": [ - { "field": "apps.1.aave.balance", "op": "greater", "value": 500 } + { "field": "apps.1.aave.balance", "op": "gt", "value": 500 } ], "logic": "and" } @@ -454,7 +454,7 @@ Use `tokens.{token_address}.balance` to filter by token balance across all chain "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "greater", + "op": "gt", "value": 1000 } ], @@ -473,7 +473,7 @@ Use `tokens.{chain_id}.{token_address}.balance` to filter by token balance on a "conditions": [ { "field": "tokens.1.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "greater", + "op": "gt", "value": 500 } ], @@ -492,7 +492,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "greater", + "op": "gt", "value": 0, "scope": "protocol" } @@ -508,7 +508,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48.balance", - "op": "greater", + "op": "gt", "value": 1000, "scope": "protocol", "appId": "aave-v3" @@ -525,7 +525,7 @@ Use `scope: "protocol"` with `appId` to filter for tokens deposited in a specifi "conditions": [ { "field": "tokens.0x0000000000000000000000000000000000000000.balance", - "op": "greater", + "op": "gt", "value": 1, "scope": "protocol", "appId": "lido" @@ -554,7 +554,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.coinbase.verified_account", "op": "equals", "value": "true" } + { "field": "labels.coinbase.verified_account", "op": "eq", "value": "true" } ], "logic": "and" } @@ -565,7 +565,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.coinbase.verified_country", "op": "equals", "value": "US" } + { "field": "labels.coinbase.verified_country", "op": "eq", "value": "US" } ], "logic": "and" } @@ -576,7 +576,7 @@ Common label IDs: ```json { "conditions": [ - { "field": "labels.passport.models_aggregate_score", "op": "greaterOrEqual", "value": "50" } + { "field": "labels.passport.models_aggregate_score", "op": "gte", "value": "50" } ], "logic": "and" } @@ -596,9 +596,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "users.net_worth_usd", "op": "greater", "value": 10000 }, + { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }, { "field": "users.ens", "op": "notEmpty" }, - { "field": "chains.1.balance", "op": "greater", "value": 0 } + { "field": "chains.1.balance", "op": "gt", "value": 0 } ], "logic": "and" }' \ @@ -615,8 +615,8 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "apps.uniswap.balance", "op": "greater", "value": 0 }, - { "field": "apps.aave.balance", "op": "greater", "value": 0 } + { "field": "apps.uniswap.balance", "op": "gt", "value": 0 }, + { "field": "apps.aave.balance", "op": "gt", "value": 0 } ], "logic": "or" }' \ @@ -633,9 +633,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "labels.coinbase.verified_account", "op": "equals", "value": "true" }, + { "field": "labels.coinbase.verified_account", "op": "eq", "value": "true" }, { "field": "labels.coinbase.verified_country", "op": "in", "value": ["US", "UK", "DE"] }, - { "field": "users.lifecycle", "op": "equals", "value": "Power user" } + { "field": "users.lifecycle", "op": "eq", "value": "Power user" } ], "logic": "and" }' \ @@ -652,9 +652,9 @@ curl -sS -X GET \ -H "Content-Type: application/json" \ -d '{ "conditions": [ - { "field": "chains.1.balance", "op": "greater", "value": 10000 }, - { "field": "chains.137.balance", "op": "greater", "value": 5000 }, - { "field": "chains.42161.balance", "op": "greater", "value": 5000 } + { "field": "chains.1.balance", "op": "gt", "value": 10000 }, + { "field": "chains.137.balance", "op": "gt", "value": 5000 }, + { "field": "chains.42161.balance", "op": "gt", "value": 5000 } ], "logic": "and" }' \ diff --git a/api/query/event-timeseries.mdx b/api/query/event-timeseries.mdx index e5a8226..369bc20 100644 --- a/api/query/event-timeseries.mdx +++ b/api/query/event-timeseries.mdx @@ -8,4 +8,4 @@ Returns total event counts per day. Combine with `filters` to scope to a specifi Set `group_by` to a breakdown dimension (`channel_type`, `device`, `browser`, `os`, `location`, `referrer`, `ref`, `builder_codes`, or any UTM column) to split the series by that dimension. When set, `event_key` carries the dimension's value (empty → `Direct`) instead of the event type/name; the response keeps the top 100 values and buckets the rest as `Others`. -`filters` supports the string operators `startsWith`, `endsWith`, and `includes` (substring matches on text columns such as `referrer`, `ref`, `utm_*`) alongside the standard comparison operators. An unsupported operator now returns `400 Invalid query parameters` rather than silently matching everything. +`filters` supports the string operators `startsWith`, `endsWith`, and `contains` (substring matches on text columns such as `referrer`, `ref`, `utm_*`) alongside the standard comparison operators. An unsupported operator now returns `400 Invalid query parameters` rather than silently matching everything. diff --git a/api/query/funnel.mdx b/api/query/funnel.mdx index 82ab4d3..822fbe9 100644 --- a/api/query/funnel.mdx +++ b/api/query/funnel.mdx @@ -34,6 +34,8 @@ The `steps` query parameter is a JSON-encoded array of 2 to 10 step specs. Each - `startsWith` - `endsWith` - `includes` + - `notEmpty` / `isEmpty` - value-less existence checks (the `value` is ignored; rejected on the numeric event columns `volume`/`revenue`/`points`) + - The numeric long forms `greater`, `greaterOrEqual`, `less`, `lessOrEqual` are accepted as equivalents of `gt` / `gte` / `lt` / `lte`. - **`operand`** may target a standard event column or a JSON property on `properties`. ## Example diff --git a/cli/analytics.mdx b/cli/analytics.mdx index 7234666..9079e7a 100644 --- a/cli/analytics.mdx +++ b/cli/analytics.mdx @@ -52,7 +52,7 @@ Requires `query:read` scope on your API key. ### `--filters` -A JSON array of `{ field, op, value }` conditions, e.g. `[{"field":"location","op":"equals","value":"US"}]`. For multi-value matching, use `in` / `notIn` with a pipe-delimited `value` (e.g. `"chrome|firefox"`). +A JSON array of `{ field, op, value }` conditions, e.g. `[{"field":"location","op":"eq","value":"US"}]`. For multi-value matching, use `in` / `nin` with a pipe-delimited `value` (e.g. `"chrome|firefox"`). ### `--params` (pipe-specific) @@ -87,7 +87,7 @@ formo analytics funnel \ formo analytics top_wallets --date-from 2026-04-01 --date-to 2026-04-30 --params '{"limit":10}' # Retention restricted to US visitors -formo analytics retention --filters '[{"field":"location","op":"equals","value":"US"}]' +formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"}]' # Revenue timeseries for a specific wallet formo analytics revenue_timeseries \ diff --git a/cli/profiles.mdx b/cli/profiles.mdx index 2e29004..bccc34e 100644 --- a/cli/profiles.mdx +++ b/cli/profiles.mdx @@ -91,18 +91,18 @@ formo profiles search --order-by net_worth_usd --order-dir desc --size 5 # Search with a typed filter condition formo profiles search \ - --conditions '[{"field":"users.net_worth_usd","op":"greater","value":10000}]' \ + --conditions '[{"field":"users.net_worth_usd","op":"gt","value":10000}]' \ --size 20 # Search profiles matching either condition formo profiles search \ - --conditions '[{"field":"users.net_worth_usd","op":"greater","value":10000},{"field":"users.volume","op":"greater","value":1000}]' \ + --conditions '[{"field":"users.net_worth_usd","op":"gt","value":10000},{"field":"users.volume","op":"gt","value":1000}]' \ --logic or \ --size 20 # Filter by onchain balance on Ethereum formo profiles search \ - --conditions '[{"field":"chains.1.balance","op":"greater","value":1000}]' \ + --conditions '[{"field":"chains.1.balance","op":"gt","value":1000}]' \ --size 20 ``` @@ -114,8 +114,8 @@ The response is a paginated envelope: `{ data, total, page, size, has_more }`. ```json [ - { "field": "users.net_worth_usd", "op": "greater", "value": 10000 }, - { "field": "chains.1.balance", "op": "greaterOrEqual", "value": 1000 } + { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }, + { "field": "chains.1.balance", "op": "gte", "value": 1000 } ] ``` @@ -125,7 +125,9 @@ The response is a paginated envelope: `{ data, total, page, size, has_more }`. ### Filter Operators -`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` +`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `notEmpty`, `isEmpty` + +The long-form spellings (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`) are deprecated aliases — still accepted by the API, but new usage should stick to the canonical operators above. --- From 20c53a205de865745d9ab5ef8dcacf9e6b0564c4 Mon Sep 17 00:00:00 2001 From: Yos Riady Date: Fri, 24 Jul 2026 16:39:34 +0700 Subject: [PATCH 3/3] Team Settings nav is 'API', not 'API Keys', in openapi descriptions Co-Authored-By: Claude Fable 5 --- api/openapi.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/openapi.json b/api/openapi.json index 3652dce..dce71f6 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -29,7 +29,7 @@ "WorkspaceApiKey": { "type": "http", "scheme": "bearer", - "description": "Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard under Team Settings > API Keys." + "description": "Workspace API key (e.g. `formo_xxx`). Create one in the Formo dashboard under Team Settings > API." }, "X402Payment": { "type": "apiKey", @@ -2236,7 +2236,7 @@ } }, "x-api-scopes": { - "description": "API key scopes control access to endpoints. Create keys with the required scopes in Team Settings > API Keys.", + "description": "API key scopes control access to endpoints. Create keys with the required scopes in Team Settings > API.", "scopes": { "alerts:read": "List and get alerts", "alerts:write": "Create, update, delete alerts (requires alerts:read)",