Skip to main content
Version: 2.4

Trigger Context and Change Origin

Availability

$trigger is supported by the Fluent Forms runtime and Form Designer in tSM UI 2.4 builds that include TSM-4228. Older deployments do not evaluate or suggest this variable.

Every effect run has a cause. A user may edit a control directly, another effect may write the value, or the form may apply a value during initialization, reset, or an external programmatic update.

The transient $trigger expression variable makes that cause available to when and the other expressions evaluated by the current effect run. It is runtime metadata about this execution, not the persisted trigger property that selects a widget event or an invoked effect in the schema.

Use it when an effect must distinguish direct user input from an automatic update:

{
"id": "react-only-to-manual-text-edit",
"listen": ["$value.text"],
"when": "${$trigger.origin == 'user'}",
"do": [
{
"type": "set",
"field": "lastManualText",
"value": "${$value.text}"
}
]
}

The condition passes when the user edits text through its widget. It does not pass when another effect, a default, a form reset, or host code changes the value.

Direct origin and chain initiator

$trigger keeps two related concepts separate:

PropertyMeaning
$trigger.originWho performed the immediate change that triggered this run.
$trigger.initiatorWho started the complete causal chain.

This distinction matters for chained effects:

user changes A
-> effect 1 sets B
-> effect 2 sets C
-> effect 3 reacts to C
Effect runorigininitiator
Effect 1 listening to Auseruser
Effect 2 listening to Beffectuser
Effect 3 listening to Ceffectuser

The user origin does not propagate as the immediate origin of writes made by effects. If it did, an automatically updated control would look as if the user had edited it manually.

Use origin for direct-input rules:

{
"when": "${$trigger.origin == 'user'}"
}

Use initiator only when every step in a chain started by the user should be eligible:

{
"when": "${$trigger.initiator == 'user'}"
}

Trigger context reference

The logical context has this shape:

type EffectTriggerType =
| 'valueChange'
| 'widgetEvent'
| 'invoked'
| 'initialization';

type EffectChangeOrigin = 'user' | 'effect' | 'system' | 'mixed';

interface EffectTriggerChange {
path: string;
origin: 'user' | 'effect' | 'system';
}

interface EffectTriggerContext {
type: EffectTriggerType;
origin: EffectChangeOrigin;
initiator: 'user' | 'system';
changes?: readonly EffectTriggerChange[];
chainId?: string;
parentEffectId?: string;
}

The object is an immutable snapshot of one effect run. It is not stored in the form value and must not leak into a later, unrelated interaction. It contains causes and paths, not the old or new field values; read current values from $value.

Properties

PropertyValuesMeaning
$trigger.typevalueChange, widgetEvent, invoked, initializationHow this effect execution started. It does not describe who changed a value.
$trigger.originuser, effect, system, mixedImmediate origin of the change relevant to this execution. Use this to require a direct user edit.
$trigger.initiatoruser, systemOrigin that started the relevant causal chain. An effect write can have origin: 'effect' and still have initiator: 'user'.
$trigger.changesArray of {path, origin}Concrete changed $value paths relevant to this effect's listen, with the origin of each path. It is empty for initialization, widget events, invoked effects, and value-change runs without relevant form-value paths.
$trigger.chainIdGenerated stringCorrelates effect executions in one causal chain. A later independent interaction receives a new ID.
$trigger.parentEffectIdEffect ID or absentIdentifies the effect that invoked this one or, when unambiguous, wrote the value that started this execution. Root executions have no parent.

The type marks changes, chainId, and parentEffectId optional. The current runtime creates a chainId for a root execution and passes it down the chain; it supplies changes: [] when there are no relevant changed form-value paths. Do not use chainId or parentEffectId as stable business identifiers. A missing parentEffectId can also mean that several effects wrote values in the same batch or that the writer has no ID.

What type says

