Rules: domain logic
Properties
Property rules
-
The assistant MUST NOT declare a property if it is used only once.
Exception: a property may still be declared if it is added to a form.
-
Every property parameter MUST be used in its expression. Unused parameters are forbidden.
-
The assistant MUST assume standard
NULLpropagation for property expressions: if any parameter isNULL, the result isNULL.Exceptions that do NOT nullify on a single
NULLoperand: the selection operators —OVERRIDE, which returns the first non-NULLoperand, andIF ... THEN ... ELSE, whose CONDITION is theNULL-tolerant part: aNULLcondition takes theELSEbranch, while a non-NULLone returns theTHENvalue as it is, soIF TRUE THEN NULL ELSE 1isNULL—MIN/MAX, theNULL-tolerant arithmetic(+)/(-), theCONCATconcatenation (aNULLoperand is skipped together with its separator), andGROUPaggregates (GROUP SUM,GROUP MAX, etc.) — aNULLoperand or value is skipped instead of propagating.OR,NOTandXORdo not propagate either: they read a non-NULLoperand asTRUE, soNULL OR TRUEisTRUEandNOT NULLisTRUE.ANDis the one that does — it returnsTRUEonly when both operands are non-NULL, soTRUE AND NULLisNULL.GROUP LASTskips aNULLonly while it has noWHERE, where non-NULLness of the aggregated expression is what serves as the condition; given an explicitWHERE, a row satisfying it contributes its value even when that value isNULL.These exceptions still yield
NULLwhen:- every operand or aggregated value is
NULL— exceptNOT, whose whole point is to answerTRUEthere; (+)/(-)orGROUP SUMproduces0(a zero result is returned asNULL).
- every operand or aggregated value is
-
The assistant MUST NOT use
GROUPwith aBYblock (includingGROUP AGGR) inside expressions: in a type cast, in arithmetic (including(+)/(-)), as an argument of another property, or as an implementation of an abstract property via+=.Such an operator defines the parameters of its result itself, so it is allowed only as an entire property definition: the right-hand side of a definition via
=or an inline definition in square brackets; in any other position the platform raises the errorBY clause in GROUP operator cannot be used in expressions. To use the result in an expression, the assistant SHOULD first rewrite the operator withoutBY, replacing each grouping with an equality condition on an outer parameter (GROUP SUM f(x) IF g(x) = y); otherwise, apply the inline form[GROUP ... BY ...](...)to arguments or declare a separate property and refer to it.The restriction is tied specifically to the
BYblock:GROUPwithoutBYtakes its parameters from the outer context and may be used inside expressions.When reasoning about
GROUP AGGR, the assistant MUST treat it asGROUP MAXwith an additional constraint. -
The assistant SHOULD avoid unnecessary conditions when the language semantics already produce the required result.
-
The assistant MUST NOT create a property whose expression is equal to one of its parameters.
-
The assistant MUST NOT create multiple properties with identical expressions.
-
To check whether a property is
NULL, the assistant SHOULD useIF NOT property(...).To check that it is not
NULL, the assistant SHOULD useIF property(...).This applies to a property of any class, not only
BOOLEAN: a condition holds when its value is notNULL, whatever that value is —hasNotes(o) = IF notes(o) THEN TRUEfor aSTRING,isScheduled(o) = IF scheduledAt(o) THEN TRUEfor aDATETIME,hasDepartment(e) = IF department(e) THEN TRUEfor an object-valued property,NOT query() OR name(o) LIKE ('%' + query() + '%')for a search filter that passes everything while the query is empty (NOTandORdo not propagateNULL, rule 3).The assistant MUST NOT compare with the
NULLliteral:expr = NULL,expr == NULLandexpr != NULLare ordinary comparisons, and by rule 3 theNULLoperand makes the resultNULLwhateverexprholds —scheduledAt(o) != NULLis neverTRUE, so aFILTERorWHEREon it selects nothing, aSHOWIFon it always hides, anIFon it always takes theELSEbranch — and the server only reports a warning when it loads the module, the code still compiles and starts. -
The assistant SHOULD specify
CHARWIDTHin the property definition rather than in form design.For a simple property composition that only forwards another property, the assistant SHOULD NOT repeat
CHARWIDTHon the derived property unless it must differ. -
For static objects, the assistant MUST NOT use
staticCaptionorstaticNameproperties.The assistant MUST use
captionandnameinstead.This applies to writing as well:
captionandnameare simple compositions over the stored caption and name, so an assignment to them passes into the stored property. A static object's caption is assigned throughcaption; the name must not be changed — changingnameis forbidden by a system constraint.namereturns the static object's canonical name —<namespace>_<Class>.<object>, not the short identifier. When the part after the dot is needed, the assistant SHOULD usebasicNamefrom theUtilssystem module. -
The assistant SHOULD NOT specify an explicit namespace for a property unless necessary.
-
When creating a DATA property — or a simple composition over a DATA property (for example, pulling the name of a related object) — for a single object's own attribute, the assistant MUST deliberately decide whether to place it in the system
idorbasegroup viaIN.Attributes that form the object's business identity and appear in its representation SHOULD go in the
idgroup; other primary attributes go in thebasegroup (idis nested underbase).A property SHOULD NOT be placed in
idorbasewhen it is not the object's own primary attribute. -
When dividing values of integer classes, the assistant MUST cast one of the operands to
NUMERIC, not the result.The ratio of two integers is integer division, so an outer cast like
NUMERIC[16,4](a * b / c)silently drops the fractional part; the correct form isNUMERIC[16,4](a) * b / c. -
The class of an expression's result can be wider than the classes it is built from, and the assistant MUST account for that wherever a narrower class is required (a
+=implementation above all: the abstract property rules).Arithmetic widens further than it looks:
+and-— likeMIN/MAXand the selection operators — take the common ancestor, widening the whole part and the scale independently, so the result can be wider than either operand:NUMERIC[16,2] + NUMERIC[10,4]isNUMERIC[18,4];*adds both the whole parts and the scales:NUMERIC[16,2] * NUMERIC[10,4]isNUMERIC[26,6];/widens catastrophically: with the default settings its scale is always the maximumNUMERICscale (32), soNUMERIC[16,2] / NUMERIC[16,2]isNUMERIC[48,32].
A
GROUPaggregate mostly keeps the class of what it aggregates — aGROUP SUM,GROUP MAXorGROUP LASTover aNUMERIC[16,2]isNUMERIC[16,2]— but it carries outward whatever that expression already widened to.GROUP CONCATis the aggregate that widens by itself: its result is a string of unlimited length. String concatenation widens as well, summing the operands' lengths:ISTRING[200] + ISTRING[126]isISTRING[326].A narrower class is obtained only by an explicit cast of the whole expression. With operands of integer classes the operand cast of rule 13 does not bound the result — the division still widens to scale
32— so both casts are needed:NUMERIC[16,2](NUMERIC[16,2](a(x)) / b(x)). -
A parameter's class annotation (
prop(SubClass x)) is a signature, not a runtime filter: it resolves same-named properties and sets the signature, but the computed set is determined by the properties used in the expression. Reading a parent-class property with a subclass-annotated parameter still ranges over ALL objects of the parent class (e.g. in aGROUP SUM— silently wrong totals).To restrict the set to a class, the assistant MUST add an explicit
x IS SubClasscondition (or use a property declared on that subclass). -
In the
GROUP ... BYoperator the assistant MUST NOT list in theBYblock the upper parameters used in the operator's expressions: each such parameter is already implicitly a group — a parameter of the created property — and keeps its place in the signature.With an explicit parameter list on the left, the
BYexpressions are mapped in order only to the parameters not used in the expressions; a mismatch in count or classes is an error.In the inline form
[GROUP ... BY ...](...)such parameters are passed automatically: the arguments correspond in order only to theBYexpressions ([GROUP SUM f(x) IF g(x) = s BY h(x)](y)), and listingsamong them is a parameter-count error. The assistant MUST make sure that a name used inside the brackets without a class is already declared outside, earlier in the text: otherwise it silently becomes a parameter of theGROUPitself, and the aggregate runs over all its values. -
MAXandMINare prefix operators over a comma-separated operand list (MAX a, b), not infix ones:a MAX bdoes not parse — the platform reportsno viable alternative at input 'MAX'.The operand list extends as far as the expression allows, so everything after the comma belongs to the operator:
MAX a, b * cisMAX(a, b * c), whilex * MAX a, bis fine as it stands. Where a following operator must apply to the maximum itself, the operator MUST be parenthesized:(MAX a, b) * c.These operators compare the operands of a single row; a maximum across rows is
GROUP MAX. -
AND,OR,XORandNOTalways yieldBOOLEAN(TRUEorNULL), never the value of an operand:name(o) AND active(o)isTRUE, not the name, anda OR bisTRUE, not the first non-NULLvalue. The assistant MUST NOT use them to select or pass a value through; for that useexpr IF condandOVERRIDE a, b.
Abstract property rules (+=)
-
The value class of a
+=implementation MUST fit within the value class declared on the abstract property; there is no implicit cast — an implementation with a wider class is rejected at server startup with a "wrong value class of implementation" error, whosespecifiedandexpectedlines name the implementation's class and the declared one.An expression that widens the value class — arithmetic above all, and division most of all (rule 14 of the property rules) — the assistant MUST wrap in an explicit cast to the declared class:
f(X x) += NUMERIC[16,2](a(x) / b(x));f(X x) += ISTRING[250](a(x) + b(x));
Ordering rules (ORDER)
-
Where two rows can share an order key and the answer depends on which of them wins — which of two same-date rows is the
GROUP LAST, which of two equal-priority rows aTOP 1takes, where aPARTITION PREVsteps back to — the assistant MUST spell the tiebreak out, usually as the object itself:ORDER date(d), d.The platform does fill an incomplete order in on its own for several of these, so the symptom is not randomness between runs; it is that the row chosen is whichever one a service order over the interfaces selects, which is not what the domain asked for. Writing the tiebreak is how the choice becomes the intended one.
-
A cumulative
PARTITION SUM ... ORDERwith noTOPorOFFSETis the case where a tiebreak MUST NOT be added by reflex. Its default frame gives every row sharing an order key the same cumulative value. Adding a tiebreak changes the result — from a total per group of equal keys to a total per row — which is a decision about the domain, not a safety measure. UnderTOPorOFFSETrule 1 applies as usual: those pick rows, and which rows they pick is worth saying. -
PARTITION LASTdoes not read the order to compute its value: it is the value of the current row.GROUP LASTis the one that picks by order. -
A
PARTITIONdoes not split its window by the parameters of the property: to number rows separately for eachlocinidx(loc, x) <- PARTITION SUM 1 IF cond(loc, x) ORDER x, the assistant MUST addBY loc; without it the numbering runs across all values ofloc. Rows whose summed expression isNULLare not in the window, soSUM 1 IF condby itself numbers, under a unique order, the rows wherecondholds from 1.
Actions and assignment
Action rules
-
The assistant MUST avoid
FORwhen the same result can be expressed with a set-based construct.FORiterates row by row and SHOULD be the last resort when no declarative alternative exists. The one measured exception runs the other way: when the assigned value is aGROUPaggregate whose bounds correlate with the row being updated, and both sets are large, the set-based form can compile to a query that materializes the whole correlation, and a row-by-rowFOR ... NOINLINEcan be the faster one — where an index on the aggregated class lets each row's aggregate be answered by an index lookup.Prefer set-based alternatives, for example:
- aggregation or set materialization
->
GROUP SUM,GROUP CONCAT,GROUP MAX,GROUP LAST,GROUP AGGR - assigning a property over a set
-> direct property assignment with parameters
instead of a
FOR ... DOloop - exporting tabular or hierarchical data
->
EXPORT FROM,EXPORT JSON FROM,EXPORT XML FROM,EXPORT CSV FROM - building structured payloads
->
JSON FROM,XML FROM - bulk integration writes
->
NEW,DELETE, or set-based property change instead of a per-rowFOR
FORis acceptable when the body has genuine per-row control flow such as conditionalAPPLY,MESSAGE,throwException, or external calls that cannot be expressed as a set operation. - aggregation or set materialization
->
-
Parameters introduced in
NEW alias = ClassandFOR expr(p) [NEW alias = Class] DO { ... }do NOT follow the usual lexical scoping rules of mainstream programming languages.Such parameters are visible ONLY inside the body of the
NEWblock or theFORloop that introduces them.The assistant MUST NOT reference these parameters outside their introducing block.
When dependent computation must reuse these parameters, the assistant SHOULD nest further
NEWorFORblocks inside the introducing block, where the parameters are still in scope, rather than lifting values out into auxiliary storage.Conversely, a parameter declared inside a
GROUPaggregate belongs to that aggregate and is NOT visible outside of it; in particular it cannot serve as the loop variable of the enclosingFOR. Declare the variable as theFOR's own parameter and use the aggregate only as a boolean condition over it. To iterate over the groups of an aggregate together with its value, apply the inline form to new typed parameters:FOR NUMERIC[16,2] q = [GROUP SUM f(x) BY h(x)](Class y) DO .... -
The assistant SHOULD avoid introducing
LOCALproperties without a concrete need.A
LOCALmaterializes a temporary table in PostgreSQL only once it holds more than one row, so the runtime cost well above a stack variable in a conventional language applies toLOCALs with parameters (buffers keyed by row number, per-object values). A parameterlessLOCALholds at most one row and always stays in memory, so parameterless flags and single values are cheap; avoid them to keep the number of entities down, not because of cost. -
A
LOCALis normally justified when BOTH conditions hold:- its value is non-trivial to compute (aggregation, joins, multi-step logic, external calls, or other work worth materializing), AND
- the same value is consumed more than once, so materializing it avoids recomputation.
-
When possible, the assistant SHOULD prefer alternatives to a fresh
LOCAL:- inline the expression at each use site if it is cheap
- nest
NEW/FORblocks so intermediate values stay in parameter scope - use a regular (non-
LOCAL) calculated property when the value is reusable across actions
-
These are recommendations, not hard prohibitions. If the assistant cannot find a working syntax for a
LOCAL-free construction, or some other approach keeps failing and a clean action cannot be built, falling back to aLOCALis acceptable as a last resort.Established
LOCALpatterns mandated by other rules (e.g. import staging, nested-session carry-over) remain valid; the assistant SHOULD still keep suchLOCALs minimal in count and scope. -
A parameter introduced locally by a top-level statement of an action body (the implicit loop of an assignment,
FOR,NEW) is visible only inside that statement: the same name in the next statement is a new parameter with its own class.In generated scripts (
eval, data seeding) the assistant SHOULD still give such parameters unique names, so that the class of each one is evident at the place of use. -
Many system utility actions return their result through a same-named parameterless
LOCALproperty (for example, inUtils: the actionfileExists[ISTRING[500]]writes into the propertyfileExists[]). Such an element is an ACTION, not a boolean property: the assistant MUST call the action first and then read the parameterless property (fileExists(path); IF fileExists() THEN ...), and MUST NOT use the parameterized form inside an expression (IF fileExists(path)is wrong).
Assignment rules (<-)
-
The arguments of the changed property on the left side of
<-may be expressions over the statement's parameters (sentFolder(account(f)) <- f), but new local parameters can be introduced only as typed parameters, not inside expressions. Writing "into a computed key" by analogy with imperativemap[key] = valueeasily breaks this.A key that is an expression over a new parameter is written from the loop that introduces that parameter:
FOR cond(Cls x) DO out(rowNum(x)) <- value(x);(rowNum[Cls]computed beforehand, e.g. byPARTITION SUM 1).So when remapping self-referential links while deep-copying an object graph, the assistant SHOULD keep an inverse map and iterate with the TARGET object as the parameter —
link(Copy n) <- newOf(link(srcOf(n))) WHERE spec(n);— rather than writelink(newOf(x)) <- newOf(link(x)); -
<- expr IF condassigns the whole expression to ALL objects: wherecondfails, the property is overwritten withNULL. It is effectively reset-plus-set.When ADDING an assignment to a property already populated earlier in the same action, the assistant MUST use the
WHEREform (prop(x) <- TRUE WHERE cond(x)), which changes only the rows matching the condition. A second IF-form assignment to the same property MUST be treated as a review red flag. -
Inline in an action or event body,
PREV(<expr>)takes the WHOLE wrapped expression to the session-start state, including its argument sub-expressions: an argument computed in the current session (aLOCAL, a property of an object created in the session) reads asNULLinsidePREV, silently nulling the result.To read previous data with current arguments, the assistant MUST wrap
PREVin a separate property —prevF(x) = PREV(f(x));— and call it instead of writingPREV(f(<session-computed arg>))inline. -
A parameter through which a property whose name is declared on several classes is read or changed MUST be annotated with its class at first use (
date(Interaction i) <- ...): an overloaded name is resolved by the parameter classes, and an untyped parameter yields an "ambiguous name" error. This especially concerns events: their statement is a separate parameter context in which the class is not inferred from anywhere else, and ani IS Interactioncondition does not set the parameter's class.
Loop rules (FOR, WHILE)
-
FORfixes its set before the first iteration: the condition is evaluated once, the matching rows are read, and the body then runs once per row of that set. What the body changes — the data under the condition included — does not add or remove iterations.WHILEis the operator that re-reads, but it does so per STEP, not per row: one step re-evaluates the condition, reads the whole matching set and runs the body for every row of it, and only then is the set read again; iteration stops when it comes back empty. So a row already in the current step still gets its turn even if an earlier row of that same step has made the condition false for it. -
Without
ORDERaFORwalks its set in arbitrary order. The assistant MUST give an explicitORDERwhenever the result depends on the sequence — numbering, running totals, anything reading what an earlier iteration wrote — or wheneverTOPlimits how many rows are taken, and MUST end thatORDERwith a key that separates any two rows.
Thread rules (NEWTHREAD, NEWEXECUTOR)
-
A server-side thread action shares the change session of the calling code, and change sessions are not thread-safe.
The assistant SHOULD therefore wrap the body of a server-side
NEWTHREADinNEWSESSION, and inNEWSESSION NEWSQLwhen it needs a database transaction of its own. It is a trade: a plainNEWSESSIONno longer sees the caller's unsaved changes, so the wrapping is left out only where sharing the session is deliberate AND the two are known not to run at the same time.Inside an
APPLYtransaction the wrapping creates no session (change-session rule 1), and whether the body is inside that transaction is decided at the moment the body runs, not at the moment it is scheduled. ASCHEDULE DELAYis a number of milliseconds, not a barrier waiting for the apply, so it guarantees nothing either. The assistant MUST NOT count on a thread started from a global handler being isolated.A client executor is the opposite case: the action is delivered to the user's connection and runs there in its own fresh session, so wrapping it adds nothing.
Events (WHEN)
Event rules (WHEN)
-
A
WHENevent fires whenever its condition becomes true during a session and writes its target property unconditionally. If the same target property is also changed explicitly elsewhere in the session (user input, action assignment, import), the event overwrites that explicit change. -
When the event's purpose is only to derive or default a value from other inputs, the assistant SHOULD guard the condition with
AND NOT CHANGED(<target>)for each target property the event writes.This prevents the event from clobbering an explicit change to the target made elsewhere in the same session.
-
The guard SHOULD be omitted only when the event must forcibly override any explicit change — for example, maintained totals, audit stamps, or invariants the user is not allowed to bypass.
-
Rules 1-3 describe the event-action form
WHEN <condition> DO <target> <- <expr>. The calculated event form<target> <- <expr> WHEN <condition>behaves differently: its change is calculated when the target property is accessed, and an explicit change of that property in the session takes priority over the event's change.So to default a value while yielding to an explicit change, the calculated event form alone is enough — no guard is needed. Testing
CHANGED(<target>)in its condition is not possible in any case: the target would then depend on its own change, forming a cycle<target>->CHANGED(<target>)-><target>.In the absence of an explicit change the event writes the value of the expression even when it is
NULL. -
A
WHENcondition is checked on deleted objects too. Deleting an object resets its data properties toNULL, so a condition that reacts to a value becomingNULLis satisfied for every deleted object whose value had been non-NULL, and the handler runs on the object that is already gone.Which change operators those are is decided by the transition each of them covers:
DROPPED,CHANGED,DROPCHANGEDandSETDROPPEDinclude non-NULLtoNULLand therefore fire on deletion;SETandSETCHANGEDrequire the new value to be non-NULLand do not.Where the condition can fire on the way to
NULL, and the handler must not act on a deletion or on an object leaving the class, it MUST be narrowed with<object> IS <Class>.
Where local events actually run
-
A local event handler does not run at the moment the data changes. It runs at a point in the session's life: a form synchronising, a form opening, an
APPLYstarting, a nested session being created, or an explicitSystem.executeLocalEvents[].Outside an interactive form — an action called from an external system, a scheduler task — the only one of those that normally happens is the apply. So reading a property right after changing the data it depends on returns the value WITHOUT the local handlers applied, unlike a calculated property, which is always current.
The assistant MUST NOT rely on a local handler having run in such a place: either let
APPLYdo it, or callSystem.executeLocalEvents[]before the read.
Constraints
-
When a value choice in one property must be restricted based on values of other properties — sibling fields on the same form, current context, related objects — the assistant SHOULD first consider
CONSTRAINT ... CHECKED BY <property>.CHECKED BYmakes the change dialog for the listed property automatically filter out values that would violate the constraint, so the restriction is enforced declaratively at the point of selection, not after the fact.The filter reaches those change dialogs only. An input mechanism that offers values in some other way does not use it, and there a violating value is rejected only when the constraint itself is checked.
-
Manual form filters or hand-rolled validation actions SHOULD be the fallback when
CHECKED BYcannot express the restriction (e.g. the filter depends on transient UI state not modeled as a property, or the rule is advisory rather than enforced) — and also when the restriction IS expressible that way but the value is picked through some other mechanism, which theCHECKED BYfilter does not reach. -
The assistant SHOULD NOT put heavy aggregates over large tables (especially nested non-materialized ones) into a
CONSTRAINTcondition: the incremental check at apply can expand into an impractically large query, even with computation hints on the properties.For such expensive checks, use a
WHENevent instead: a cheap change-detector condition, reads of the heavy values intoLOCALs in the handler, thenMESSAGE+CANCELon violation.
Change sessions (NEWSESSION, APPLY)
-
Before introducing
NEWSESSION, the assistant MUST decide which session behavior is required. None of the choices below —NEWSQLincluded — applies during anAPPLYtransaction: inside a global event handler or an applied action no session is created at all, the inner action is deferred and runs in the current session, inside the same transaction. The assistant MUST NOT expect an independent commit there.- isolated independent unit ->
NEWSESSION - isolated unit that must also see selected local properties
from the upper session ->
NEWSESSION NESTED (...) - isolated unit that must see all local properties
from the upper session ->
NEWSESSION NESTED LOCAL - child dialog or editor that must work with unsaved upper-session
objects and return its changes to that upper session
->
NESTEDSESSION; the assistant MUST NOT replace it with plainNEWSESSIONwhile the parent object may still be unsaved in the form session
- isolated independent unit ->
-
Plain
NEWSESSIONis the default for isolated work that must not accidentally apply the caller's pending form changes:- readonly list forms with
PROPERTIES(...) NEWSESSION NEW, EDIT, DELETE - external or integration actions that isolate HTTP calls and persist their own results
- small immediate UI updates with
NEWSESSION { APPLY { ... } }
An action started on a form with editable properties MUST either be fully independent of that form's unsaved changes or save them first:
APPLY;IF canceled() THEN RETURN;NEWSESSION { ... }This is the pattern before status changes, dependent document creation and other isolated follow-up actions. - readonly list forms with
-
If inner logic depends on upper-session local state such as selections, marks, or import buffers, the assistant MUST carry that state explicitly through
NESTED (...)orNESTED LOCALon the operator, or declare the property itselfDATA LOCAL NESTED, which carries it over without being listed on the operator. Neither route works underNEWSQL: on a connection of its own it migrates nothing, so the assistant MUST NOT combineNEWSQLwith a dependency on upper-session local state. -
A successful
APPLYclears the session, and with it every plainLOCALproperty in it: after such anAPPLYreturns, theLOCALis empty again. ALOCALsurvives a successfulAPPLYonly when it is declaredNESTED(LOCAL NESTED name = Type ();orname = DATA LOCAL NESTED Type (...);) or when theAPPLYpreserves it explicitly —APPLY NESTED (name1, ..., nameN)orAPPLY NESTED LOCALfor all locals. A staged value that must outliveAPPLY— for example, an import buffer read during post-apply follow-up — MUST take one of these routes; so must the locals carried in byNEWSESSION NESTED (...)orNEWSESSION NESTED LOCALwhen their result is to be copied back to the upper session, since it is the cleared values that would be copied back.An
APPLYthat fails or is cancelled leaves the session as it was, locals included — which is why the assistant MUST NOT read aLOCALafterAPPLYto tell success from failure;canceled()is what tells them apart. Inside a nested session there is no clearing at all: the changes are copied to the parent session and the nested one is left standing, locals and all. -
After
APPLY, the assistant MUST checkcanceled()only when later logic depends on whether the save succeeded — to early-return, skip a follow-up side effect, or roll back staged work.APPLYin an interactive context shows the constraint message to the user on its own. The assistant MUST NOT addIF canceled() THEN MESSAGE applyMessage()afterAPPLYin interactive actions solely to report the failure — it duplicates the message the platform already shows. Explicit surfacing viaapplyMessage()orthrowException(applyMessage())is required only for non-interactive callers (API endpoints, background integrations) where no dialog is shown.If
APPLYfails because of a constraint, the changes remain unsaved in the current session, and any followingAPPLYin the same session will also fail until the offending data is fixed or the changes are discarded (for example withCANCEL). -
The assistant SHOULD keep
NEWSESSIONblocks small and purpose-specific: isolate one unit of work, apply it if needed, and exit.The assistant MUST NOT introduce
NEWSESSIONmerely to hide session-visibility bugs. If upper-session changes must remain visible, nested session semantics are required. -
The body of
APPLYmay run more than once. The apply transaction MAY be retried automatically after an update conflict, a deadlock or a timeout — whether it is depends on the failure and on the attempt limit — and the applied action and the synchronous global handlers are inside what a retry repeats.So they MUST be safe to repeat. An irreversible external side effect — sending mail, calling an HTTP API, printing, writing a file — MUST NOT be done there: it belongs after the apply has succeeded, where
canceled()says whether it did.