Dyrected
Model ContentContent Rules

Declarative Expressions & Helpers

Learn how to write Cloud-safe Jexl expressions for field hooks, reactive form logic, and access control rules using Dyrected's built-in helper utility suite.

When configuring field state, reactive form logic, or access control policies in Dyrected, you can write declarative string expressions.

Declarative string expressions are powered by Jexl. Because they are plain strings stored inside your schema files, they are 100% serializable. They survive npx dyrected sync:schema and run safely across both self-hosted servers and Dyrected Cloud.

By the end of this guide, you will understand when to reach for declarative expressions, what context variables are in scope, and how to use Dyrected's built-in helper utilities.

Choose the right hook shape for your goal

In Dyrected, you can write hook and policy logic in two ways:

  1. Declarative string expressions ("slugify(siblingData.title)"): Recommended for value derivations, format conversions, and access checks. They are stored directly in your schema, survive Cloud schema sync, and run anywhere.
  2. Function hooks (({ siblingData }) => siblingData.title): Best for self-hosted Node.js servers performing custom asynchronous side effects or calling external microservices. Function hooks cannot be serialized to Dyrected Cloud and are stripped during schema sync.

When a field calculation, access check, or UI condition can be expressed as a string, use the declarative string form.

Basic Expression Syntax

Declarative expressions support property lookups, ternary conditionals, mathematical operators, and built-in helper functions:

// Auto-derive a slug when empty, but preserve custom edits
"value == '' || value == null ? (siblingData.title != null ? slugify(siblingData.title) : value) : value"

// Role and document ownership check
"user.role == 'admin' || user.id == doc.authorId"

// Relative date check for scheduled content
"isPast(doc.publishAt)"

Context Variables by Surface

Dyrected provides an explicit context object to each declarative expression based on where it is executed:

SurfaceAvailable Context VariablesCommon Use Case
Form Reactive Hooksvalue, siblingData, dataadmin.hooks.onChange live form reactivity
Field & Collection Hooksreq, user, data, doc, operation, valuebeforeChange, beforeRead server-side evaluation
Access Control Policiesuser, req, doc, data, idaccess.read, access.update permissions
Admin UI Field Conditionsdata, siblingData, user, idadmin.condition dynamic field visibility

Built-in Helper Function Reference

Dyrected includes 30+ zero-dependency helper functions that are registered globally in all Jexl evaluation contexts.

String Helpers

Use string helpers to clean, reformat, or inspect text fields:

FunctionWhat it accomplishesWorking ExampleOutput
slugify(str)Transforms text into a URL-safe slugslugify("Hello World!")"hello-world"
lower(str)Converts text to lowercaselower("Dyrected")"dyrected"
upper(str)Converts text to uppercaseupper("dyrected")"DYRECTED"
trim(str)Strips leading and trailing whitespacetrim(" hello ")"hello"
capitalize(str)Capitalizes the first letter of textcapitalize("hELLO")"Hello"
truncate(str, len, ellipsis?)Truncates text with a custom suffixtruncate(doc.body, 20)"Long article body..."
readingTime(str)Calculates reading time in minutesreadingTime(doc.content)3
wordCount(str)Counts total words in a text stringwordCount(doc.content)450
replace(str, search, replace)Replaces occurrences of a patternreplace(doc.title, "Draft", "Final")"Final Title"
startsWith(str, prefix)Checks if text starts with a prefixstartsWith(doc.url, "https")true
endsWith(str, suffix)Checks if text ends with a suffixendsWith(doc.file, ".pdf")true

Date & Time Helpers

Use date helpers for scheduled publishing windows, expiration checks, and relative time calculations:

FunctionWhat it accomplishesWorking ExampleOutput
now()Returns current ISO 8601 timestampnow()"2026-07-27T00:00:00.000Z"
today()Returns current date (YYYY-MM-DD)today()"2026-07-27"
formatDate(date, style?)Formats dates ('short', 'iso', 'date', 'datetime', 'full')formatDate(doc.publishedAt, "short")"Jul 27, 2026"
addDays(date, days)Adds or subtracts days from a timestampaddDays(now(), 7)"2026-08-03T00:00:00.000Z"
diffDays(dateA, dateB)Calculates integer day difference between two datesdiffDays(now(), doc.createdAt)14
isPast(date)Returns true if timestamp is in the pastisPast(doc.publishAt)true
isFuture(date)Returns true if timestamp is in the futureisFuture(doc.expireAt)false

Array & Collection Helpers

Use array helpers to inspect tags, user roles, or multi-select field lists:

FunctionWhat it accomplishesWorking ExampleOutput
includes(arrOrStr, item)Checks array or substring membershipincludes(user.roles, "admin")true
join(arr, separator?)Joins array elements into a stringjoin(siblingData.tags, ", ")"news, tech"
first(arr)Safely returns the first array elementfirst(siblingData.images)"{ id: 1, ... }"
last(arr)Safely returns the last array elementlast(siblingData.history)"{ step: 3, ... }"
compact(arr)Removes empty, null, and undefined itemscompact(["a", null, "b"])["a", "b"]
unique(arr)Deduplicates array elementsunique(["tag1", "tag1", "tag2"])["tag1", "tag2"]
length(val)Returns array length, string length, or key countlength(siblingData.blocks)4

