Declarative Expressions & Conditions
Learn how to write type-safe, Cloud-safe expressions for field visibility, live preview URLs, access control rules, and reactive form hooks using the expr expression builder.
When configuring field visibility, live preview routing, access control policies, or reactive form transforms in Dyrected, you can write declarative expressions.
Dyrected provides the type-safe expr expression builder (@dyrected/sdk or @dyrected/core) as the recommended standard for constructing these expressions. Because expr compiles to pure serializable JEXL strings, your logic remains 100% portable: it survives npx dyrected sync:schema and runs identically across self-hosted servers, headless API runtimes, and Dyrected Cloud.
By the end of this guide, you will understand how to build reactive conditions with expr, how to configure preview URLs and access rules, and how expressions work under the hood.
Quickstart
Import the expr expression builder from @dyrected/sdk (or @dyrected/core):
import { defineCollection, defineNumberField, defineTextField, expr } from "@dyrected/sdk";
export const Products = defineCollection({
slug: "products",
admin: {
useAsTitle: "title",
// Preview URL routing: /products/slug
previewUrl: expr.concat("/products/", "slug"),
},
access: {
// Only admins or the document creator can edit
update: expr.any(expr.access.isOwner("author"), expr.access.isAdmin()),
},
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineTextField({ name: "couponCode", label: "Coupon Code" }),
defineNumberField({
name: "discountPercent",
label: "Discount Percentage",
// Reveals this field only when a coupon code is entered
admin: {
condition: expr.notEmpty("couponCode"),
},
}),
],
});1. Conditional Field & Block Visibility (admin.condition)
Use admin.condition on any field or block to conditionally reveal it based on the editor's current form input or block variant.
Match by Field Value
Check if a field equals a specific value or is non-empty:
import { defineNumberField, defineTextField, expr } from "@dyrected/sdk";
// Show field when 'hasCustomTax' is checked
export const CustomTaxField = defineNumberField({
name: "taxRate",
label: "Tax Rate (%)",
admin: {
condition: expr("hasCustomTax").isTrue(),
},
});
// Show field when category is in a set of allowed values
export const DeliveryDateField = defineTextField({
name: "deliveryWindow",
label: "Delivery Window",
admin: {
condition: expr("category").in("furniture", "appliances", "perishables"),
},
});Match Block Variants
In layout builders and polymorphic blocks, reveal fields only when a specific block layout variant is selected:
import { defineMediaField, expr } from "@dyrected/sdk";
export const AvatarField = defineMediaField({
name: "avatar",
label: "Author Avatar",
admin: {
// Only visible when the editor selects the 'card' or 'split' variant
condition: expr.variant("card", "split"),
},
});Compound Conditions (expr.all, expr.any, expr.not)
Combine multiple conditions using boolean combinators:
// Visible if status is draft AND current user has editor/admin role
admin: {
condition: expr.all(
expr("status").equals("draft"),
expr.userRole("admin", "editor")
),
}
// Visible if stock is below 10 OR promoted is true
admin: {
condition: expr.any(
expr("stock").lessThan(10),
expr("isPromoted").isTrue()
),
}2. Live Preview URLs & Dynamic Routing (admin.previewUrl)
The admin.previewUrl setting determines the frontend route loaded in the live preview panel and the external view button.
Path Concatenation (expr.concat)
Safely combine static path prefixes with dynamic field identifiers:
import { defineCollection, expr } from "@dyrected/sdk";
export const Articles = defineCollection({
slug: "articles",
admin: {
// Generates: '/news/' + slug
previewUrl: expr.concat("/news/", "slug"),
},
fields: [ ... ],
});Fallback Routing (expr.ifElse)
Handle root homepages or custom path overrides with ternary expressions:
// If slug is 'home', route to '/'; otherwise route to '/' + slug
admin: {
previewUrl: expr.ifElse(
expr("slug").equals("home"),
"/",
expr.concat("/", "slug")
),
}Multi-Branch Routing (expr.match)
When a collection supports multiple categories or localized routes, chain branches with expr.match():
admin: {
previewUrl: expr.match()
.case(expr("slug").equals("home"), "/")
.case(expr.notEmpty("customPath"), "customPath")
.case(expr("category").equals("news"), expr.concat("/news/", "slug"))
.case(expr("category").equals("portal"), expr.concat("/country-portals/", "slug"))
.otherwise(expr.concat("/", "slug")),
}3. Declarative Access Control (collection.access)
Define declarative Row-Level Security (RLS) policies evaluated against { doc, user }:
import { defineCollection, expr } from "@dyrected/sdk";
export const Documents = defineCollection({
slug: "documents",
access: {
// Public can read published docs; admins can read everything
read: expr.access.isPublishedOrAdmin(),
// Document authors or admins can edit
update: expr.any(expr.access.isOwner("authorId"), expr.access.isAdmin()),
// Only superadmins can delete
delete: expr.access.hasRole("superadmin"),
},
fields: [ ... ],
});4. Reactive Field Transforms (admin.hooks.onChange)
Automatically transform values live in the browser as the editor types:
import { defineCollection, defineTextField, expr } from "@dyrected/sdk";
export const Posts = defineCollection({
slug: "posts",
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineTextField({
name: "slug",
label: "Slug",
admin: {
hooks: {
// Auto-slugify title if slug is empty, but preserve manual edits
onChange: expr.ifElse(
expr.empty("value"),
expr.slugify("siblingData.title"),
"value"
),
},
},
}),
],
});expr API Cheatsheet
| Helper | Description | Output |
|---|---|---|
expr('field').equals(val) | Equality check | field == "val" |
expr('field').notEquals(val) | Inequality check | field != "val" |
expr('field').in('a', 'b') | Set membership | field in ["a", "b"] |
expr('field').isTrue() | Boolean true check | field == true |
expr.notEmpty('field') | Non-empty string / non-null check | field != null && field != '' |
expr.empty('field') | Empty or null check | field == null || field == '' |
expr('price').greaterThan(10) | Numerical greater than | price > 10 |
expr('price').between(10, 50) | Numerical range | price >= 10 && price <= 50 |
expr.variant('card', 'hero') | Block variant matching | variant in ['card', 'hero'] |
expr.all(c1, c2) | Logical AND | c1 && c2 |
expr.any(c1, c2) | Logical OR | c1 || c2 |
expr.ifElse(cond, ifTrue, ifFalse) | Ternary expression | cond ? ifTrue : ifFalse |
expr.match().case().otherwise() | Multi-branch switch | c1 ? v1 : c2 ? v2 : v3 |
expr.concat('/path/', 'slug') | Safe string concatenation | '/path/' + slug |
expr.slugify('title') | JEXL slugify transform | slugify(title) |
expr.access.isOwner('author') | Document ownership match | author == user.id || author.id == user.id |
expr.access.isAdmin() | Admin role check | user.role == 'admin' || ('admin' in user.roles) |
expr.access.isPublishedOrAdmin() | Published or admin read rule | status == 'published' || user.role == 'admin' |
Under the Hood: JEXL Specification & Raw Strings
The expr expression builder produces serializable JEXL (Javascript Expression Language) strings.
Because expressions run inside a sandboxed JEXL runtime rather than eval(), they cannot execute arbitrary Node.js code or perform unauthorized network requests.
Because expressions run inside a sandboxed JEXL runtime rather than eval(), they cannot execute arbitrary Node.js code or perform unauthorized network requests.
Raw JEXL String Escape Hatch
If you have legacy schemas or generate conditions dynamically, you can pass raw JEXL strings directly:
export const Orders = defineCollection({
slug: "orders",
fields: [
defineNumberField({
name: "discount",
label: "Discount",
// Raw JEXL string equivalent
admin: {
condition: "couponCode != null && couponCode != ''",
},
}),
],
});Context Variables by Surface
Dyrected provides the following context variables based on where the expression is evaluated:
| Surface | In-Scope Variables | Common Use Case |
|---|---|---|
defaultValue | doc, docs, user, siblingData, data | Dynamic initial prefill on modal/form open |
admin.condition | data, siblingData, doc, docs, user, variant, block | Dynamic field visibility |
admin.hooks.onChange | value, siblingData, data, doc, docs, user | Real-time form and action modal reactivity |
admin.previewUrl | doc, entry, siteUrl, all root fields | Frontend preview routing |
access.* | user, doc, data, id, req | Row-Level Security permissions |
Global Built-in JEXL Functions
Dyrected includes 30+ built-in helper functions available in all JEXL evaluation contexts:
- Strings:
slugify(str),lower(str),upper(str),trim(str),capitalize(str),truncate(str, length),wordCount(str),readingTime(str),startsWith(str, prefix),endsWith(str, suffix). - Dates:
now(),today(),formatDate(date, style),addDays(date, n),diffDays(d1, d2),isPast(date),isFuture(date). - Arrays:
includes(arr, item),join(arr, delim),first(arr),last(arr),compact(arr),unique(arr),length(val). - Math & Logic:
round(num, decimals),clamp(num, min, max),default(val, fallback),coalesce(...vals),isEmpty(val),get(obj, path, fallback).
Recommended Next Steps
- Learn how field conditions isolate block editing in Block Fields.
- Explore role and ownership access control patterns in Collections Access Control.
- Wire Click-to-Edit annotations with
useDyPathin Live Preview Client-Side.