Board Statement DSL
A Board Statement is a named boolean expression owned by one Board. The first
consumer is a Board Constraint. Its
Binding combines one or more applicable Statements through AND or OR.
Only exact TRUE passes a Constraint. An expression result of FALSE or
NULL is treated as a failed fact.
Board Statements extend the existing Moltaro statement language instead of introducing a separate general-purpose language.
They are evaluated over a bounded, typed mutation context and are not compiled into PostgreSQL functions. Moltaro reuses the shared parser, schema binder, type system, and diagnostics, then evaluates the bound plan in memory so proposed values and date/time operations have the same behavior in preview and execute.
Context roots
Section titled “Context roots”The context provides:
| Root | Meaning |
|---|---|
BoardItem | Direct process facts such as status, cycle, run, due date, timestamps, and target identity. |
BoardData | Fields of the Board-owned record that stores process-specific data for the item. |
Target | Fields and named Entity Statements of one explicitly selected target definition. |
During a move, the Statement reads the effective proposed state, including the
destination status and values supplied by the command. Event selection itself
belongs to the Binding; the first version does not expose arbitrary Old and
New object graphs inside the expression.
Expression capabilities
Section titled “Expression capabilities”| Capability | Syntax or behavior |
|---|---|
| Boolean composition | AND, OR, NOT, parentheses |
| Comparison | =, ==, !=, <>, >, >=, <, <= |
| Membership and ranges | IN, NOT IN, BETWEEN |
| Text tests | CONTAINS, STARTS WITH, ENDS WITH |
| Null and empty tests | IS NULL, IS NOT NULL, IS EMPTY, IS NOT EMPTY |
| Direct owned collections | EXISTS ... WHERE (...), COUNT(...) |
| Reusable entity facts | references to named Entity Statements on BoardData or Target |
| Direct inverse collections | EXISTS and COUNT for event constraints only |
| Date variables | @today and @now captured once per evaluation |
| Actor variable | @currentUserId for event constraints |
Operands must be type-compatible. Null is not equal to a value; use IS NULL
and IS NOT NULL. Raw SQL, arbitrary queries, C#, network calls, and deep or
recursive object traversal are not part of the language.
A scalar Reference path yields only its referenced record id. For example,
Target.Customer = 'customer-id' compares ids. Target.Customer.Name is not
supported: a Reference is not an object traversal or a relationship between
Boards.
Operators by value family
Section titled “Operators by value family”| Value family | Tests |
|---|---|
| Text | truthiness, equality, IN/NOT IN, text tests, null, empty |
| Boolean | truthiness, equality, IN/NOT IN, null |
| Integer and decimal | truthiness, equality, ordering, IN/NOT IN, BETWEEN, null |
| Date, time, and date-time | truthiness, equality, ordering, IN/NOT IN, BETWEEN, null |
| User, Role, File, and scalar Reference stable ids; single Select option key | truthiness, equality, IN/NOT IN, null |
| Multi-Select | truthiness, CONTAINS, null, empty |
| Complex Address value | presence and null only |
| Direct owned Table | EXISTS ... WHERE (...) and COUNT(...) comparisons |
An Address whose text components are null, empty, or whitespace and whose
coordinates are absent is canonicalized to null. An Address object is present
when at least one text component or coordinate has a meaningful value. This
rule is identical for API mutations, generated Package/Net Operation Project
contexts, persisted snapshots, and proposed-state evaluation.
Money exposes its decimal Amount and normalized uppercase CurrencyCode
components. A missing Amount makes the whole Money value canonical null, even
when a currency value is stored. When Amount is present and CurrencyCode is
omitted, null, empty, or whitespace, the field’s BaseCurrencyCode is used.
Persisted and proposed values follow the same rule. Field-to-field comparison
requires compatible types. Classifier fields and CATEGORY(...) literals are
not exposed by the Board Statement V1 profile.
Date and time values
Section titled “Date and time values”- Typed date/time values are written as quoted string literals.
DateOnlyuses ISOYYYY-MM-DD, for example'2026-08-02'.TimeOnlyusesHH:mm, optionally followed by seconds and fractional seconds, for example'14:30:00'.DateTimeOffsetincludesZor an explicit numeric offset and is normalized to UTC, for example'2026-08-02T14:30:00+02:00'.- Compatible date and time values support equality, ordering,
IN, andBETWEEN. @todayis aDateOnlyin the configured workspace time zone.@nowis one capturedDateTimeOffsetinstant.
Duration literals are 7d, 12h, 30min, and 15s. Required V1 arithmetic
and extraction are:
| Expression | Meaning |
|---|---|
date + 7d, date - 7d | Add or subtract whole calendar days. |
instant + 12h, instant - 30min | Add or subtract fixed elapsed time. |
date1 - date2 | Signed integer calendar-day difference. |
instant1 - instant2 | Signed duration, comparable with a duration literal. |
YEAR, QUARTER, MONTH, ISO_WEEK, DAY, DAY_OF_WEEK | Extract calendar components; ISO weekdays are Monday 1 through Sunday 7. |
HOUR, MINUTE, SECOND | Extract time components. |
DATE, TIME | Extract workspace-local date or time from an instant. |
START_OF_MONTH, END_OF_MONTH | Return the calendar boundary. |
ADD_DAYS, ADD_MONTHS, ADD_YEARS | Add calendar units to a date. |
ADD_HOURS, ADD_MINUTES, ADD_SECONDS | Add fixed elapsed units to an instant. |
Duration literals are signed whole numbers. Decimal durations such as 1.5h
and calendar durations such as 1month or 1year are invalid. Use
ADD_MONTHS or ADD_YEARS when the operation is calendar-based.
The complete function contract is:
| Function | Accepted argument families | Result |
|---|---|---|
YEAR, QUARTER, MONTH, ISO_WEEK, DAY, DAY_OF_WEEK | (Date) or (DateTimeOffset) | Integer |
HOUR, MINUTE, SECOND | (Time) or (DateTimeOffset) | Integer |
DATE | (DateTimeOffset) | workspace-local Date |
TIME | (DateTimeOffset) | workspace-local Time |
START_OF_MONTH, END_OF_MONTH | (Date) | Date |
ADD_DAYS, ADD_MONTHS, ADD_YEARS | (Date, Integer) | Date |
ADD_HOURS, ADD_MINUTES, ADD_SECONDS | (DateTimeOffset, Integer) | DateTimeOffset |
DateTimeOffset component extraction first converts the instant to the
workspace time zone. TimeOnly supports comparison and extraction but not
arithmetic in V1, avoiding implicit day wrapping. Calendar month/year changes
use explicit functions because those units do not have fixed duration.
ADD_MONTHS and ADD_YEARS clamp to the last valid day of the resulting month:
adding one month to January 31 produces the last day of February, and adding
one year to February 29 in a non-leap year produces February 28.
Calendar and elapsed-time operations are intentionally different around a
daylight-saving time change. ADD_DAYS(date, 1) means the next workspace-local
calendar date. instant + 1d means exactly 24 elapsed hours. DATE, TIME,
and all component functions convert an instant through the configured workspace
time zone before extracting a value. The configured workspace time zone is
therefore part of the expression’s meaning; the authoring endpoint describes
the available temporal functions and literal families, not a replacement
client-side calendar policy.
YEAR(BoardData.RequiredAt) = YEAR(@today)DAY_OF_WEEK(@today) IN (6, 7)Target.DueDate + 7d <= @todayTarget.DueAt - @now <= 48hADD_MONTHS(Target.ContractStart, 1) <= @today@now and @today are captured from the same clock instant once for the
entire evaluation. A long expression cannot observe two different values of
“now”. Null date/time operands propagate NULL; only exact TRUE passes a
Constraint. Invalid literal families, arithmetic overflow, and unavailable
time-zone data fail closed.
Entity Statement references
Section titled “Entity Statement references”A Board Statement can reuse a named
Entity Statement from BoardData or its selected
Target with BoardData.Statements.<Name> or
Target.Statements.<Name>. Moltaro resolves the stable Statement name when
validating the Board Statement and tracks the dependency. Renaming or deleting
the referenced Statement is blocked while the Board Statement still depends on
it. Updating the referenced Statement expression is an intentional semantic
change and invalidates the definition cache so the Board plan is rebound.
The Entity Statement’s ExposeInClient setting does not control server-side
Board evaluation. Referencing it does not publish its value to a client.
Target scope
Section titled “Target scope”A Statement that does not read Target can be used for any item on the Board.
The safe first version binds a target-reading Statement to one exact Board
target definition. Provider targets without a typed expression schema can use
BoardItem and BoardData, but not Target fields.
One expression does not span several target definitions in V1. A Constraint
Binding instead combines several target-scoped Statements through AND or
OR, evaluating only the Statements applicable to the current item. The
configuration must provide coverage for every possible target; there is no
missing-member or missing-as-null fallback.
Event-safe and invariant-safe expressions
Section titled “Event-safe and invariant-safe expressions”A transition, entry, or exit constraint checks one attempted event. It can read
a supported first-level Reference or direct Inverse Reference collection
because the result is needed only at that moment. A direct inverse collection
supports EXISTS and COUNT, for example
COUNT(Target.ImplementationTasks) > 0.
EXISTS Target.ImplementationTasks WHERE (Status = 'ready')COUNT(Target.ImplementationTasks) BETWEEN 1 AND 5The collection must be the direct inverse exposed by /authoring. Paths from
an inverse row into another Reference, and recursive or transitive inverse
lookups, are rejected. Direct inverse collections are event-only and cannot be
used by a StatusInvariant Binding.
A status invariant promises more: every supported mutation while the item remains in the status must preserve the Statement. Its dependencies are therefore limited to direct Board Item facts, the Board Data aggregate, the direct Target aggregate, and Entity Statements built only from those same mutation-local facts.
The first version rejects status-invariant use of:
- fields below independently changing References;
- inverse references and deeper paths;
BoardData.DisplayNameandTarget.DisplayName;- runtime- or projection-computed system values, including
Number,ModifiedAt,ModifiedByUserId, andLastActivityDateTime; LineNumber, Classifier, and every other projection-managed field or owned collection;- every calculated root or owned-row field;
- Entity Statements whose transitive closure reaches
DisplayName, a calculated field,Number,Modified*,LineNumber, Classifier, a projection-managed aggregate, a Reference traversal, or another event-only dependency; - calendar and time-component extraction from
DateTimeOffsetvalues, because all component functions, includingYEAR,QUARTER,MONTH,ISO_WEEK,DAY,DAY_OF_WEEK,HOUR,MINUTE,SECOND,DATE, andTIME, interpret an instant in the configured workspace time zone; - clock- or current-user-dependent expressions;
- external or arbitrary query data.
For example, YEAR(Target.DueAt) = 2026 is valid for an event Binding but not
for a StatusInvariant Binding when DueAt is a DateTimeOffset. Changing the
workspace time zone can change the extracted calendar value without mutating
the governed Target.
This prevents a configuration from being presented as an invariant when the platform cannot observe every mutation that could change its result.
Configuration API authoring flow
Section titled “Configuration API authoring flow”All routes below are under
/api/workspace/admin/boards/boards/{boardId}/statements and require Board
configuration access.
| Operation | Route |
|---|---|
| List Statements | GET / |
| Read one Statement | GET /{boardStatementId} |
| Read the server-owned DSL profile | GET /authoring?boardTargetDefinitionId=... |
| Validate and bind without saving | POST /validate |
| Evaluate explicit proposed values | POST /preview |
| Create | POST / |
Update with RowVersion | PUT /{boardStatementId} |
Delete with RowVersion | DELETE /{boardStatementId} |
| Replace display order | POST /reorder |
Always obtain roots, paths, operators, variables, functions, examples, and
diagnostic codes from /authoring; do not hard-code a client-side copy. Omit
BoardTargetDefinitionId for a Board-wide Statement. Supply one exact Entity
Definition target id to expose the Target root. Unsupported provider targets
are rejected rather than exposed with an incomplete schema.
POST /validate returns source-positioned diagnostics and the normalized
expression, dependency manifest, complexity metrics, fingerprints, and event
or status-invariant eligibility. POST /preview accepts only an explicit
proposed context keyed by paths from /authoring; it does not become an
unrestricted record-read endpoint.
Moltaro stores the authored expression and bind metadata, but recompiles and rebinds against the current schema when needed. Package YAML carries the portable source and target scope, not a serialized executable plan. A missing field, renamed referenced Entity Statement, incompatible schema, or damaged profile fails closed with a stable diagnostic.
Copyable recipes
Section titled “Copyable recipes”Require both an owner and a future due date on Board Data:
BoardData.Owner IS NOT NULL AND BoardData.DueDate > @todayReuse a Target Entity Statement and add a Board-specific requirement:
Target.Statements.IsCommerciallyReady AND BoardData.ApprovalCode IS NOT EMPTYAccept a record when either a manual approval or a trusted Target fact passes:
BoardData.ManualApproval = TRUE OR Target.Statements.IsAutomaticallyApprovedRequire a contract to have started and not be older than one calendar year:
Target.ContractStart <= @todayAND ADD_YEARS(Target.ContractStart, 1) >= @todayRequire at least one directly related implementation task at an event boundary:
EXISTS Target.ImplementationTasks WHERE (Status = 'ready')Invalid expressions and diagnostics
Section titled “Invalid expressions and diagnostics”The following examples are intentionally invalid:
Target.Customer.Name = 'Acme'@today + 12hTarget.DueAt + 1monthTarget.StartTime + 30minTarget.UnknownField IS NOT NULLThey fail, in order, because Reference traversal is not supported; date arithmetic accepts whole days only; a month is not a fixed duration literal; time-only arithmetic is not supported; and the final path is absent from the selected profile. Board Statement expressions do not support inline comments, so explanatory text must stay outside the expression.
Validation diagnostics identify a stable code and the source start/length so an
editor can highlight the exact failing token. Treat /authoring and
/validate as the contract: do not infer field availability or reproduce the
type checker in a client.
When validation unexpectedly changes after a schema edit, fetch /authoring
again and validate the stored expression. A removed field, renamed Entity
Statement, changed target definition, profile-version mismatch, or incompatible
field type requires an explicit expression update. Moltaro never converts the
missing dependency to NULL and never keeps executing a stale compiled plan.