Skip to content

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.

The context provides:

RootMeaning
BoardItemDirect process facts such as status, cycle, run, due date, timestamps, and target identity.
BoardDataFields of the Board-owned record that stores process-specific data for the item.
TargetFields 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.

CapabilitySyntax or behavior
Boolean compositionAND, OR, NOT, parentheses
Comparison=, ==, !=, <>, >, >=, <, <=
Membership and rangesIN, NOT IN, BETWEEN
Text testsCONTAINS, STARTS WITH, ENDS WITH
Null and empty testsIS NULL, IS NOT NULL, IS EMPTY, IS NOT EMPTY
Direct owned collectionsEXISTS ... WHERE (...), COUNT(...)
Reusable entity factsreferences to named Entity Statements on BoardData or Target
Direct inverse collectionsEXISTS 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.

Value familyTests
Texttruthiness, equality, IN/NOT IN, text tests, null, empty
Booleantruthiness, equality, IN/NOT IN, null
Integer and decimaltruthiness, equality, ordering, IN/NOT IN, BETWEEN, null
Date, time, and date-timetruthiness, equality, ordering, IN/NOT IN, BETWEEN, null
User, Role, File, and scalar Reference stable ids; single Select option keytruthiness, equality, IN/NOT IN, null
Multi-Selecttruthiness, CONTAINS, null, empty
Complex Address valuepresence and null only
Direct owned TableEXISTS ... 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.

  • Typed date/time values are written as quoted string literals.
  • DateOnly uses ISO YYYY-MM-DD, for example '2026-08-02'.
  • TimeOnly uses HH:mm, optionally followed by seconds and fractional seconds, for example '14:30:00'.
  • DateTimeOffset includes Z or 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, and BETWEEN.
  • @today is a DateOnly in the configured workspace time zone.
  • @now is one captured DateTimeOffset instant.

Duration literals are 7d, 12h, 30min, and 15s. Required V1 arithmetic and extraction are:

ExpressionMeaning
date + 7d, date - 7dAdd or subtract whole calendar days.
instant + 12h, instant - 30minAdd or subtract fixed elapsed time.
date1 - date2Signed integer calendar-day difference.
instant1 - instant2Signed duration, comparable with a duration literal.
YEAR, QUARTER, MONTH, ISO_WEEK, DAY, DAY_OF_WEEKExtract calendar components; ISO weekdays are Monday 1 through Sunday 7.
HOUR, MINUTE, SECONDExtract time components.
DATE, TIMEExtract workspace-local date or time from an instant.
START_OF_MONTH, END_OF_MONTHReturn the calendar boundary.
ADD_DAYS, ADD_MONTHS, ADD_YEARSAdd calendar units to a date.
ADD_HOURS, ADD_MINUTES, ADD_SECONDSAdd 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:

FunctionAccepted argument familiesResult
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 <= @today
Target.DueAt - @now <= 48h
ADD_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.

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.

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.

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 5

The 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.DisplayName and Target.DisplayName;
  • runtime- or projection-computed system values, including Number, ModifiedAt, ModifiedByUserId, and LastActivityDateTime;
  • 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 DateTimeOffset values, because all component functions, including YEAR, QUARTER, MONTH, ISO_WEEK, DAY, DAY_OF_WEEK, HOUR, MINUTE, SECOND, DATE, and TIME, 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.

All routes below are under /api/workspace/admin/boards/boards/{boardId}/statements and require Board configuration access.

OperationRoute
List StatementsGET /
Read one StatementGET /{boardStatementId}
Read the server-owned DSL profileGET /authoring?boardTargetDefinitionId=...
Validate and bind without savingPOST /validate
Evaluate explicit proposed valuesPOST /preview
CreatePOST /
Update with RowVersionPUT /{boardStatementId}
Delete with RowVersionDELETE /{boardStatementId}
Replace display orderPOST /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.

Require both an owner and a future due date on Board Data:

BoardData.Owner IS NOT NULL AND BoardData.DueDate > @today

Reuse a Target Entity Statement and add a Board-specific requirement:

Target.Statements.IsCommerciallyReady AND BoardData.ApprovalCode IS NOT EMPTY

Accept a record when either a manual approval or a trusted Target fact passes:

BoardData.ManualApproval = TRUE OR Target.Statements.IsAutomaticallyApproved

Require a contract to have started and not be older than one calendar year:

Target.ContractStart <= @today
AND ADD_YEARS(Target.ContractStart, 1) >= @today

Require at least one directly related implementation task at an event boundary:

EXISTS Target.ImplementationTasks WHERE (Status = 'ready')

The following examples are intentionally invalid:

Target.Customer.Name = 'Acme'
@today + 12h
Target.DueAt + 1month
Target.StartTime + 30min
Target.UnknownField IS NOT NULL

They 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.