Logs, Dashboard, and Charts
Formula Language
One language selects entries, calculates values, and produces Charts. This is the authoritative language reference.
Overview
An entry formula is logs() or logs(filter), used only at the top level. It cannot be nested, used in math, or requested in series mode. A numeric formula is math over aggregate calls. An aggregate looks at the logs in the Dashboard timeframe and returns one number. You can combine aggregates and plain numbers with + - * / and parentheses.
The Logs tab keeps its message search and level filter. It builds an entry formula from those controls; search text is literal, not formula syntax. Use the query API for full entry formulas and numeric comparisons. Dashboard and Chart editors accept numeric formulas.
1logs()
2logs(level = ERROR and data.durationMs > 1000)
3logs(data.durationMs >= 1000 and data.durationMs < 5000)
4count()
5count(level = ERROR)
6count(level in (ERROR, WARNING) and message contains "failed")
7count(flowId = "checkout-123")
8count(data.user.plan = "pro" and data.durationMs != 250)
9distinct(data.user.id)
10distinct(data.user.id, data.event = "app_used")
11sum(data.amount, data.status = "paid")
12avg(data.durationMs)
13min(data.durationMs)
14max(data.durationMs)
15count(level = ERROR) / count() * 100
16count(data.plan = "pro") - count(data.plan = "free")The same language is used in the Dashboard card editor, the chart editor, and the Admin API. Raw SQL, pipes, joins, arbitrary grouping, arrays, regex, explicit null/missing checks, and general or/not syntax are not accepted.
Aggregates
A numeric formula needs at least one aggregate. Aggregates cannot be nested inside other aggregates. Numeric aggregates only accept data fields. When no numeric values match, their result is null and the UI shows a dash.
For distinct on a data field, strings, numbers, and booleans are counted; objects and arrays are ignored. JSON numbers 1 and 1.0 are the same value.
Filters
A filter is one or more conditions joined with and. Each condition is field operator value. There is no or and no not; use in (…) to match several values.
Fields
data.user.plan. Values can be quoted text up to 200 characters, a number, or true / false.Operators
Comparisons >, >=, <, and <= require a data.* field and a numeric literal. They match only JSON numbers. Strings, booleans, nulls, arrays, objects, and missing values do not match. Thresholds need not appear in samples. Conditions cannot compare fields or contain arithmetic.
Equality is case-sensitive and type-aware. != is the complement of equality, including other types, null, and missing fields. It is not a missing-field check. Membership may mix scalar types. contains treats % and _ literally and does not promise Unicode case folding.
| Stored data.x | = 42 | != 42 | in (42, "42", true) | > 40 |
|---|---|---|---|---|
| Number 42 | true | false | true | true |
| Number 43 | false | true | false | true |
| String "42" | false | true | true | false |
| Boolean true | false | true | true | false |
| Boolean false | false | true | false | false |
| Array or object | false | true | false | false |
| Null or missing | false | true | false | false |
Values
Text values use double quotes: flowId = "checkout". Use a backslash to escape a quote or a backslash inside the text. Numbers and true / false are only allowed for data fields. Types stay separate: 1, "1", and true are three different values.
Single and double quotes are accepted. A backslash quotes the next character literally; it never creates a newline. Actual line breaks in strings are rejected. The printer escapes backslashes and double quotes. Encode the complete formula with JSON.stringify or your language’s JSON serializer.
1Formula: logs(message = "\n")
2JSON: {"formula":"logs(message = \"\\n\")"}
3Parsed text: the letter n1Formula: logs(message = "\\n")
2JSON: {"formula":"logs(message = \"\\\\n\")"}
3Parsed text: one backslash followed by nNumeric literals use decimal digits with an optional fraction and minus sign. No exponent syntax is accepted. Boolean literals are case-insensitive and print as true or false. A valid data path is accepted even when it has not appeared in the dataset.
A data key path is 1 to 200 characters after data.. Each dot-separated part must start with a letter or underscore and may then contain letters, numbers, or underscores. Keys containing dots, spaces, or hyphens are not supported.
Time and result rules
Time, page size, and series mode are request options. Saved cards and Charts store no active range. Use start: now-5m, now-15m, or now-30m with end: now. now and now-<non-negative integer><unit> accept ms, s, m, h, d, and w. Days are fixed 24-hour durations; weeks are seven days. Absolute timestamps require Z or an explicit offset. The server captures one clock for all bounds and calculations in a request and returns UTC bounds.
Entries default to All time. Either bound may be omitted or null; entry ranges have no duration or future limit. Numeric execution requires both bounds, 0 < duration ≤ 2592000000 ms (30 days), and end ≤ server time + 300000 ms. There is no timezone-change detection. A configuration-only saved read omits both bounds.
All ranges include start and exclude end. Series buckets use mathematical floor in Unix milliseconds, including before 1970. at is the aligned bucket start; point start and end are clipped to the requested range. Only non-empty intervals are returned. Counts and distinct counts in empty buckets are zero; other empty aggregates are null.
Entry pages default to 100 and allow 1–1,000; Logs UI pages use 100. Follow next unchanged for more pages. Aggregates use every matching row. Results return formula, kind, and timeframe. Numeric parts contain each unique aggregate’s formula, logsFormula, field, and raw value, in first-occurrence order. Matching logs for a field aggregate can include rows whose field did not contribute.
Z8Log never deletes log rows. Time bounds only select reads. Existing whole-dataset deletion is separate.
Math
Use + - * / and parentheses. Normal precedence applies: * and / bind tighter than + and -. Numbers can be integers or decimals. A leading - negates a value.
Division by zero gives an undefined value. The card or chart shows - and the API returns { "raw": null, "formatted": "-" }.
Empty count and distinct return 0; empty sum, avg, min, and max return null. Null operands make arithmetic null. Non-finite database aggregate results and unsafe integer results become null. Counts and distinct counts also require non-negative integers. Sums use floating-point numbers to avoid integer overflow; unsafe results become null without failing other calculations. Arithmetic uses JavaScript floating-point numbers; division by zero and non-finite results become null. A finite arithmetic result is not rejected only because it exceeds the safe integer range. Floating-point math can have rounding error.
Percentages
The card format percent only adds one decimal place and a percent sign. The formula itself must multiply by 100.
1{
2 "title": "Error rate",
3 "tone": "red",
4 "formula": "count(level = ERROR) / count() * 100",
5 "format": "percent"
6}Case and normalization
Keywords (count, distinct, sum, avg, min, max, and, in, contains), the fixed field names, and level names are not case-sensitive. Data key paths and text values are case-sensitive, except that contains ignores ASCII case when matching.
The server stores a normalized version of every formula. Spacing is fixed, keywords are lower-cased, level names are upper-cased, and in with one value becomes =. The normalized text is what you get back from the Admin API.
1COUNT( Level IN (error) AND message CONTAINS "Timeout" )/count()*100
2
3→ count(level = ERROR and message contains "Timeout") / count() * 100Limits
The page controls the timeframe. Charts also choose the time buckets. There is no way to put a timeframe, a time bucket, or a group by inside a formula.
Errors
An invalid formula is rejected with a message that names the column of the problem, for example Unknown field "user" at column 7. Use level, flowId, message, or data.key. The card editor shows the message while you type. The Admin API returns status 400 and an error that starts with Formula:.
Grammar
The full grammar in EBNF-like form.
1formula = "logs" "(" [filter] ")" | expression
2expression = term (("+" | "-") term)*
3term = factor (("*" | "/") factor)*
4factor = number | "-" factor | "(" expression ")" | aggregate
5aggregate = "count" "(" [filter] ")"
6 | "distinct" "(" field ["," filter] ")"
7 | numericAggregate "(" dataField ["," filter] ")"
8numericAggregate = "sum" | "avg" | "min" | "max"
9filter = condition ("and" condition)*
10condition = field "=" value
11 | field "!=" value
12 | field "in" "(" value ("," value)* ")"
13 | field "contains" string
14 | dataField (">" | ">=" | "<" | "<=") number
15field = "level" | "flowId" | "message" | "data" "." key ("." key)*
16dataField = "data" "." key ("." key)*
17value = string | number | "true" | "false" | levelName