typeHow it startsTypical context
valueChangeA path in listen changes after the form settles. The path may be a form value or another supported context value.origin depends on the relevant value changes; changes lists matching $value paths when available.
initializationA value-change effect with runOnInitialization: true runs when the initial form state has settled.origin: 'system', initiator: 'system', changes: [].
widgetEventA declared widget event matches the effect's trigger.origin: 'system', initiator: 'system', changes: [] in the current runtime. Use $event or $payload for event details.
invokedAnother effect calls this effect with runEffect.origin: 'effect', initiator inherited from the caller, changes: []. Passed inputs are in $input.

type: 'valueChange' does not imply that a person edited a control. A host patch, reset, or previous effect can start the same type of execution. Check origin or initiator for that distinction.

One chain, two trigger snapshots

Suppose the user edits source. The effect copy-source listens to it and sets derived; use-derived then listens to derived. Their snapshots are:

{
"type": "valueChange",
"origin": "user",
"initiator": "user",
"changes": [{"path": "$value.source", "origin": "user"}],
"chainId": "8fbd4c96-0b54-4f2c-9d4b-1e7a4a2e0fc1"
}
{
"type": "valueChange",
"origin": "effect",
"initiator": "user",
"changes": [{"path": "$value.derived", "origin": "effect"}],
"chainId": "8fbd4c96-0b54-4f2c-9d4b-1e7a4a2e0fc1",
"parentEffectId": "copy-source"
}

The ID is illustrative. Both executions belong to the same chain, but only the first one directly observes the user's edit. A separate form interaction gets a different chainId.

Origin values

Changeorigin
The user enters a value through a widget/CVAuser
A set, copy, or clear action writes a valueeffect
An effect applies clear with mode: "reset"effect
Initialization or a default writes a valuesystem
The form is reset outside an effectsystem
Host code or an API result patches the form programmaticallysystem
One run contains relevant changes with different originsmixed

system deliberately groups programmatic sources that are not effect writes. In the current runtime, a widget event also has origin: 'system' even if the user clicked the widget; the event alone does not prove that the user changed a form value. mixed applies to a value-change run with relevant paths from more than one origin. A future version may add more detailed source metadata without changing the meaning of origin.

initiator answers a different question: what started this chain of relevant changes? It remains user through later effect writes caused by a user edit. For an independent programmatic patch it is system. If one settled pass contains both user and system changes, each effect receives the initiator for the paths it listens to; an effect listening only to the system path is not classified as user-initiated.

Combining origin with business conditions

$trigger is part of the existing JEXL condition instead of a separate listenOrigin schema property. This keeps trigger filtering composable:

{
"listen": ["$value.text"],
"when": "${$trigger.origin == 'user' && $value.enabled}",
"do": [
{
"type": "set",
"field": "confirmedText",
"value": "${$value.text}"
}
]
}

Effects that do not reference $trigger keep their existing behavior.

Multiple changes in one run

A value-change effect can listen to multiple paths. One settled runtime pass may contain more than one relevant change.

When all relevant changes have the same immediate origin, $trigger.origin contains that origin. When their origins differ, it is mixed and the exact information is available in $trigger.changes:

{
"type": "valueChange",
"origin": "mixed",
"initiator": "user",
"changes": [
{"path": "$value.firstName", "origin": "user"},
{"path": "$value.displayName", "origin": "effect"}
]
}

To accept a run when at least one relevant change came directly from the user, inspect the collection:

{
"when": "${$trigger.changes.some(change => change.origin == 'user')}"
}

changes contains only paths relevant to the current effect's listen, not every changed value in the root form. Each item has two fields:

FieldMeaning
pathConcrete root-form path such as $value.customer.name or $value.orders[2].lines[1].source, with actual array indexes rather than a $row placeholder.
originImmediate origin of that individual path: user, effect, or system. An individual change is never mixed; only the aggregate $trigger.origin can be mixed.

changes is a list of causes, not a value diff: it contains no old or new values. A value-change effect that listens only to $context can run with changes: [], because there is no matching form-value path to report. For widgetEvent, invoked, and initialization, the runtime also supplies an empty array. Guard access to particular array items when it may be empty.