Math & Logical Helpers

Use math and logical helpers for numeric constraints, fallbacks, and safe nested property retrieval:

FunctionWhat it accomplishesWorking ExampleOutput
round(num, decimals?)Rounds a number to specified decimal placesround(siblingData.price * 1.15, 2)29.99
clamp(num, min, max)Constrains a number between upper and lower boundsclamp(siblingData.rating, 1, 5)5
default(val, fallback)Provides a fallback for null, undefined, or ""default(siblingData.title, "Untitled")"Untitled"
coalesce(...args)Returns the first non-empty valuecoalesce(doc.shortTitle, doc.title, "Draft")"Draft"
isEmpty(val)Returns true for empty arrays, objects, strings, or nullisEmpty(siblingData.items)true
get(obj, path, fallback?)Safely retrieves nested object propertiesget(siblingData, "author.name", "Guest")"Guest"

Practical Working Examples

1. Auto-generating a URL Slug from a Title

The following collection configuration auto-generates a clean slug when slug is empty, but preserves manual edits once the user types into the field:

import { defineCollection, defineTextField } from "@dyrected/core";

export const Pages = defineCollection({
  slug: "pages",
  fields: [
    defineTextField({ name: "title", label: "Title", required: true }),
    defineTextField({
      name: "slug",
      label: "Slug",
      required: true,
      unique: true,
      admin: {
        hooks: {
          onChange:
            "value == '' || value == null ? (siblingData.title != null ? slugify(siblingData.title) : value) : value",
        },
      },
    }),
  ],
});

2. Scheduled Publishing Access Rule

Restrict public document access so non-admin users cannot read articles until publishAt is in the past:

import { defineCollection } from "@dyrected/core";

export const Articles = defineCollection({
  slug: "articles",
  access: {
    read: "includes(user.roles, 'admin') || (doc.status == 'published' && isPast(doc.publishAt))",
  },
  fields: [
    /* ... fields */
  ],
});

3. Dynamic Field Visibility

Only display a discount percentage input when the selected customer tier is either 'vip' or 'partner':

import { defineNumberField } from "@dyrected/core";

export const discountField = defineNumberField({
  name: "discountPercentage",
  label: "Discount %",
  admin: {
    condition: "includes(['vip', 'partner'], siblingData.customerTier)",
  },
});

4. Auto-calculating Reading Time & Excerpt

Automatically compute estimated article reading time and generate a truncated SEO summary whenever the document content changes:

import { defineCollection, defineRichTextField, defineNumberField, defineTextField } from "@dyrected/core";

export const BlogPosts = defineCollection({
  slug: "blog-posts",
  fields: [
    defineRichTextField({ name: "content", label: "Article Content" }),
    defineNumberField({
      name: "readingTimeMinutes",
      label: "Reading Time (mins)",
      admin: {
        readOnly: true,
        hooks: {
          onChange: "readingTime(siblingData.content)",
        },
      },
    }),
    defineTextField({
      name: "seoExcerpt",
      label: "SEO Excerpt",
      admin: {
        hooks: {
          onChange: "isEmpty(value) ? truncate(siblingData.content, 150) : value",
        },
      },
    }),
  ],
});

5. Formatting Author Full Names & User Handles

Combine first and last name fields into a display name and auto-lowercase social handles:

import { defineCollection, defineTextField } from "@dyrected/core";

export const Authors = defineCollection({
  slug: "authors",
  fields: [
    defineTextField({ name: "firstName", label: "First Name" }),
    defineTextField({ name: "lastName", label: "Last Name" }),
    defineTextField({
      name: "displayName",
      label: "Display Name",
      admin: {
        hooks: {
          onChange: "trim(coalesce(join(compact([siblingData.firstName, siblingData.lastName]), ' '), value))",
        },
      },
    }),
    defineTextField({
      name: "twitterHandle",
      label: "Twitter Handle",
      admin: {
        hooks: {
          onChange: "lower(trim(value))",
        },
      },
    }),
  ],
});

6. Default Expiration Date Window

Set an automatic 30-day expiration window when creating new promotion codes if no custom expiration date is provided:

import { defineCollection, defineDateTimeField, defineTextField } from "@dyrected/core";

export const Promotions = defineCollection({
  slug: "promotions",
  fields: [
    defineTextField({ name: "code", label: "Promo Code", required: true }),
    defineDateTimeField({
      name: "expiresAt",
      label: "Expiration Date",
      admin: {
        hooks: {
          onChange: "isEmpty(value) ? addDays(now(), 30) : value",
        },
      },
    }),
  ],
});

7. Safe Deep Property Retrieval & Owner Access Policies

Allow users to update documents if they are either an admin or the owner specified in a deeply nested metadata field:

import { defineCollection } from "@dyrected/core";

export const Projects = defineCollection({
  slug: "projects",
  access: {
    update: "includes(user.roles, 'admin') || user.id == get(doc, 'metadata.ownerId')",
  },
  fields: [
    /* ... fields */
  ],
});

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