Dyrected
Model ContentContent Rules

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

HelperDescriptionOutput
expr('field').equals(val)Equality checkfield == "val"
expr('field').notEquals(val)Inequality checkfield != "val"
expr('field').in('a', 'b')Set membershipfield in ["a", "b"]
expr('field').isTrue()Boolean true checkfield == true
expr.notEmpty('field')Non-empty string / non-null checkfield != null && field != ''
expr.empty('field')Empty or null checkfield == null || field == ''
expr('price').greaterThan(10)Numerical greater thanprice > 10
expr('price').between(10, 50)Numerical rangeprice >= 10 && price <= 50
expr.variant('card', 'hero')Block variant matchingvariant in ['card', 'hero']
expr.all(c1, c2)Logical ANDc1 && c2
expr.any(c1, c2)Logical ORc1 || c2
expr.ifElse(cond, ifTrue, ifFalse)Ternary expressioncond ? ifTrue : ifFalse
expr.match().case().otherwise()Multi-branch switchc1 ? v1 : c2 ? v2 : v3
expr.concat('/path/', 'slug')Safe string concatenation'/path/' + slug
expr.slugify('title')JEXL slugify transformslugify(title)
expr.access.isOwner('author')Document ownership matchauthor == user.id || author.id == user.id
expr.access.isAdmin()Admin role checkuser.role == 'admin' || ('admin' in user.roles)
expr.access.isPublishedOrAdmin()Published or admin read rulestatus == '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:

SurfaceIn-Scope VariablesCommon Use Case
defaultValuedoc, docs, user, siblingData, dataDynamic initial prefill on modal/form open
admin.conditiondata, siblingData, doc, docs, user, variant, blockDynamic field visibility
admin.hooks.onChangevalue, siblingData, data, doc, docs, userReal-time form and action modal reactivity
admin.previewUrldoc, entry, siteUrl, all root fieldsFrontend preview routing
access.*user, doc, data, id, reqRow-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).

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend