VX Script
Every dynamic tag is a short VX Script expression. Most of the time the picker writes it for you. Knowing how it reads lets you write richer bindings in the Advanced editor, in visibility rules, and in PHP.
text
Open until @post(closing_time).date_format("H:i")That's plain text ("Open until "), then a tag that reads a field, then a modifier that formats it.
Tags
A tag starts with @, names a group, and gives a path in parentheses:
text
@post(tagline)
@post(address.city)
@post(writers.title)
@site(name)The groups are @post, @author, @user, @term and @site; see Adding dynamic tags for what each reads. The path uses dots to go deeper: into a Group, into a Repeater's sub-fields, or through a relation to the linked entry's fields.
If a field key contains a dot, escape it with a backslash (@post(odd\.key)). If it contains spaces or other characters, put that part of the path in quotes (@post("opening hours".monday)).
An @ that isn't followed by a group and ( is ordinary text, so an email address in the text stays as it is.
Modifiers
Modifiers follow a tag and change its value, left to right:
text
@post(description).strip_tags().truncate(120, "…")Arguments are text in quotes, bare numbers, or other tags:
text
@post(tagline).fallback(@site(tagline))Text arguments can contain tags too: "Rated @post(rating) out of 5". See the modifiers reference for the full list.
Whitespace inside a tag and between modifiers doesn't matter, so long expressions can be spread over several lines:
text
@post(gallery)
.first()
.fallback(@post(featured_image))Lists
Some values are lists: a Gallery, a Repeater, a multi-choice field, a relation to several entries. Written on its own, a list of simple values shows its first item. A list of rows or entries shows nothing, because there's no single text to show.
To use a whole list, either loop a block over it, or collapse it with a list modifier:
text
@post(team.name).count() → 4
@post(team.name).list(", ") → Ana, Bruno, Carla, Dinis
@post(photos).first()Inside a loop, the same paths refer to the current item: loop over @post(team) and @post(team.name) is that member's name.
Conditions
Comparison modifiers turn a value into yes or no, and then() and else() choose what to show:
text
@post(rating).is_greater_than(4).then("Top rated").else("")Math
#math calculates. Put the calculation in quotes; it supports + - * /, parentheses, and functions such as sqrt, min, max, avg and round:
text
#math("@post(price) * 1.23").round(2)For readability, pass values in by name with .let() instead of placing tags inside the calculation:
text
#math("price * (1 + vat)")
.let("price", @post(price))
.let("vat", @post(vat_rate))
.round(2).let() must come before modifiers that use the result, such as .round(). A calculation with a missing value or a division by zero shows nothing rather than an error.
#query_var("name") reads a WordPress query variable, such as a value from the page address.
When something is wrong
A mistake never breaks the page. A malformed tag is shown as the text you typed; an unknown group, path or modifier shows nothing. Use the editor preview to check a new expression.
Technical details for LLMs
- Grammar (EBNF):text
template = ( text | tag | builtin )* ; tag = "@" ident "(" path? ")" modifier* ; builtin = "#" ident "(" args? ")" modifier* ; path = segment ( "." segment )* ; segment = ident_seg | string ; (* quoted segment allows any key *) ident_seg = ( letter | digit | "_" | "-" | "\." )+ ; modifier = "." ident "(" args? ")" ; args = arg ( "," arg )* ; arg = tag | builtin | string | number ; string = '"' ( text | tag )* '"' | "'" ( text | tag )* "'" ; (* escapes: \" \' \@ \\ \n \t *) number = "-"? digit+ ( "." digit+ )? ; ident = ( letter | digit | "_" )+ ; - A modifier chain continues across whitespace only when the next non-space token is
.ident(. Inside strings, a quote closes the string only when not inside a nested tag. - To string:
null→'';true→'1',false→''; a flat list → its first scalar; a list of objects →''. #mathis lazy:.let()binds variables; evaluation happens when coerced; unbound variables, syntax errors and division by zero yield''..let()is server-side only and a no-op on non-#mathvalues.- Builtins:
math,query_var. Register more withvxframework/dynamic-data/builtins. - Test:
vx-framework/evaluate-expressionrenders an expression against a real post/term/user;vx-framework/get-dynamic-data-cataloglists valid paths for an entity context. PHP:\VxFramework\Modules\Dynamic_Data\Expression::render( $expr, [ 'post_id' => … ] ).