| name | edg-expression |
| description | Help compose edg expressions. Explain functions, debug syntax, and generate the right incantation for a given use case. |
| user-invocable | true |
edg Expression Helper
You help users compose, debug, and understand edg expressions. edg uses expr-lang/expr as its expression engine, extended with ~60 built-in functions for data generation, distribution sampling, and reference data access.
What you do
- Explain what a specific function does and when to use it
- Compose expressions for a described use case
- Debug expression syntax errors
- Suggest the REPL for interactive testing:
edg repl
Expressions in YAML vs DSL
All expressions are identical in both formats - same functions, same syntax. Only the surrounding config structure differs:
YAML:
args:
- ref_rand('fetch_users').id
- gen('email')
DSL (positional):
query_name `SELECT ...` (ref_rand('fetch_users').id, gen('email'))
DSL (named):
query_name `SELECT ...` (user_id: ref_rand('fetch_users').id, email: gen('email'))
When helping with expressions, ask which format the user is working with if it affects the answer (e.g., wrapping syntax). The expression itself is always the same.
Quick Reference
Identifiers
| Function | Description |
|---|
uuid_v7() | Sortable UUID (preferred for PKs) |
uuid_v4() | Random UUID |
seq(start, step) | Auto-incrementing counter per worker |
seq_alpha(length) | Auto-incrementing alpha sequence per worker (aaa, aab, aac, ...) |
seq_global(name) | Shared auto-incrementing counter across all workers (requires seq: config section) |
seq_alpha_global(name) | Shared auto-incrementing alpha sequence across all workers (requires seq: config with length) |
seq_rand(name) | Uniform random from generated sequence values |
seq_zipf(name, s, v) | Zipfian-distributed pick from sequence (hot early values) |
seq_norm(name, mean, stddev) | Normal-distributed pick from sequence |
seq_pareto(name, alpha) | Pareto-distributed pick from sequence (continuous power-law) |
seq_exp(name, rate) | Exponential-distributed pick from sequence |
seq_lognorm(name, mu, sigma) | Log-normal-distributed pick from sequence |
objectid() | MongoDB ObjectID (24-char hex string) |
Random Data (gofakeit)
| Function | Description |
|---|
gen('email') | Random email |
gen('firstname') | Random first name |
gen('lastname') | Random last name |
gen('number:1,100') | Random int in range |
gen('sentence:5') | Random sentence |
regex('[A-Z]{3}-[0-9]{4}') | String matching regex |
bool() | Random true/false |
Numeric Distributions
| Function | Description |
|---|
uniform(min, max) | Flat/even distribution |
uniform_f(min, max, precision) | Uniform float with decimal places |
norm(mean, stddev, min, max) | Bell curve |
norm_f(mean, stddev, min, max, precision) | Bell curve float |
exp(rate, min, max) | Exponential decay |
lognorm(mu, sigma, min, max) | Right-skewed long tail |
pareto(alpha, max) | Continuous power-law, lower values dominate |
zipf(s, v, max) | Power-law hot keys |
nurand(A, x, y) | TPC-C non-uniform random |
Set Distributions
| Function | Description |
|---|
set_rand(values, weights) | Uniform or weighted pick from a set |
set_norm(values, mean, stddev) | Normal distribution over set indices |
set_exp(values, rate) | Exponential over set indices |
set_pareto(values, alpha) | Pareto over set indices (continuous power-law) |
set_zipf(values, s, v) | Zipfian over set indices |
set_lognorm(values, mu, sigma) | Log-normal over set indices |
Dates & Times
| Function | Description |
|---|
timestamp(min, max) | Random RFC3339 timestamp |
timestamp_step() | Next monotonic timestamp (requires timestamp_steps in count:) |
timestamp_steps(min, max, interval_or_count) | Count of steps between min and max; third arg is interval string ('5m') or integer count (10000). Sets up timestamp_step() |
date(format, min, max) | Random formatted date |
date_offset(duration) | Now +/- duration |
duration(min, max) | Random Go duration string |
time(min, max) | Random HH:MM:SS |
Strings & Formatting
| Function | Description |
|---|
template(format, args...) | Go fmt.Sprintf |
json_obj(k1, v1, ...) | Build JSON object |
json_arr(minN, maxN, pattern) | Build JSON array |
array(minN, maxN, pattern) | PostgreSQL array literal |
vector(dims, clusters, spread) | vector literal with clustered unit vectors (uniform) |
vector_pareto(dims, clusters, spread, alpha) | vector literal with Pareto centroid selection |
vector_zipf(dims, clusters, spread, s, v) | vector literal with Zipfian centroid selection |
vector_norm(dims, clusters, spread, mean, stddev) | vector literal with normal centroid selection |
embed(text...) | Real vector embedding via external API (OpenAI-compatible). Variadic, joins args with space. Requires a license and --embed-api-key |
LLM / Structured Generation
| Function | Description |
|---|
complete(tool_name, prompt) | Generate structured data via LLM tool calling. Returns a map; access fields with dot notation. Retries up to 3 times on missing/invalid tool calls. Validates response types against tool schema. 120s per-request timeout |
complete_array(tool_name, prompt, count) | Generate N structured items in a single LLM call. Returns []map; use with ref_each() to iterate. Schema auto-wrapped in array. Memoized by (tool, prompt, count). Same retry/validation as complete() |
Use locals to call once per row and access multiple fields:
locals:
review: 'complete("review", "Review: " + name)'
args:
- local("review").review_text
- local("review").sentiment
- local("review").rating
locals:
reviews: 'complete_array("review", "Generate 5 reviews", 5)'
args:
- ref_each(local("reviews")).review_text
- ref_each(local("reviews")).sentiment
- ref_each(local("reviews")).rating
Requires a license and --complete-api-key (or EDG_COMPLETE_API_KEY). Configure endpoint with --complete-url, model with --complete-model. Any OpenAI-compatible API works (Ollama, vLLM, etc.). Tool schema defined in complete: YAML section.
Reference Data
| Function | Scope | Description |
|---|
ref_rand(name) | None | Fresh random row every call |
ref_same(name) | Per-query | Same row within one query execution |
ref_diff(name) | Per-query | Unique row per call within one query |
ref_perm(name) | Per-worker | Fixed row for worker lifetime |
ref_each(query_or_dataset) | Expansion / Per-query | SQL string: one execution per returned row. Named dataset (unquoted): sequential iteration with same-row caching |
ref_n(name, field, min, max) | None | N unique values as CSV string |
ref_norm(name, mean, stddev) | None | Row picked via normal distribution |
ref_exp(name, rate) | None | Row picked via exponential distribution |
ref_lognorm(name, mu, sigma) | None | Row picked via log-normal distribution |
ref_pareto(name, alpha) | None | Row picked via Pareto distribution |
ref_zipf(name, s, v) | None | Row picked via Zipfian distribution |
Aggregation (over named datasets)
| Function | Description |
|---|
count(name) | Row count |
sum(name, field) | Sum of field |
avg(name, field) | Average of field |
min(name, field) | Minimum of field |
max(name, field) | Maximum of field |
distinct(name, field) | Count of distinct values |
Conditionals & Dependencies
| Function | Description |
|---|
arg(index) | Reference earlier arg by zero-based index |
arg('name') | Reference earlier arg by name (named args only) |
result() | First row of current query's SELECT result (post_print only) |
cond(pred, trueVal, falseVal) | Ternary conditional (inline, for args) |
nullable(expr, probability) | NULL with given probability |
coalesce(v1, v2, ...) | First non-nil value |
const(value) | Literal constant |
fail(message) | Stop current worker gracefully with error |
fatal(message) | Terminate entire process immediately |
Control Flow (config-level, not expression functions)
Conditional branching uses the same expr-lang expressions but at the config level, not inside args:
if: condition must evaluate to boolean (e.g., "ref_same('x').balance >= local('amount')")
match: expression can be any type; compared to each eq: value as strings
eq: expressions can be any type (string literals need inner quotes: "'savings'")
- All conditional expressions have access to
ref_same(), ref_rand(), local(), global(), etc.
Correlated Totals
| Function | Description |
|---|
distribute_sum(total, minN, maxN, precision) | N random parts summing exactly to total (comma-separated) |
distribute_weighted(total, weights, noise, precision) | Split total by proportional weights with controlled noise (0=exact, 1=random) |
Multi-Value
| Function | Description |
|---|
nurand_n(A, x, y, minN, maxN) | N unique NURand values (comma-separated) |
norm_n(mean, stddev, min, max, minN, maxN) | N unique normally-distributed values (comma-separated) |
weighted_sample_n(name, field, weightField, minN, maxN) | N weighted unique rows from a dataset (comma-separated) |
Batch
| Function | Description |
|---|
batch(n) | Sequential indices [0, n) |
gen_batch(total, size, pattern) | Batched gofakeit values |
iter() | 1-based row counter for batch queries (resets per query) |
Uniqueness
| Function | Description |
|---|
uniq(expression) | Retry expression until unique value found (100 attempts default) |
uniq(expression, maxRetries) | Same, with custom retry limit |
uniq(expr1, expr2 [, ...]) | Composite uniqueness - retries until the tuple is unique. Returns []any; index to pick column. Same-row calls with identical expressions return cached tuple |
uniq(expr1, expr2, maxRetries) | Composite with custom retry limit |
PII & Masking
| Function | Description |
|---|
gen_locale('first_name', 'ja_JP') | Locale-aware name/address/phone generation |
gen_locale('name', 'de_DE') | Full name in locale order (eastern = last+first, western = first last) |
mask(value) | Deterministic 16-char hex token (same input -> same output) |
mask(value, length) | Hex token truncated to length chars |
mask(value, 'base64') | Base64-encoded token |
mask(value, 'base32') | Base32-encoded token |
mask(value, 'asterisk') | Repeated * characters (default 16) |
mask(value, 'asterisk', 4) | 4 asterisks |
mask(value, 'redact') | Fixed string [REDACTED], length ignored |
mask(value, 'email') | Preserves @domain, masks local part with * |
mask(value, 'email', 4) | ****@domain.com |
Supported locales: en_US, ja_JP, de_DE, fr_FR, es_ES, pt_BR, zh_CN, ko_KR (aliases: ja, de, fr, etc.)
Iteration Counter
| Function | Description |
|---|
global_iter() | Monotonic iteration counter shared across all workers. Increments with every query execution regardless of which statement runs. Use with math functions to make data change shape over the life of a workload |
Math
| Function | Description |
|---|
abs(x) | Absolute value |
acos(x) | Arc cosine (radians) |
asin(x) | Arc sine (radians) |
atan(x) | Arc tangent (radians) |
atan2(y, x) | Two-argument arc tangent (radians) |
ceil(x) | Smallest integer >= x |
cos(x) | Cosine (radians) |
floor(x) | Largest integer <= x |
log(x) | Natural logarithm |
log10(x) | Base-10 logarithm |
mod(x, y) | Floating-point remainder |
pi | Pi constant (3.14159...) |
pow(x, y) | x raised to the power y |
sin(x) | Sine (radians) |
sqrt(x) | Square root |
tan(x) | Tangent (radians) |
Other
| Function | Description |
|---|
blob(n) | Random n bytes as raw binary (cross-database) |
bytes(n) | Hex-encoded random bytes (pgx only) |
bit(n) | Fixed-length bit string |
varbit(n) | Variable-length bit string |
inet(cidr) | Random IP in CIDR block |
ltree(parts...) | PostgreSQL ltree path from dot-joined parts |
point(lat, lon, radiusKM) | Random geographic point (map) |
point_wkt(lat, lon, radiusKM) | Random point as WKT string |
polygon(lat, lon, minKM, maxKM, points) | Jagged polygon vertices ([]map with .lat/.lon) |
polygon_wkt(lat, lon, minKM, maxKM, points) | Jagged polygon as WKT POLYGON string |
Common Patterns
Mutually exclusive columns
args:
- bool()
- cond(arg(0), gen('email'), nil)
- cond(!arg(0), gen('phone'), nil)
Computed total from earlier args
args:
- uniform_f(1.00, 99.99, 2)
- gen('number:1,10')
- arg(0) * float(arg(1))
args:
price: uniform_f(1.00, 99.99, 2)
quantity: gen('number:1,10')
total: arg('price') * float(arg('quantity'))
Full name from parts
args:
- gen('firstname')
- gen('lastname')
- arg(0) + ' ' + arg(1)
args:
first: gen('firstname')
last: gen('lastname')
full: arg('first') + ' ' + arg('last')
Weighted category selection
args:
- set_rand(['electronics', 'clothing', 'books', 'food'], [40, 30, 20, 10])
Hot-key access (Zipfian)
args:
- zipf(2.0, 1.0, 999)
Hotspot rows (distribution-based ref)
args:
- ref_zipf('fetch_ids', 2.0, 1.0).id
- ref_pareto('fetch_ids', 2.0).id
- ref_norm('fetch_ids', 0.5, 0.2).id
- ref_exp('fetch_ids', 1.5).id
- ref_lognorm('fetch_ids', 0.0, 0.5).id
Worker-pinned partition
args:
- ref_perm('fetch_warehouses').w_id
Invoice line items (distribute_sum)
args:
- uuid_v4()
- uniform_f(100, 10000, 2)
- distribute_sum(arg(1), 3, 7, 2)
Subtotal/tax/shipping breakdown (distribute_weighted)
args:
- uniform_f(100, 10000, 2)
- distribute_weighted(arg(0), [85, 10, 5], 0.1, 2)
Masking PII
args:
first_name: gen_locale('first_name', 'ja_JP')
last_name: gen_locale('last_name', 'ja_JP')
full_name: arg('last_name') + arg('first_name')
email: gen('email')
masked_name: mask(arg('full_name'))
masked_email: mask(arg('email'), 'email', 4)
redacted_phone: mask(arg('phone'), 'redact')
Unique codes in batch inserts
- name: populate_airport
type: exec_batch
count: 200
size: 100
args:
- iter()
- uniq("gen('airlineairportiata')")
query: |-
INSERT INTO airport (id, code) VALUES ($1::INT, $2)
Composite uniqueness across columns
- name: populate_people
type: exec_batch
count: 500
size: 100
args:
- uniq("gen('first_name')", "gen('last_name')")[0]
- uniq("gen('first_name')", "gen('last_name')")[1]
- gen('email')
query: |-
INSERT INTO people (first_name, last_name, email) VALUES ($1, $2, $3)
Real embeddings
args:
- gen('productname')
- gen('sentence:3')
- embed(arg(0), arg(1))
query: |-
INSERT INTO product (name, description, embedding)
VALUES ($1, $2, $3::VECTOR)
args:
- ref_each(product_catalog).name
- ref_each(product_catalog).description
- embed(ref_each(product_catalog).name, ref_each(product_catalog).description)
query: |-
INSERT INTO product (name, description, embedding)
VALUES ($1, $2, $3::VECTOR)
Requires a license and --embed-api-key (or EDG_EMBED_API_KEY). Configure endpoint with --embed-url, model with --embed-model, dimensions with --embed-dimensions, max batch size with --embed-max-batch. Any OpenAI-compatible API works (Ollama, vLLM, etc.).
Error handling with fail/fatal
args:
- {'us': 'us-east-1', 'eu': 'eu-west-1'}[env('REGION')] ?? fail('unknown REGION')
- fatal('missing required config')
fail() stops only the current worker; fatal() terminates the entire process.
Temporal patterns (global_iter + math)
Combine global_iter() with math functions to make generated data change shape over the life of a workload. Define total_iters as a global and estimate it from: workers * duration_seconds / avg_latency_seconds.
zipf(initial_skew + (final_skew - initial_skew) * global_iter() / total_iters, 1, products - 1)
floor(base_price * (1.0 + log(1.0 + global_iter() / 1000.0)) * 100.0) / 100.0
floor(abs(base_traffic + 0.5 * sqrt(global_iter()) + amplitude * sin(2.0 * pi * global_iter() / period)))
pow(cos(pi * mod(global_iter(), interval) / interval), 2.0) * 10.0
100.0 + (2.0 * atan(sqrt(global_iter()) / 100.0) / pi) * 15.0 + norm_f(0, 0.5, -2, 2, 2)
Debugging Tips
- Use
edg repl to test any expression interactively without a database
- Expressions are compiled at startup; syntax errors will be caught immediately
arg(index) is zero-based and only works within the same query's args list; arg('name') works with named args (map-style args:)
- Named and positional arg forms are mutually exclusive per query
ref_same resets between queries; ref_perm never resets
- Set distributions accept expr-lang array literals:
['a', 'b', 'c']
- Weights in
set_rand are relative, not percentages ([40, 30, 20, 10] and [4, 3, 2, 1] behave identically)