Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,10 @@ formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"

> Requires `query:read` scope. Run `formo analytics <pipe> --help` for the pipe-specific params accepted via `--params`.

On `kpis`, `top_*`, `revenue_*` and `volume_by_metric`, `--params '{"page_scope":"session"}'` widens a `page` filter from page-scoped metrics (the default) to the legacy session scope.

All user-attribute, profile, social, lifecycle and resource predicates go in the single `--filters` array, using the canonical envelope with named qualifiers (`chain_id`, `app_id`, `token_address`, `scope`, `tag_id`). The retired per-family params — `socials`, `chain_filters`, `app_filters`, `token_filters`, `label_filters`, `profile_filters`, `lifecycle_filter` — are rejected with a `400` if passed through `--params`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align analytics qualifier support with the CLI validator

When a user follows this guidance for a resource predicate, such as --filters '[{"field":"chains.balance","op":"gt","value":1,"chain_id":"1"}]', the command fails locally instead of reaching the API: validateAnalyticsFilter only permits field, op, value, and optional filters (src/commands/analytics.ts:61-82), so every named qualifier listed here is rejected as an extra key. Either teach the analytics validator to accept and validate these qualifiers or avoid documenting them as usable through this CLI.

Useful? React with 👍 / 👎.


---

## `formo import`
Expand Down Expand Up @@ -500,7 +504,7 @@ formo events ingest --events '[{"type":"track","event":"First"},{"type":"track",
| Field | Type | Description |
|---|---|---|
| `field` | `string` | `users.{attribute}` or one of the four resource paths (see below) |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains` (social fields only), `notEmpty` / `isEmpty` (value-less existence checks). Long-form spellings (`equals`, `greater`, `includes`, …) are retired — the API rejects them with a `400` naming the token |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `endsWith`, `notEmpty` / `isEmpty` (value-less existence checks). Support is per field — see [operator support](#operator-support). Long-form spellings (`equals`, `greater`, `includes`, …) are retired — the API rejects them with a `400` naming the token |
| `value` | `any` | Value to compare against; must be a number on the `.balance` fields |
| `chain_id` | `string` | _(optional on any resource filter)_ restrict to one chain; omit to match any |
| `app_id` | `string` | _(required by `apps.balance`, and by `tokens.balance` with `scope: protocol`)_ e.g. `aave-v3` |
Expand All @@ -520,6 +524,21 @@ User attributes for `users.{attribute}`: `net_worth_usd`, `volume`, `revenue`,
`points`, `device`, `location`, `lifecycle`, `ens`, `farcaster`, and the other
social handles.

### Operator support

The canonical vocabulary is shared, but each field implements a subset. The API
rejects an unsupported pairing with a `400`:

| Field class | Supported operators |
|---|---|
| `chains.balance`, `apps.balance`, `tokens.balance` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` (value must be a JSON number) |
| `labels.value` | comparison operators plus `contains` (case-insensitive) |
| Numeric profile metrics (`users.net_worth_usd`, `users.volume`, `users.revenue`, `users.points`) | comparison operators |
| Routable string attributes (`users.device`, `users.os`, `users.referrer`, `users.utm_*`, `users.click_id`, and the `first_*`/`last_*` attribution variants) | full vocabulary; `contains`/`startsWith`/`endsWith` match case-sensitively |
| Social fields (`users.twitter`, `users.email`, `users.farcaster`, …) | `contains` (case-insensitive) and `notEmpty`; `startsWith`/`endsWith`/`isEmpty` are rejected |
| `users.paid_source` (and `first_`/`last_` variants) | `eq`, `neq`, `in`, `nin`, `notEmpty`, `isEmpty` — it is a fixed ad-network enum |
| `users.lifecycle` | `eq` (one stage) and `in` (a list of stages) |

Combine multiple filters with `--logic and` (default) or `--logic or`.

