ProtectedBuildThe deferral that hands this run to the durable substrate, or undefined to execute normally.
Returns undefined for every case that must stay inline: a binding that did not opt in, or a lifecycle event that participates in the save (Validate / Before*).
After* + Durable with no queue submitter (local / no-queue process — e.g. CLI
mj sync push): do not nest in the caller's EntityTransactionScope. Defer until
TransactionDepth is 0, then fire-and-forget. Dropping the work would make Durable worse
than leaving it off; nesting it is what blew up cheese (LogActivity inside Person.Save
on a shared provider).
Builds the ActionInvocationProvenance for a dispatch: which binding fired, from which
lifecycle event, against which record. Stamped onto ActionExecutionLog so a workflow failure is
diagnosable without correlating by timestamp, and used by the redaction rules — the binding's
EntityActionParam rows are what carry ValueType and the per-binding LogValue override.
OptionalentityObject: BaseEntitythe record the run operates on; omitted for a dispatch not yet resolved to one.
Case insensitive helper method to find a param by valueType
Runs the action for this invocation type.
Returns null when the action did not run at all — the binding is scoped
(ScopeEntityID/ScopeRecordID) and this record falls outside it, or a filter refused it.
That is an ordinary outcome, not a failure: there is simply no result to report. The type
says so because callers were dereferencing it — HandleEntityActions guards correctly, the
GraphQL resolver did not, and an out-of-scope binding surfaced to clients as a server error.
This method will map the Entity Action Params to the Action Params and where needed evaluate scripts to get the run-time values that need to get passed to the Action.
ProtectedResolveThe filter rows that gate this binding, in the order the binding declares.
Two things this does beyond the obvious lookup:
Disabled bindings are skipped. Filters fail closed by design, so a disabled binding
that still gated would not merely be inert — it would prevent the action, and the only
visible symptom is a trigger that silently stopped firing. Pending still gates: it is the
column default, so treating it as inert would open every gate that was never explicitly
activated.ActionFilter the engine cannot see is a misconfiguration; running unfiltered would be
the worst possible reading of it, since the whole point of the row is to narrow when this
fires. Previously the undefined entry reached the evaluator and threw there — fail-closed by
accident, with no usable reason in the log.Attempt to execute a script and wraps in try/catch to handle any errors so that no exceptions are thrown The scripts are passed an object called EntityActionContext which has a property called entityObject that is the entity object for the current Entity Action. The script can do whatever it wants to do, and passes back a value in the result property of the EntityActionContext object
the script to execute
the entity object to pass to the script
ProtectedscheduleLocal / no-queue Durable fallback (CLI mj sync push is one host): wait until the save's
transaction has settled, then run the action without DeferExecution. Errors are logged; the
originating Save already succeeded. Protected so subclasses can replace the wait/run policy.
Handles the
Validateinvocation type.Deliberately has no
InvokeActionof its own. It used to override the single-record implementation with a near-copy that had drifted into a strict subset: same parameter mapping, same filters, but missing two things the parent does.IsEntityActionInScope, so a binding narrowed to one record viaScopeEntityID/ScopeRecordIDranValidateagainst every record of the entity — the same class of bug as a workflow trigger that claims to be scoped and is not.RunActionwas called without it, so logging and redaction could not see which binding produced the run — meaning a whole-recordValidateparameter was logged raw, ignoring the binding'sLogValuerows and itsLoggingMode.Inheriting is what keeps those two facts true for
Validateforever, rather than until the next time the two copies drift. The class remains because@RegisterClassneeds a distinct type to resolve theValidatekey.