Signals¶
Signals are named values that flow through a validation run. They let workflow authors write assertions that reference data by name rather than hard-coded paths. A signal might be a mapped submission value like "expected floor area" that the author names at the workflow level, a validator input like "wall R value" that comes from a step's binding configuration, or a validator output like "site electricity consumption" that an EnergyPlus simulation produces.
Signals are the mechanism that connects the dots between submission metadata,
validator execution, and assertion evaluation. Without them, assertions would
need to hard-code paths into raw payloads. With them, a workflow author writes
s.site_eui_kwh_m2 < 100 and the platform resolves the value automatically.
For a concrete worked example, see Signals Tutorial Example.
Three concepts: Signals vs Validator Inputs vs Validator Outputs¶
Understanding the signal system requires distinguishing three related but different concepts. Each occupies a different namespace in CEL expressions and is managed by a different model.
Signals (the s namespace)¶
Signals are author-defined named values available to every step in a workflow. They come from two sources:
- Workflow-level signal mappings (
WorkflowSignalMapping) -- the author names a value and maps it to a path in the submission data. Resolved once before any step runs. - Promoted validator outputs (
SignalDefinition.signal_name) -- the author promotes a specific output from an earlier step into the signal namespace. The value becomes available to downstream steps.
In CEL expressions, signals are accessed as s.<name> or signal.<name>.
Validator inputs (the s namespace, step-level)¶
Validator inputs are values a step expects to receive. They are declared as
SignalDefinition rows with direction=INPUT and are resolved from the
submission data via StepSignalBinding rows or the signal's contract_key.
Input signals are injected into the s namespace alongside workflow-level
signals, but workflow-level signals take precedence if there is a name
collision.
Validator outputs (the o / output namespace)¶
Validator outputs are values a step produces during execution. They are
declared as SignalDefinition rows with direction=OUTPUT. After the
validator runs, the output payload is placed in the o / output namespace
so assertions can reference values as o.<name> or output.<name>.
Outputs can optionally be promoted to the signal namespace by setting
signal_name on the SignalDefinition. This makes the output value available
as s.<signal_name> in all downstream steps.
Summary table¶
| Concept | CEL namespace | Model | Scope |
|---|---|---|---|
| Workflow signals | s.<name> / signal.<name> |
WorkflowSignalMapping |
All steps |
| Promoted outputs | s.<signal_name> / signal.<signal_name> |
SignalDefinition (with signal_name) |
Downstream steps |
| Validator inputs | s.<contract_key> (step-level) |
SignalDefinition (direction=INPUT) |
Current step |
| Validator outputs | o.<contract_key> / output.<contract_key> |
SignalDefinition (direction=OUTPUT) |
Current step |
| Cross-step outputs | steps.<step_key>.output.<name> |
Run summary storage | Downstream steps |
| Raw payload | p.<path> / payload.<path> |
(none -- raw data) | Current step |
The CEL context structure¶
Every CEL expression evaluates against a context with four namespaces and
two aliases. The context is built by _build_cel_context() in
validibot/validations/validators/base/base.py.
context = {
"p": payload, # alias for payload
"payload": payload, # raw submission or validator output data
"s": signals_dict, # alias for signal
"signal": signals_dict, # workflow signals + promoted outputs + step inputs
"o": output_dict, # alias for output
"output": output_dict, # this step's declared output signals
"steps": steps_context, # outputs from completed upstream steps
}
p / payload -- raw submission data¶
Always present. Contains the raw submission data (for input-stage assertions)
or the validator's output envelope (for output-stage assertions). Authors
access raw fields via p.building.envelope.wall_r_value or
payload.results[0].value.
s / signal -- author-defined signals¶
Contains the merged signal namespace built from three sources (in priority order):
- Workflow-level signals from
RunContext.workflow_signals(resolved fromWorkflowSignalMappingrows) - Promoted validator outputs from
SignalDefinitionrows with non-emptysignal_name(injected by_inject_promoted_outputs()) - Step-bound input signals from
StepSignalBindingrows (resolved from submission data, only during input stage)
Workflow-level signals take precedence over step-level bindings with the same
name. Authors access signals via s.target_eui or signal.target_eui.
o / output -- validator output signals¶
For output-stage assertions, this contains the full validator output payload
(the dict produced by the validator). For input-stage assertions, declared
output signals are resolved from the payload so output.name is available
even before the validator runs (useful for cross-direction comparisons).
Authors access output values via o.site_eui_kwh_m2 or
output.site_eui_kwh_m2.
steps -- cross-step outputs¶
Contains validator outputs from completed upstream steps. Each entry is keyed
by the step's step_key (a stable slug set on WorkflowStep) and contains
an output dict with the step's extracted signal values.
Authors access cross-step values via steps.envelope_check.output.floor_area_m2.
CEL expression examples¶
# Workflow signal (mapped from submission data)
s.target_eui < 100
# Promoted output from a prior step
s.simulated_eui < s.target_eui
# This step's output
output.T_room < 300.15
# Raw payload access
p.building.envelope.wall_r_value > 10
# Cross-step output
steps.energyplus_step.output.site_eui_kwh_m2 < 100
# Null guard for optional signals
s.max_unmet_hours != null && output.unmet_hours < s.max_unmet_hours
Model: WorkflowSignalMapping¶
File: validibot/workflows/models.py
Defines a workflow-level signal -- an author's named vocabulary for a data point in the submission payload. Each row maps a signal name to a source path in the submission data. These signals are resolved once before any step runs and are available to all steps in the workflow.
Fields¶
| Field | Type | Purpose |
|---|---|---|
workflow |
FK to Workflow |
The workflow that owns this mapping. |
name |
CharField(100) |
Signal name. Must be a valid CEL identifier. Used as s.<name>. |
source_path |
CharField(500) |
Data path resolved against the submission payload. |
default_value |
JSONField (nullable) |
Fallback value when the source path resolves to nothing. |
on_missing |
CharField(10) |
Behavior when resolution fails: "error" (default) or "null". |
data_type |
CharField(20) |
Expected type hint: number, string, boolean, or empty (infer). |
position |
PositiveIntegerField |
Display order in the signal mapping editor. |
Constraints¶
unique_signal_name_per_workflow: One signal name per workflow, enforced at the database level.
on_missing behavior¶
error(default): The validation run fails immediately with a clear error message before any step is attempted.null: The signal is injected asnull. The author must guard withs.name != nullin CEL expressions. Accessing a null signal without a guard produces a fail-fast evaluation error with guidance on how to fix it.
Example¶
A workflow that validates energy models might define:
| name | source_path | on_missing |
|---|---|---|
target_eui |
metadata.target_eui_kwh_m2 |
error |
building_type |
metadata.building_type |
null |
floor_area |
building.gross_floor_area_m2 |
error |
All three signals become available as s.target_eui, s.building_type, and
s.floor_area in every step's CEL expressions.
Model: SignalDefinition¶
File: validibot/validations/models.py
The stable data contract for a named signal at the validator or step level.
A SignalDefinition declares that a validator or workflow step expects (input)
or produces (output) a named data point with a specific type. It is the
"what" -- the contract -- not the "where" (that is the binding).
This model unifies signal metadata that was previously scattered across three
legacy storage formats (ValidatorCatalogEntry, FMU config JSON, template
config JSON) into a single relational table.
Key concepts¶
contract_key vs native_name: contract_key is the stable,
slug-safe identifier used in CEL expressions, the API, and data path bindings
(e.g., panel_area). native_name preserves the provider's original name
verbatim (e.g., an FMU's Panel.Area_m2 or an EnergyPlus template variable
#{heating_setpoint}). The contract_key is what Validibot uses; the
native_name is what the provider uses.
Ownership (XOR constraint): Each signal is owned by exactly one of:
- A Validator -- shared signal definitions that apply to every step using
that validator (library validators).
- A WorkflowStep -- per-step signal definitions for step-level FMU uploads,
template scans, or author-customized signals.
This is enforced by the ck_sigdef_one_owner database constraint.
Output promotion via signal_name: When an output-direction
SignalDefinition has a non-empty signal_name, the output value is
promoted to the s (signal) namespace in CEL expressions for all downstream
steps. This is how a validator output from one step becomes a named signal
that later steps can reference as s.<signal_name>.
Fields¶
| Field | Type | Purpose |
|---|---|---|
contract_key |
SlugField(255) |
Stable slug identifier used in CEL, API, and bindings. |
native_name |
CharField(500) |
Provider's original name, preserved verbatim. |
label |
CharField(255) |
Human-readable display label. |
description |
TextField |
Detailed description. |
direction |
CharField(10) |
INPUT or OUTPUT (from SignalDirection choices). |
data_type |
CharField(20) |
Value type: NUMBER, STRING, BOOLEAN, TIMESERIES, OBJECT. |
origin_kind |
CharField(20) |
How created: from config declaration, FMU probe, or template scan. |
source_kind |
CharField(20) |
How the value is obtained: PAYLOAD_PATH or INTERNAL (see below). |
is_path_editable |
BooleanField |
Whether the workflow author can edit the source data path in the step binding. |
validator |
FK to Validator (nullable) |
Owner for library validators. XOR with workflow_step. |
workflow_step |
FK to WorkflowStep (nullable) |
Owner for step-level signals. XOR with validator. |
order |
PositiveIntegerField |
Display ordering within the owner's signal list. |
is_hidden |
BooleanField |
Hidden from the default signals UI. |
unit |
CharField(50) |
Unit of measurement (e.g., kW, m2, degC). |
provider_binding |
JSONField |
Validator-type-specific binding properties (see below). |
metadata |
JSONField |
Arbitrary metadata for extensions and integrations. |
signal_name |
CharField(100) |
Output promotion name. When set, value is available as s.<signal_name>. |
Constraints¶
| Constraint | Fields | Purpose |
|---|---|---|
ck_sigdef_one_owner |
validator, workflow_step |
Exactly one owner (XOR). |
uq_sigdef_validator_key_dir |
validator, contract_key, direction |
Unique per validator. |
uq_sigdef_step_key_dir |
workflow_step, contract_key, direction |
Unique per step. |
provider_binding examples¶
FMU signals store causality and value reference:
EnergyPlus template signals store variable type and constraints:
Signal source kinds¶
The source_kind field declares how the signal's value is obtained. This
distinction is surfaced in the UI so workflow authors know which signals they
can configure and which are fixed by the validator.
PAYLOAD_PATH (default): The signal's value comes from a known data path
in the submission payload or metadata. The workflow author may (depending on
is_path_editable) configure the exact path via the step's signal binding.
Most FMU input signals and template signals use this mode -- the author wires
each input to the right field in their submission data.
INTERNAL: The validator has its own mechanism for extracting or computing
the value. Examples include EnergyPlus simulation metrics (extracted from the
output envelope), THERM signals (parsed from XML), and FMU output variables
(read from the FMU runtime). The source path in the step binding is typically
fixed and should not be changed by the author.
is_path_editable controls whether the source data path field in the
signal edit modal is enabled or disabled. When False, Django's
field.disabled = True provides server-side protection -- even if someone
tampers with the form HTML, Django ignores the submitted value.
| Validator | Direction | source_kind |
is_path_editable |
|---|---|---|---|
| EnergyPlus | Input | INTERNAL |
False |
| EnergyPlus | Output | INTERNAL |
False |
| THERM | Output | INTERNAL |
False |
| FMU | Input | PAYLOAD_PATH |
True |
| FMU | Output | INTERNAL |
False |
| Template | Input | PAYLOAD_PATH |
True |
| Custom | Any | PAYLOAD_PATH |
True |
Typed metadata accessors¶
SignalDefinition provides typed access to provider-specific metadata
through Pydantic accessor properties:
sig.fmu_binding--FMUProviderBinding(causality, value_reference, etc.)sig.fmu_metadata--FMUSignalMetadata(display hints)sig.template_metadata--TemplateSignalMetadata(variable type, constraints)
How the two models relate¶
WorkflowSignalMapping and SignalDefinition serve different roles in the
signal architecture, but they share the same s namespace in CEL expressions.
WorkflowSignalMapping SignalDefinition
(workflow-level) (validator/step-level)
name: "target_eui" contract_key: "site_eui_kwh_m2"
source_path: "metadata.target_eui" direction: OUTPUT
signal_name: "simulated_eui"
│ │
▼ ▼
s.target_eui s.simulated_eui
│ │
└──────────── CEL ─────────────────────┘
│
s.simulated_eui < s.target_eui
WorkflowSignalMapping creates signals by extracting values from submission data. These are resolved once before any step runs and are available everywhere.
SignalDefinition declares the inputs and outputs of individual validators
and steps. Outputs with a non-empty signal_name are "promoted" into the
signal namespace, making their values available as s.<signal_name> in
downstream steps.
Cross-table signal name uniqueness¶
Signal names must be unique within a workflow across both models. A workflow
cannot have a WorkflowSignalMapping named floor_area and a promoted
output SignalDefinition with signal_name="floor_area" in the same
workflow.
This is enforced at the application level by validate_signal_name_unique()
in validibot/validations/services/signal_resolution.py. The function
queries both tables:
- Checks
WorkflowSignalMapping.objects.filter(workflow_id=..., name=...) - Checks
SignalDefinition.objects.filter(workflow_step__workflow_id=..., signal_name=...)
Both models call this function in their clean() methods. Additionally,
validate_signal_name() in the same module checks that names are valid CEL
identifiers and not reserved words. The reserved names list includes all
CEL context keys (p, payload, s, signal, o, output, steps),
CEL built-in functions, and CEL keywords.
Signals vs custom data paths¶
Assertions in Validibot target data in one of two ways, and understanding this distinction is fundamental to how the platform works.
Declared signals (the data contract)¶
When a validator author defines signals, they are publishing a data contract: "this validator knows about these specific data points." Signals have names (slugs), types, stages (input or output), and metadata. They appear in dropdowns, support type-appropriate operators, and enable compile-time validation of CEL expressions.
This is the structured, guided path. The validator author has done the work of mapping data paths to meaningful names, and workflow authors benefit from that investment.
Examples of validators with declared signals:
- EnergyPlus declares ~36 signals (floor area, site EUI, unmet hours, etc.)
- FMU auto-discovers signals by introspecting the model's variables
- Custom validators where the author manually adds signals through the UI
Custom data paths (no contract)¶
Some validators don't declare signals. The Basic validator, JSON Schema
validator, and XML Schema validator validate structure but don't pre-declare
what specific fields exist in the data. When a workflow author uses one of
these validators and wants to write assertions, they reference data using
custom data paths -- dot-notation expressions accessed via the p
(payload) namespace, like p.building.thermostat.setpoint or
p.results[0].value.
This is the flexible, exploratory path. The workflow author navigates the data shape themselves, without the guardrails that declared signals provide.
How the two modes interact¶
The allow_custom_assertion_targets flag on Validator controls whether
workflow authors can go beyond declared signals:
| Scenario | Signals exist? | Custom paths allowed? | What the author sees |
|---|---|---|---|
| EnergyPlus | Yes (36+) | No | Signal dropdown only |
| Custom validator with signals | Yes | Configurable | Dropdown + optional free-form paths |
| Basic validator | No | Yes (always) | Free-form path entry only |
| JSON Schema / XML Schema | No | Yes | Free-form path entry only |
When both modes are available, the form shows "Target Signal or Path" and attempts to match user input against signal definitions first, falling back to treating it as a custom path.
Signal resolution: resolve_workflow_signals()¶
File: validibot/validations/services/signal_resolution.py
This is the pre-step resolution phase. Before any workflow step executes, all
WorkflowSignalMapping rows are resolved against the submission payload. The
result is stored in RunContext.workflow_signals and injected into the CEL
context as the s / signal namespace.
Resolution algorithm¶
- Query
WorkflowSignalMappingrows for the workflow, ordered byposition. - For each mapping, call
resolve_path(submission_data, mapping.source_path). - If the path resolves, store
mapping.name -> value. - If not found and
default_valueis set, use the default. - If not found and
on_missing == "null", injectNone. - If not found and
on_missing == "error", record an error. - If any errors accumulated, raise
SignalResolutionError.
Where resolution is called¶
StepOrchestrator._resolve_workflow_signals() calls
resolve_workflow_signals() before each step execution. The resolved dict is
passed via RunContext.workflow_signals to the validator, which injects it
into the CEL context.
Promoted output reconstruction: _inject_promoted_outputs()¶
File: validibot/validations/validators/base/base.py
When a SignalDefinition with direction=OUTPUT has a non-empty
signal_name, the output value from the producing step is "promoted" into
the s namespace for all downstream steps.
How it works¶
_inject_promoted_outputs()runs inside_build_cel_context()when thestepscontext is non-empty (i.e., there are completed upstream steps).- It queries
SignalDefinitionrows with non-emptysignal_nameacross all steps in the current workflow. - For each promoted signal, it looks up the producing step's
step_keyin thestepscontext. - It extracts the output value using the signal's
contract_keyfrom the step's output dict. - If found, it injects the value into
signals_dictunder the promotedsignal_name.
Why it runs on every step¶
Promoted outputs are only available after the producing step completes. Since
different steps may complete at different times (especially with async
validators), _inject_promoted_outputs() runs fresh on every step rather
than once at the start of the run.
Example¶
Given a SignalDefinition:
- contract_key = "site_eui_kwh_m2", direction = OUTPUT,
signal_name = "simulated_eui", on step with step_key = "energyplus_step"
And a run summary:
The promoted output injects signals_dict["simulated_eui"] = 75.2, making
it accessible as s.simulated_eui in downstream CEL expressions.
How signals are defined¶
Config-based definition (advanced validators)¶
Advanced validators define their signals in config.py modules co-located with
the validator code. Each config module exports a ValidatorConfig instance
containing a list of CatalogEntrySpec objects that seed SignalDefinition
rows. Each CatalogEntrySpec can declare source_kind and is_path_editable
to control how the signal's value is obtained and whether the author can
change the source path (see Signal source kinds).
Key files:
validibot/validations/validators/base/config.py--CatalogEntrySpecandValidatorConfigPydantic modelsvalidibot/validations/validators/energyplus/config.py-- EnergyPlus signal definitions (~36 entries)validibot/validations/validators/fmu/config.py-- FMU config (emptycatalog_entries; signals created dynamically via introspection)
Dynamic definition (FMU validators)¶
FMU validators don't predefine signals in config. Instead, when an FMU file is
uploaded, sync_fmu_catalog() in validibot/validations/services/fmu.py
introspects the FMU's modelDescription.xml, discovers all input/output
variables, and creates SignalDefinition rows dynamically.
Each FMU variable's causality (input, output, parameter) determines whether
it becomes an INPUT or OUTPUT signal. The contract_key is derived from the
variable name via slugify(), and the native_name preserves the original
FMU variable name.
Custom validators¶
Users can add signals to custom validators through the UI. The signal definition forms handle creation and editing.
Syncing configs to the database¶
The sync_validators management command
(validibot/validations/management/commands/sync_validators.py)
discovers all ValidatorConfig instances via discover_configs() and
upserts Validator + SignalDefinition rows.
How signals flow during execution¶
Complete lifecycle¶
1. DEFINITION config.py, FMU introspection, or UI
+- SignalDefinition Django model rows (validator or step owned)
+- WorkflowSignalMapping Django model rows (workflow-level)
2. WORKFLOW RUN STARTS StepOrchestrator.execute_workflow_steps()
+- resolve_workflow_signals() Resolve WorkflowSignalMapping -> s namespace
+- _extract_downstream_signals() Collect prior step outputs -> steps namespace
3. EACH STEP EXECUTES _build_cel_context()
+- Input assertion evaluation s + p + output namespaces populated
+- Validator execution Container or in-process
+- _inject_promoted_outputs() Promote output signal_name -> s namespace
+- Output assertion evaluation o/output namespace populated from results
4. OUTPUT STORAGE store_signals()
+- run.summary["steps"][step_key]["output"] = signals
+- Available as steps.<step_key>.output.<name> for downstream steps
Within a single step¶
-
CEL context building.
_build_cel_context()assembles the four namespaces:p/payload(raw data),s/signal(workflow signals + promoted outputs + step inputs),o/output(declared output signals), andsteps(upstream step outputs). -
Input assertion evaluation. CEL expressions reference signals via the
snamespace and raw data viap. For example,s.expected_floor_area > 0checks a mapped submission value. -
Validator execution. The validator runs (container launch, in-process check, AI call, etc.).
-
Output extraction. The validator returns extracted outputs. For advanced validators,
extract_output_signals()converts the container output envelope into a flat dict. -
Output assertion evaluation. CEL expressions reference output values via
o.site_eui_kwh_m2oroutput.site_eui_kwh_m2. -
Signal storage.
store_signals()persists the output dict torun.summary["steps"][step_key]["output"].
Across steps (cross-step communication)¶
Signals from earlier steps are available to later steps in the same run:
- Storage. When step N completes, its outputs are saved to
validation_run.summary:
{
"steps": {
"energyplus_step": {
"output": {
"site_eui_kwh_m2": 87.5,
"site_electricity_kwh": 12500
}
}
}
}
-
Collection. Before step N+1 runs,
StepOrchestrator._extract_downstream_signals()reads the summary and collects outputs from all prior steps. -
Context injection. The collected outputs are passed to the validator via
RunContext.downstream_signals, then exposed in the CEL context under thestepsnamespace:
- Promoted outputs. If step N has a
SignalDefinitionwithsignal_name="simulated_eui",_inject_promoted_outputs()places the value into thesnamespace:
This lets a downstream step write assertions that reference outputs from an
earlier step either by the full path (steps.<key>.output.<name>) or by
promoted signal name (s.<signal_name>).
CEL context building in detail¶
The _build_cel_context() method on BaseValidator
(validibot/validations/validators/base/base.py) is the heart of signal
resolution. It builds the dictionary that CEL expressions evaluate against.
Signature:
def _build_cel_context(
self,
payload: Any,
validator: Validator,
*,
stage: str = "input",
) -> dict[str, Any]
What it does:
- Builds the
s(signals) namespace from three sources: - Workflow-level signals from
RunContext.workflow_signals - Step-bound input signals resolved via
_resolve_bound_input_context()(input stage only; workflow signals take precedence over step bindings) -
Declared input signal defaults (ensures every declared input exists in the namespace, even if unresolved, to avoid undefined-variable CEL errors)
-
Builds the
o/outputnamespace. At the output stage, the full validator output payload is used. At the input stage, declared output signals are resolved from the payload. -
Builds the
stepsnamespace fromRunContext.downstream_signalsor the run summary. -
Injects promoted outputs via
_inject_promoted_outputs()into the signals namespace. -
Assembles the final context with all six keys (
p,payload,s,signal,o,output,steps). All roots are always present (even if empty) so CEL expressions can reference them without undefined-variable errors.
Output signal elevation pipeline¶
Output signals from advanced validators (FMU, EnergyPlus) go through a multi-stage pipeline before they become CEL variables.
Stage 1: Extraction -- extract_output_signals()¶
Each advanced validator class defines a extract_output_signals() classmethod
that converts the container's output envelope into a flat Python dict of
signal names to values. For FMU validators, this dict contains the final
time-step values of each output variable:
This method is called in AdvancedValidator.post_execute_validate() after
the container completes. The extracted dict is stored in
ValidationResult.signals and later persisted to run.summary by the
processor's store_signals() method.
Stage 2: Payload merging¶
Before output-stage assertions are evaluated, the submission's input data is
merged with the extracted output signals. The output dict becomes the o /
output namespace directly.
Stage 3: CEL context building¶
The merged payload flows into _build_cel_context(), which places the
output dict in the o / output namespace. The s / signal namespace
contains workflow signals, promoted outputs, and step-bound inputs.
Stage 4: CEL evaluation¶
When cel-python compiles the expression o.T_room < 300.15, it parses
the dot as member access -- the standard CEL operator for selecting a
field from a map. At evaluation time:
- CEL looks up the variable
oin the activation context - Finds the Python dict
{"T_room": 296.63, ...} - cel-python's
json_to_cel()converts the dict to a CELMapType - The
.T_roomselector retrieves the value296.63from the map - The comparison
296.63 < 300.15evaluates totrue
This is standard CEL -- no custom operators, no dialect extensions.
Signal extraction for advanced validators¶
EnergyPlus extraction¶
File: validibot/validations/validators/energyplus/validator.py
@classmethod
def extract_output_signals(cls, output_envelope: Any) -> dict[str, Any] | None:
metrics = output_envelope.outputs.metrics
if hasattr(metrics, "model_dump"):
metrics_dict = metrics.model_dump(mode="json")
return {k: v for k, v in metrics_dict.items() if v is not None}
The EnergyPlusSimulationMetrics Pydantic model (from validibot-shared)
defines all possible output fields. model_dump() converts them to a dict,
and None values are filtered out.
Not every signal will be populated for every IDF. The signal definitions declare the full set of metrics that the validator knows how to extract, but EnergyPlus only produces a value when the IDF is configured to generate it. When a signal is absent from the extracted dict, the display layer reports "Value not found".
FMU input resolution¶
File: validibot/validations/services/cloud_run/launcher.py
Before launching an FMU container, the launcher resolves input signals from
the submission payload using SignalDefinition rows with direction=INPUT.
Storage¶
Signals are not stored in a dedicated table. They live in the summary
JSONField on ValidationRun, nested under
steps.<step_key>.output. This keeps signal storage lightweight
(no extra rows per signal per run) and naturally scoped to the run lifecycle.
The store_signals() method on ValidationStepProcessor
(validibot/validations/services/step_processor/base.py) handles persistence:
def store_signals(self, signals: dict[str, Any]) -> None:
if not signals:
return
summary = self.validation_run.summary or {}
steps = summary.setdefault("steps", {})
step_key = self.step_run.workflow_step.step_key or str(self.step_run.id)
step_data = steps.setdefault(step_key, {})
step_data["output"] = signals
self.validation_run.summary = summary
self.validation_run.save(update_fields=["summary"])
The _extract_downstream_signals() method on StepOrchestrator
(validibot/validations/services/step_orchestrator.py) reads these
stored outputs back for downstream steps, structuring them as
{step_key: {"output": {...}}}.
Path resolution¶
The resolve_path() function in
validibot/validations/services/path_resolution.py handles dotted and
bracket notation for navigating nested dict/list payloads. Both
_build_cel_context() and resolve_workflow_signals() use this shared
function.
Supported syntax:
- Dotted paths:
building.envelope.wall.u_value - Bracket notation:
results[0].temp - Mixed:
building.floors[0].zones[1].sensors[2]
Key function reference¶
| Function | File | Purpose |
|---|---|---|
resolve_workflow_signals() |
services/signal_resolution.py |
Resolve WorkflowSignalMapping rows against submission data |
validate_signal_name() |
services/signal_resolution.py |
Validate signal name is a valid CEL identifier and not reserved |
validate_signal_name_unique() |
services/signal_resolution.py |
Cross-table uniqueness check (both models) |
_build_cel_context() |
validators/base/base.py |
Build the namespaced CEL context for assertion evaluation |
_inject_promoted_outputs() |
validators/base/base.py |
Promote output signal_name values into the s namespace |
_resolve_bound_input_context() |
validators/base/base.py |
Resolve step-bound input signals from submission data |
_resolve_path() |
validators/base/base.py |
Wrapper for shared path resolution |
resolve_path() |
services/path_resolution.py |
Shared dotted/bracket path resolution |
store_signals() |
services/step_processor/base.py |
Persist output signals to run summary |
_extract_downstream_signals() |
services/step_orchestrator.py |
Collect outputs from prior steps for the steps namespace |
_resolve_workflow_signals() |
services/step_orchestrator.py |
Orchestrator-level call to resolve_workflow_signals |
extract_output_signals() |
validators/energyplus/validator.py |
Extract signals from EnergyPlus output envelope |
sync_fmu_catalog() |
services/fmu.py |
Create FMU SignalDefinition rows from model introspection |
evaluate_assertions_for_stage() |
validators/base/base.py |
Evaluate assertions against signal context |
Related documentation¶
- Signals Tutorial Example -- End-to-end worked example
- Validators -- Signal definition model and seed data
- Assertions -- How signals are referenced in rules
- Step Processor -- Signal extraction and storage implementation
- Workflow Engine -- Signal flow through workflow execution
- Results -- How signal values appear in run summaries