Constraint Execution Model
FCS triggers execute constraints in a specific order with a specific scope model. Understanding this is essential for multi-step pipelines like "call API → download result → vectorize."
How constraints execute
A trigger is an FCS object with a Constraints array. Each element in the array is processed sequentially (one after another), and after each constraint runs, the FCS frame is re-evaluated so that subsequent constraints see updated values.
myTrigger := {
Constraints = [
constraintA, # runs first, frame re-evaluated after
constraintB, # runs second, sees updated state from A
constraintC, # runs third, sees updated state from A + B
],
}
Key rule: The total number of constraints in the array must not change between sequential runs. If it does, you get:
"The number of sequentially running constraints in a trigger must not change between the individual runs."
Batch (flat) vs Sequential (nested) constraints
Batch: constraints at the same nesting level
When multiple constraint objects appear inside the same array element (via sub-array flattening), they execute as a batch — all evaluated against the same frame snapshot.
# These two constraints run in the SAME batch (same frame)
trigger := {
Constraints = [
[constraintA, constraintB], # both see the original frame
],
}
Sequential: constraints at top-level array positions
When constraints occupy separate top-level positions in the Constraints array, they execute sequentially with frame re-evaluation between them.
# These run SEQUENTIALLY (frame refreshed between each)
trigger := {
Constraints = [
constraintA, # position 0: runs first
constraintB, # position 1: runs second, sees A's changes
],
}
Combined: reference expansion
When you reference another trigger's Constraints (e.g., recognitionTrigger.Constraints), the referenced array is inlined. This is how chaining works:
# Each .Constraints reference becomes one element in the outer array
combinedTrigger := {
Constraints = [
recognitionTrigger.Constraints, # element 0: all of recognition's constraints as one batch
downloadMaskTrigger.Constraints, # element 1: all of download's constraints as one batch
],
}
In CheckFixTheExpressionTrigger, the outer loop iterates constraints.Count times (the top-level array length). For each position i, EnumerateGetListValues(constraints[i]) recursively flattens any nested arrays or string references into individual constraint objects that execute within the same batch.
Source: MsContraints.cs:296-347 (sequential loop), MsContraints.cs:203-232 (recursive flattening)
VariableScope
Each constraint can specify where its Variable path is resolved relative to. This is controlled by the VariableScope attribute.
{
Variable = "myVar",
VariableScope = "global", # resolve myVar from root
Condition = False,
InitByGet = { Url = someUrl, MediaFormat = "object" },
}
Scope values
| Value | Behavior | When to use |
|---|---|---|
(default) fromconstraint |
Resolved relative to the constraint's own path in the FCS tree | Most cases — variable lives near the trigger |
"global" |
Resolved from the FCS root | Variable is at the top level of the component, trigger is nested |
"local" |
Resolved relative to the constraint path with tail appended | Variable is a child of the constraint's parent |
"parent" or "parent:N" |
Goes up N levels from the constraint path, then appends variable name | Variable is N levels up from the constraint location |
Source: MsContraints.cs:351-453
Practical example
If a trigger at path subComponent.details needs to set a variable resultData that lives at the FCS root:
{
Variable = "resultData",
VariableScope = "global",
Condition = False,
InitByGet = { Url = apiUrl, MediaFormat = "object" },
}
Without VariableScope = "global", the engine would look for subComponent.details.resultData and fail.
The "multi-step constraints" error
When you chain triggers like this:
detectBuildingsTrigger := {
Constraints = [
recognitionTrigger.Constraints, # step 1: call API
downloadMaskTrigger.Constraints, # step 2: download mask (depends on step 1)
],
}
Step 2 depends on step 1's output (the mask URL comes from the recognition response). The engine handles this correctly because:
- Step 1 executes → PVC updated with
reconResponse - Frame re-evaluated →
maskAbsoluteUrlnow resolves correctly - Step 2 executes → uses the fresh URL
However, the trigger path must still be findable after the frame update. If the trigger is defined with a path that changes between steps, you get:
"Failed to evaluate constraint in trigger path '...' for the constraint [N of M] run. Did any previous constraint run modify this array of constraints? Please consider using global trigger for these multi step constraints."
Fix: Define the combined trigger at the FCS root level (or use VariableScope = "global") so the path remains stable across frame updates.
Constraint attributes reference
Each constraint object supports these attributes:
| Attribute | Type | Purpose |
|---|---|---|
Variable |
string | Target PVC variable name |
VariableScope |
string | Variable resolution scope (see above) |
Condition |
bool | If False, the constraint fires (unconditional). If True, skipped |
Validation |
bool | If False, stops all triggers and shows explanation. Used for pre-checks |
Explanation |
string | User-visible message (shown as note or error) |
InitByGet |
object | { Url, MediaFormat, TimeoutMs, BasicAuthCredentials } — HTTP GET |
InitByPost |
object | { Url, Body, MediaFormat, TimeoutMs, BasicAuthCredentials } — HTTP POST |
Init |
expression | Direct value assignment (no REST call) |
InitReset |
bool | If True, resets the variable (removes from PVC) |
InitCopyFromPvcPath |
string | Copy PVC values from another path |
InitCopyFromPvcSliceToken |
string | Copy PVC from a remote component |
InitTransform / ResponseTransform |
callable | Transform function applied to the response |
UpdateAssumptionConditions |
bool | Re-evaluate assumption conditions after this constraint |
MediaFormat values
For GET:
| Value | Result |
|---|---|
"object" |
Parse JSON → FCS object |
"base64" |
Binary → base64 string |
"string" |
Raw text string |
For POST:
| Value | Request → Response |
|---|---|
"string2string" |
string body → string response |
"object2string" |
JSON body → string response |
"object2object" |
JSON body → JSON response |
"string2translatedString" |
string body → localized string |
"object2translatedString" |
JSON body → localized string |
"object2translatedObject" |
JSON body → localized JSON |
Source: MsContraints.cs:17-41 (attributes), ParameterValueConstraints.cs:186-211 (REST call dispatch)
Quick vs Non-Quick Trigger Execution
When a trigger fires (via a button click or the update-properties API), the system decides whether to run it inline (quick) or as a background WebJob (non-quick). This determines which container processes the constraints.
How it works
| Mode | Where constraints run | When used |
|---|---|---|
| Quick (inline) | Engine container (via webapp) | Fast triggers, user edits with value changes |
| Non-quick (WebJob) | WebJobs2 container | Heavy/long-running triggers, explicit opt-out of quick |
What controls the mode
The IsQuick() function in ComponentUpdateService.cs:40-50 evaluates these conditions — if any is true, the trigger runs as quick:
=> validationTrigger.IsQuick // 1. :quick suffix in trigger name
|| string.IsNullOrEmpty(validationTrigger.Name) // 2. no trigger name
|| validationTrigger.Name.EndsWith("__QuickTrigger") // 3. naming convention
|| (request.EditedDiff != null && request.EditedDiff.Items.Count != 0) // 4. user changed values
Additionally, isMoreThanAction (line 343) forces quick when the VM's ParamValueDto is not a ParamValueActionVM (i.e., the trigger is associated with a value change, not a button click).
FCS definition
Fcs.Parameter.ItemAction{
HumanName = "🚀 Run Pipeline",
ValidationTrigger = "myTrigger",
IsQuick = True, # True → UI appends :quick suffix
IsAuthPost = False,
CssStyleClass = "fcs-parameterItem-inMenuButton"
}
When IsQuick = True, the Aurelia frontend appends :quick to the trigger name before sending. When IsQuick = False, no suffix is appended.
The :quick suffix protocol
The UI constructs the trigger name with suffixes that the server parses:
detectBuildingsTrigger:quick → IsQuick = true
detectBuildingsTrigger:authpost → IsAuthPost = true (authenticated POST)
detectBuildingsTrigger → parsed as-is (may still be quick via other conditions)
Source: ParamValueVMToolsWeb.cs:72, ComponentUpdateService.cs:338-370
Calling the API for non-quick mode
To force the non-quick (WebJob) path via the update-properties API:
- Load properties for any variable to get the base VM
- Set
ParamValueDto = nullto avoidisMoreThanAction = true - Send the trigger name without
:quick - Ensure no edited values in the VM (same values as original PVC)
# Step 1: Load VM
$vm = (POST /load-properties {contextPath: "someVar"}).Content | ConvertFrom-Json
# Step 2: Null out ParamValueDto
$vm.ParamValueDto = $null
# Step 3: Send without :quick
POST /update-properties {vm: $vm, validationTrigger: "detectBuildingsTrigger"}
# Response: {"ResultType": "ResultJobStatus", "jobstatus": {JobId: "abc...", CommitUrl: "..."}}
The response returns a ResultJobStatus with:
JobId— poll atGET /api/spaces/{space}/jobs/{jobId}/status(returns"done"when complete)CommitUrl— POST to commit the job result to the component
# Step 4: Poll job
GET /api/spaces/{space}/jobs/{jobId}/status → "done"
# Step 5: Commit result
POST /{space}/en/Properties/CommitUpdateResult?resultKey={key}
# Body: {ComponentId, ComponentSubRevision, ComponentPrivateKey, OriginalPvcB64}
When to use non-quick
- Heavy constraints that make slow API calls (prevents UI timeout)
- Constraints that need SkiaSharp — WebJobs container has native libs (
fontconfig,libgdiplus), engine container may not - Long-running pipelines where you want background processing
Important limitation
Non-quick mode only affects constraint execution (the InitByGet/InitByPost HTTP calls). Lazy FCS expressions (like maskImageData := Fcs.Presentation.ImageData.FromBase64(...)) are always evaluated by the engine on demand, regardless of quick/non-quick. If the engine container lacks native dependencies, those expressions will still fail.
To fix engine-side SkiaSharp: Add native deps to Dockerfile-engine (see Dockerfile-jobs for reference).
Source: ComponentUpdateService.cs:40-50 (IsQuick), ComponentUpdateService.cs:152-194 (dispatch), ComponentUpdateService.cs:229-244 (commit after job)
String constraint indirection
Constraint arrays support string references — instead of embedding constraint objects directly, you reference them by name. The engine resolves strings at execution time via CascadeEvaluator (MsContraints.cs:208).
Why it matters
When you embed trigger objects directly:
# PROBLEM: maskSmoothGeom is evaluated when this object is CONSTRUCTED
# (before steps 1-2 populate maskBase64)
detectBuildingsTrigger := {
Constraints = [
recognitionTrigger.Constraints, # triggers eager evaluation of everything
vectorizeTrigger.Constraints, # Init = maskSmoothGeom evaluated NOW → {}
],
}
With string references, resolution is deferred to execution time:
# CORRECT: each string resolved only when constraint engine processes it
detectBuildingsTrigger := {
Constraints = [
"recognitionConstraint",
"downloadMaskConstraint",
"vectorizeConstraint",
],
}
Important: strings must resolve to constraint objects
The string must resolve to a constraint object (with Variable, Condition, Init, etc.), NOT a trigger wrapper ({Constraints: [...]}). If it resolves to a trigger object, CheckFixConstraint won't find the Variable attribute and silently skips it.
# WRONG: "myTrigger" resolves to {Constraints: [...]}, not a constraint object
detectBuildingsTrigger := { Constraints = ["myTrigger"] }
# CORRECT: "myConstraint" resolves to {Variable: "x", Condition: False, Init: ...}
detectBuildingsTrigger := { Constraints = ["myConstraint"] }
How it works in the engine
In EnumerateGetListValues (MsContraints.cs:203-230):
case string constraintExpression:
var (foundPath, foundConstraint) = CascadeEvaluator.TryDeleteScopeSafeCascadeRead(
frame, localPath, constraintExpression);
foreach (var subItem in EnumerateGetListValues(foundConstraint, ...))
yield return subItem;
The top-level Constraints attribute also supports string indirection (MsContraints.cs:150):
# The Constraints attribute itself can be a string
myTrigger := { Constraints = "sharedConstraintArray" }
Source: MsContraints.cs:150-151 (top-level), MsContraints.cs:208-218 (recursive)
Lazy vs Eager in constraints (:= vs =)
FCS uses := for lazy evaluation and = for eager evaluation. This distinction is critical inside constraint definitions.
The rule
Always use := (lazy) for Init in multi-constraint triggers. Eager = evaluates the expression when the constraint object is constructed, which may be before preceding constraints have populated their target variables.
# WRONG: Init evaluated when constraint object is built (before mask is downloaded)
vectorizeConstraint := {
Variable = "buildingOutlines",
Init = maskSmoothGeom, # eager: evaluated NOW, before maskBase64 exists → {}
}
# CORRECT: Init evaluated when constraint engine processes this constraint
vectorizeConstraint := {
Variable = "buildingOutlines",
Init := maskSmoothGeom, # lazy: evaluated at processing time, after mask is downloaded
}
How Init := works with CapturedLazyValueExpression
At MsContraints.cs:557:
triggerReponse.rhs = new CapturedLazyValueExpression(
() => constraint.GetAttributeValue(InitAttribute), Frame);
The lambda captures the constraint and frame. When Init is lazy (:=), GetAttributeValue triggers lazy evaluation at the moment the rhs is consumed — not at capture time. Combined with string indirection, this provides double safety for multi-step pipelines.
Source: MsContraints.cs:555-558, ParameterValueConstraints.cs:154-157
Geometry persistence in PVC
FCS geometry objects (FcsBufferedGeometry) are not PVC-serializable. Attempting to store one via Init into an ItemGallery gives:
"Can not convert 'FCS.Geometry.Buffering.FcsBufferedGeometry' to ParameterValueClass"
The solution: persistence strings
Serialize geometry to a string with ToPersistenceString() and parse back with Fcs.Geometry.Buffered.Parse():
# ── Store in PVC as string (runs in trigger / WebJobs) ──
buildingOutlines := "" # ItemString, not ItemGallery
vectorizeConstraint := {
Variable = "buildingOutlines",
Condition = False,
Init := maskSmoothGeom.ToPersistenceString(-8), # serialize to compact string
}
# ── Read from PVC in scene generation (runs in Engine, no SkiaSharp needed) ──
hasBuildingOutlines := buildingOutlines != ""
gblock {gb} gcomposite (hasBuildingOutlines
? [Fcs.Geometry.Composite(Fcs.Geometry.Buffered.Parse(buildingOutlines))]
: []) if (hasBuildingOutlines)
Precision parameter
ToPersistenceString(precision) controls output compactness:
| Precision | Meaning | Example |
|---|---|---|
16 (default) |
16 significant digits | Full precision, large strings |
-8 |
8 significant digits | Good balance for mask-derived geometry |
-12 |
12 significant digits | Used in diagnostics |
Negative values = significant digits (compact). Positive = decimal places.
The persistence string format
(x1,y1 x2,y2 ...)[v1,v2 v3,v4 ...][+0+1+2 -0+3+4][+0 +1]
↑ vertices ↑ edges (curves) ↑ loops (oriented) ↑ areas/solids
Example with 3 building polygons:
(46,-26.6 49.2,-27 47,-21.2 ...)[0,1 1,2 2,3 3,0 ...][+0+1+2+3 +4+5+6+7+8 +9+10+11+12+13+14][+0 +1 +2]
Source: FcsBufferedGeometry.cs:78-79, BufferedGeometry3D.cs:32-33, GeometryDiag.cs:82
Best practices
- Keep combined triggers at root level to avoid path resolution issues after frame updates
- Use
VariableScope = "global"when a nested trigger needs to write to a root-level variable - All target variables must be exposed as InputItems — the constraint system can only write to variables declared in
InputPageGallery_ParameterItemClasses(useVisibilityCondition = "False"to hide them) - Use sequential (separate array positions) when step N depends on step N-1's output
- Use batch (nested arrays) when constraints are independent and can share the same frame
- Don't dynamically change the constraint count — the engine checks
noConstraints != constraints.Countand throws if it changes - Use
Init :=(lazy) always — eagerInit =evaluates at object construction time, which may be before preceding constraints have run - Use string constraint references for multi-step pipelines — strings are resolved at execution time, preventing eager evaluation leakage across trigger boundaries
- Store geometry as persistence strings — use
ToPersistenceString(-8)to serialize,Fcs.Geometry.Buffered.Parse()to deserialize. UseItemString(notItemGallery) for the PVC parameter