Skip to content

Syntax ​

The whole grammar by example: comments, let, addresses, ABI literals, the three places with { … } may go, and ranges.

A query ​

cql
// A comment runs to the end of the line.
/* A block comment
   can span several. */
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;

from stETH at latest-21600..latest every 7200 as s
| extend ethPerShare = s.getPooledEthByShares(1e18)
| where ethPerShare > 0
| project { block: $block, ethPerShare: format(ethPerShare, 18) }

Open in workbench →

A query is any number of let statements, then one pipeline: a source and zero or more operators, each introduced by |. Keywords are case-insensitive; names are not. Whitespace and line breaks are free, so a long pipeline can be laid out one operator per line.

let ​

cql
let stETH  = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;                      // a contract
let lido   = abi ['function getPooledEthByShares(uint256) view returns (uint256)'];     // an ABI
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
let one    = 1e18;                                                                       // a value
let queue  = ethereum:0x889edC2eDab5f40e902b864aD4d7AdE8E412F9B1 with { abi: 'WithdrawalQueue' }; // with options

from stETH with { abi: lido } as s
| project { ethPerShare: s.getPooledEthByShares(one) with { block: 26_000_000 } }

Open in workbench →

A let binds a name and ends with ;. It can hold an address (optionally with with { … } attached, so the name carries its options), an ABI, or any expression that does not read from a row. Names are looked up after aliases and columns, so a column called wstETH would shadow the let — CQL warns (CQL2900) when that happens. A let may not use a built-in ABI's name.

Addresses ​

An address is a chain, a colon and forty hex digits: ethereum:0x…, base:0x…. Write it in lowercase, or in the mixed case of its checksum — a mixed-case address with a wrong letter is refused (CQL1006) with the corrected spelling in the hint. There is no address without a chain.

Literals ​

KindExamples
whole numbers1000000, 1_000_000, 1e18, 1.5e18
decimals1.05, 0.5
hex bytes0xff, 0x3ca7c3e38968823ccb4c78ea688df41356f182ae1d159e4ee608d30d68cef320
strings'TokenRebased', "it's", 'a \'quoted\' word', 'é'
durations30s, 15m, 1h, 7d, 2w
dates and timesdatetime('2026-09-01'), datetime('2026-09-01T12:00:00Z')
booleans and nulltrue, false, null
arrays[a, b, c], [] where the type is known
records{ block: $block, modules: r.getStakingModuleIds() }

Hex has an even number of digits and is bytes, not a number; tobigint(0xff) is the number. Strings take either quote and the escapes \\, \', \", \n, \t, \r and \uXXXX; a string cannot contain a raw line break.

ABI literals ​

cql
let a = abi ['function getTotalShares() view returns (uint256)',
             'event TokenRebased(uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, uint256 postTotalShares, uint256 postTotalEther, uint256 sharesMintedAsFees)'];
let b = abi [{ "type": "function", "name": "getTotalShares", "inputs": [], "outputs": [{ "type": "uint256" }], "stateMutability": "view" }];

from ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 with { abi: a } as s
| project { totalShares: s.getTotalShares() }

Open in workbench →

After abi, write a list of human-readable signatures, a JSON ABI array, a library name in quotes (abi 'lido'), or none. An event signature may carry indexed and parameter names; anonymous events are not supported.

with { … } in three places ​

cql
let stETH  = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84 with { abi: erc20 };  // on the name
let wstETH = ethereum:0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;

from stETH with { caller: wstETH } as t                                               // on the source
| project { balance: t.balanceOf(wstETH) with { block: 26_000_000 } }                // on one call

Open in workbench →

The options are abi, caller, block and value. The innermost clause wins: a with on a call overrides the source's, which overrides the name's. Written after an address-valued expression rather than a call — locator with { abi: v } — the options apply to every call made through that value. with options has each option.

Ranges ​

WrittenReads
at latest, at finalizedone block: the newest, or the newest the chain will not reorganise
at 26_000_000one block by number
at latest-7200..latesta block range, both ends included
at 25_800_000..26_000_000 every 7200the range, at multiples of 7 200 plus the end
at time '2026-09-01'..'2026-09-08'a range by timestamp
at time '2026-09-01'..'2026-09-08' every 1dthe same, at day boundaries plus the end

every takes a number on a block range and a duration on a time range, not the other way round.

Names ​

A name is letters, digits and underscores, not starting with a digit. The words CQL keeps for itself are let, from, with, where, project, extend, expand, summarize, join, union, order, take, distinct, and, or, not, in, between, true, false, null, latest and finalized; a column with one of those names is written in backticks, `from`, or reached through its alias, t.from. Thirteen more — abi, as, asc, at, by, desc, events, every, index, kind, none, on, time — are keywords only in their position and are otherwise ordinary names.

Precedence, lowest first ​

c ? a : b · or · and · not · == != < <= > >= in between · + - · * /% · unary - · .member [index] (call) as type with { … }.

Two consequences worth knowing: not a == b means not (a == b), and a < b < c is refused — write a < b and b < c.

See also ​