Parent paths and array rows

  • When an effect listens to $value.customer and only the name changes, the concrete change path is $value.customer.name.
  • A $row effect receives changes for the concrete row represented by its current $row value.
  • A scoped nested-array effect receives changes for its concrete inner row; $row is that row, and $parents identifies the enclosing rows nearest first.
  • A control that became user-dirty during an earlier interaction does not make a later programmatic sibling change a user change.
  • Inner forms and Characteristics preserve the per-change metadata when they propagate their value to the owning form.

Initialization

An effect with runOnInitialization: true receives a fresh initialization context:

{
"type": "initialization",
"origin": "system",
"initiator": "system",
"changes": []
}

Therefore an effect whose condition is only ${$trigger.origin == 'user'} does not run its actions during initialization. To explicitly allow both cases:

{
"when": "${$trigger.type == 'initialization' || $trigger.origin == 'user'}"
}

Invoked effects

An effect started through runEffect has type: "invoked" and origin: "effect". It inherits initiator from its caller:

{
"type": "invoked",
"origin": "effect",
"initiator": "user"
}

An invocation outside an existing user chain uses initiator: "system".

Widget events

A widget-event effect has type: "widgetEvent", origin: "system", initiator: "system", and changes: [] in the current runtime. This remains true when a person clicks the widget: the event identifies an action, but the runtime does not classify that event itself as a direct form-value edit.

The existing $event and $payload variables remain the source of the widget event name, pointer, row, and payload. $trigger describes causality; it does not replace them.

Availability in effect expressions

The same $trigger snapshot is available throughout one effect execution:

  • when;
  • set.value;
  • scriptData;
  • runEffect.inputs;
  • the success and error branches of the same script action.

Asynchronous continuations preserve the snapshot of their originating run. They do not read the trigger context of a newer form change.

Expression editor assistance

Every expression editor used to author an effect when condition provides the same context-aware suggestions. This includes the Form Logic Editor, the effect detail editor, and any shared JEXL editor used for the same property.

Autocomplete provides:

  • $trigger at the effect-expression root;
  • type, origin, initiator, and changes after $trigger.;
  • valueChange, widgetEvent, invoked, and initialization when comparing type;
  • user, effect, system, and mixed when comparing origin;
  • user and system when comparing initiator.

The suggestions come from one shared effect-expression catalog so the editors, schema validation, and runtime contract cannot drift independently. The when editor remains JEXL-only; $trigger does not introduce a static-value mode or another expression language.

Editor coverage verifies the root variable, member completion, and the enum values for type, origin, and initiator, then saves and reopens the condition to prove that the expression round-trips unchanged.

$trigger is not suggested in ordinary widget expressions because those expressions do not have an effect execution frame.

Diagnostics

An effect frame in the Form Runtime Debugger can expose:

type
origin
initiator
changes
chainId
parentEffectId

These values distinguish an effect rejected by when from an effect started by direct input, another effect, or a mixed batch. They also correlate multiple effect runs that belong to the same causal chain.

initiator: "user" is not proof that the current value was typed manually. Use origin for that question.

Backward compatibility

  • Existing persisted effect objects do not change.
  • Existing effects that do not reference $trigger run as before.
  • $trigger is transient expression metadata, not form data.
  • No bulk schema migration is required.
  • A schema that uses $trigger requires a runtime version that implements this contract.

Runtime requirements

The runtime implementation follows these rules:

  1. Record origin for the concrete value change; do not infer it only from the control's persistent dirty state.
  2. Mark effect action writes as effect before they trigger the next value-change pass.
  3. Mark direct CVA/widget changes as user.
  4. Mark initialization, defaults, external patches, and form resets outside an effect as system unless they carry more precise explicit metadata.
  5. Preserve initiator and chainId through a cascade, but assign a new immediate origin for every write.
  6. Build changes from the intersection of actual changed paths and the current effect's listen paths.
  7. Keep $trigger in the immutable execution frame used by diagnostics and asynchronous continuations.
  8. Keep cycle detection, maximum cascade depth, and global rate limiting unchanged.