---
Expand Down
18 changes: 17 additions & 1 deletion SKILLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ formo profiles search --order-by tx_count --order-dir desc --page 2 --size 20 --
| Property | Type | Description |
|---|---|---|
| `field` | `string` | **Canonical path** — `users.{attribute}` or a resource path below. A bare name like `net_worth_usd` is silently ignored by the API; identifier-in-path fields like `chains.1.balance` are rejected with a `400` |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains` (social fields only), `notEmpty` / `isEmpty` (value-less existence checks). Long-form spellings (`equals`, `greater`, `includes`, …) are retired — the API rejects them with a `400` naming the token |
| `op` | `string` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `startsWith`, `endsWith`, `notEmpty` / `isEmpty` (value-less existence checks). Support is per field — see the operator table below. Long-form spellings (`equals`, `greater`, `includes`, …) are retired — the API rejects them with a `400` naming the token |
| `value` | `any` | Value to compare against; must be a number on the `.balance` fields |
| `chain_id` | `string` | _(optional on any resource filter)_ restrict to one chain; omit to match any |
| `app_id` | `string` | _(required by `apps.balance`, and by `tokens.balance` with `scope: protocol`)_ e.g. `aave-v3` |
Expand All @@ -142,6 +142,18 @@ User attributes for `users.{attribute}`: `net_worth_usd`, `volume`, `revenue`,
`points`, `device`, `location`, `lifecycle`, `ens`, `farcaster`, and the other
social handles.

**Operator support per field** — the vocabulary is shared, but each field implements a subset and the API rejects an unsupported pairing with a `400`:

| Field class | Supported operators |
|---|---|
| `chains.balance`, `apps.balance`, `tokens.balance` | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` (value must be a JSON number) |
| `labels.value` | comparison operators plus `contains` (case-insensitive) |
| Numeric profile metrics (`users.net_worth_usd`, `users.volume`, `users.revenue`, `users.points`) | comparison operators |
| Routable string attributes (`users.device`, `users.os`, `users.referrer`, `users.utm_*`, `users.click_id`, `first_*`/`last_*` variants) | full vocabulary; `contains`/`startsWith`/`endsWith` match case-sensitively |
| Social fields (`users.twitter`, `users.email`, `users.farcaster`, …) | `contains` (case-insensitive) and `notEmpty`; `startsWith`/`endsWith`/`isEmpty` are rejected |
| `users.paid_source` (and `first_`/`last_` variants) | `eq`, `neq`, `in`, `nin`, `notEmpty`, `isEmpty` — it is a fixed ad-network enum |
| `users.lifecycle` | `eq` (one stage) and `in` (a list of stages) |

Combine multiple filters with `--logic and` (default) or `--logic or`.

---
Expand Down Expand Up @@ -233,6 +245,10 @@ formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"

Each pipe accepts pipe-specific params via `--params` (see each command's `--help`): e.g. `funnel` → `steps`, `window_seconds`, `funnel_type`, `group_by`, `limit`, `attribution`; `kpis` → `group_by`, `limit`; `top_*` → `limit`, `offset`.

On `kpis`, `top_*`, `revenue_*` and `volume_by_metric`, `--params '{"page_scope":"session"}'` widens a `page` filter from page-scoped metrics (the default) to the legacy session scope.

All user-attribute, profile, social, lifecycle and resource predicates now go in the single `--filters` array, using the same canonical envelope with named qualifiers (`chain_id`, `app_id`, `token_address`, `scope`, `tag_id`). The retired per-family params — `socials`, `chain_filters`, `app_filters`, `token_filters`, `label_filters`, `profile_filters`, `lifecycle_filter` — are rejected with a `400` if passed through `--params`.

---

## Project Alerts
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@formo/cli",
"version": "1.1.1",
"version": "1.2.0",
"packageManager": "pnpm@11.1.2",
"engines": {
"node": ">=22.12"
Expand Down
9 changes: 7 additions & 2 deletions src/commands/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,8 +405,13 @@ profiles.command('search', {
'apps.balance (+ "app_id", optional "chain_id"); ' +
'tokens.balance (+ "token_address", "scope":"any"|"protocol", "app_id" when scope is "protocol", optional "chain_id"); ' +
'labels.value (+ "tag_id", optional "chain_id"). ' +
'op: eq, neq, gt, gte, lt, lte, in, nin, contains, notEmpty, isEmpty ' +
'(contains = substring, social fields only; notEmpty/isEmpty = value-less existence checks on string fields). ' +
'op: eq, neq, gt, gte, lt, lte, in, nin, contains, startsWith, endsWith, notEmpty, isEmpty. ' +
'Operator support is per field: the .balance fields take comparison operators only; ' +
'contains works on routable string attributes (users.device, users.os, users.referrer, users.utm_*, users.click_id — case-sensitive), ' +
'on social fields and on labels.value (case-insensitive); ' +
'startsWith/endsWith are routable string attributes only; ' +
'notEmpty/isEmpty are value-less existence checks on string fields; ' +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify existence-operator support in profile help

When users consult profiles search --help for social or lifecycle string fields, this sentence advertises both existence operators even though the support table added by the same change says isEmpty is rejected for social fields and users.lifecycle accepts only eq and in (README.md:538-540). Following the generated CLI help therefore produces an API 400; list the actual eligible string field classes or explicitly say these operators apply only where supported.

Useful? React with 👍 / 👎.

'users.lifecycle takes only eq and in. ' +
'Retired and rejected with a 400: identifier-in-path fields (chains.1.balance, apps.uniswap-v3.balance, tokens.0x….balance, labels.vip), ' +
'the "appId" spelling, and long-form operators (equals, notEquals, greater, greaterOrEqual, less, lessOrEqual, notIn, includes).',
),
Expand Down