Skip to main content
The Intelligence SQL editor runs read-only SELECT queries over a curated, organization-scoped schema. Every table below is pre-filtered to your workspace before your query runs, so a query can never read another organization’s data — even if you join across tables.

Query rules

Also enforced:
  • No semicolons anywhere in the query.
  • No SQL comments--, /*, and */ are all rejected.
  • No double quotes, so no quoted identifiers. Use bare snake_case names.
  • Your query must begin with SELECT. A leading WITH is rejected — see Common gotchas.
  • Every table after FROM or JOIN must be one of the eleven tables listed below.
Saved SQL runs are free and never consume AI credits. Credits apply only when you generate SQL from a prompt.
Identical queries completing within 10 minutes of each other are served from cache and return cacheHit: true without re-executing.

Reading the type column

TIMESTAMP values serialize as ISO 8601 strings. JSONB columns come back as parsed objects. Note that BIGINT and NUMERIC values are returned as strings, not numbers — all the aggregate counts and rate columns in campaign_performance and the email_* counters in contact_360 are affected. Cast in your client before doing arithmetic.

Tables

app_events

Custom events you send from your own product. This is how product behavior — signups, deposits, upgrades — becomes queryable alongside on-chain and messaging data.
Events sent with test: true are excluded from this table by design, so your test traffic never contaminates a segment or report.
Use occurred_at rather than created_at for time windows — occurred_at is when the event happened in your system, created_at is when we received it.

contacts

Your audience contacts — wallet-first identity with optional email and name.

delivery_events

Per-recipient message events across email and in-app push.
campaign_run_id identifies a send run, not a campaign. To attribute events to a campaign, join on metadata->>'campaignId' — there is no campaign_id column.

audience_profiles

Wallet profiles with channel reachability and ENS. Keyed by (organization_id, wallet_address) — there is no id column.

audience_segments

Saved segments — wallet lists or rule criteria.

campaigns

Campaigns with status, type, and UTM parameters lifted out of the tracking JSON.

campaign_performance

Per-campaign engagement rollup, ready for reporting. Derived — one row per campaign, no per-recipient grain. sent counts both email.sent and email.queued. Rates are computed as unique events over sent, rounded to 4 decimal places, and are NULL when nothing has been sent.

user_onchain_metrics

Enriched per-wallet metrics: portfolio value, 90-day activity, LTV estimate, engagement score. One row per wallet.

wallet_activity_summary

Activity summary per wallet per chain.

nft_holdings

NFT holdings per wallet: collections, items, floor prices. One row per (wallet, chain, contract, token_id).
balance is TEXT because ERC-1155 balances can exceed native integer range. Cast it (balance::numeric) before comparing or summing.

contact_360

The customer-360 view — one row per contact, joining identity, on-chain metrics, activity, and email engagement, plus derived churn signals. This is usually the fastest place to start. contact_360 has no organization_id column — it’s already scoped to your workspace.

Column values

Several TEXT columns carry a fixed or conventional set of values.

churn_risk

Exactly one of these, never null:

channel

Uppercase: EMAIL, INAPP, TELEGRAM, X, FARCASTER, DISCORD.

event_type

Open-ended, lowercase and dot-separated. The common values are email.queued, email.sent, email.delivered, email.open, email.click, email.bounce, email.complaint, email.unsubscribe, email.failed, and email.suppressed. Because new provider statuses pass through as email.<status>, prefer IN (...) over NOT IN (...) when filtering.

status and type (campaigns)

status: DRAFT, SCHEDULED, SENDING, SENT, ARCHIVED, PAUSED. type: EMAIL_BLAST, DRIP, SMART_SENDING.

chain and primary_chain

Chain slugs such as eth-mainnet, base-mainnet, matic-mainnet, arbitrum-mainnet, optimism-mainnet, bsc-mainnet, sei-mainnet, and solana-mainnet, plus testnets like base-sepolia-testnet and eth-sepolia. Defaults to eth-mainnet when unspecified.

Joining tables

Joins are fully supported, including UNION, subqueries, and window functions. Because every table is org-scoped before your query runs, cross-tenant joins are structurally impossible.
Wallet casing is not normalized on contacts. Always join wallets with lower() on both sides or you’ll silently drop rows.
wallet_activity_summary and nft_holdings are per-chain and per-token respectively, so joining them to a per-wallet table fans out rows. Aggregate or filter on chain first.

Common gotchas

The validator requires the query to begin with SELECT, so a top-level CTE is rejected. Rewrite it as a subquery:
The read-only check scans for banned keywords as plain substrings, so ordinary text can trip it. DO , SET , CALL , and COPY are the easy ones to hit:
Restructure the predicate or match on a different pattern.
Every name after FROM or JOIN is checked against the ten tables above, including names you define yourself. Alias your subqueries rather than referencing them as tables.
BIGINT and NUMERIC columns serialize as strings. open_rate of 0.2500 arrives as "0.2500". Cast client-side before comparing or charting.

Error messages

Example queries