Skip to main content
timed_condition is a stateful conditional node that holds the β€œactive” signal for at least min_duration_s seconds before firing, then re-arms only after the input has been inactive for at least cooldown_s seconds. It is the generalised successor to the original presence_dwell node β€” the dwell semantics are still the default behaviour, but the input now accepts any signal shape (not just detection arrays), so the same primitive applies to non-vision workflows.
Backward compatibility. Workflows authored before the rename use the legacy presence_dwell subtype. The cloud executor and edge codegen continue to accept that string and resolve it to this same schema, so existing zone-based intrusion alerts keep working without edits. New nodes created from the palette use the canonical timed_condition subtype.

When to use this node

detection_event_gate (the existing class-change gate) emits on the first frame a class appears. That’s correct for β€œperson detected at all” alerts but wrong for β€œperson has been loitering in zone A for 10 seconds” β€” there is no time component in the event-gate state. timed_condition is the time-dimension counterpart:
NodeWhen it fires
detection_event_gate (on_enter)On the first frame a target class appears.
timed_condition (mode = "sustained")On the frame the cumulative β€œactive” duration crosses the threshold.
timed_condition (mode = "timeout")On the frame the cumulative β€œinactive” duration crosses the threshold.
timed_condition (mode = "debounce")Immediately on the first activation, then suppressed for cooldown_s.

Modes

ModeBehaviour
sustainedFires once the signal has been continuously active for β‰₯ min_duration_s. Re-arms after cooldown_s of inactive signal.
timeoutWatchdog. Fires once the signal has been continuously inactive for β‰₯ min_duration_s. Re-arms automatically on the next active frame (cooldown is unused).
debounceLeading-edge fire with suppression. Fires immediately on the first activation, then suppresses re-fires for cooldown_s regardless of signal state. min_duration_s is unused.
All three modes share the same per-node state and the same input predicate (active-when-truthy / above-threshold / non-empty), so swapping modes never requires re-wiring the upstream graph. Unknown modes are rejected loudly by the cloud executor and the edge codegen so a stale UI can’t silently run the wrong shape.

Inputs

FieldTypeDescription
signal (legacy: detections)array | boolean | numberThe signal to evaluate. Detection array + non-empty (after the optional class filter) counts as active. Boolean is active when truthy. Number is active when β‰₯ threshold. The port is named detections for back-compat with workflows authored before the rename.
modestringOne of sustained (default), timeout, or debounce. See the Modes table above.
min_duration_snumberContinuous β€œactive” seconds required before the gate fires (sustained) or continuous β€œinactive” seconds before the timeout fires (timeout). Ignored by debounce. 0 fires on first observation.
cooldown_snumberInactive seconds required after firing before the gate re-arms (sustained) or suppression window after firing (debounce). Ignored by timeout, which re-arms on the next active frame.
target_classesarrayOptional class allow-list applied when the signal is a detection array. Empty / null matches any class. Ignored for boolean and number signals.
thresholdnumberNumeric threshold used when the signal is a number. The signal is active when value β‰₯ threshold. Default 0. Ignored for boolean and array signals.

Outputs

FieldTypeDescription
condition_metbooleantrue on the frame the time threshold is first crossed; false otherwise.
dwell_msnumberHow long the signal has been continuously active when the gate fires; 0 when not firing. (Kept under the legacy dwell_ms name for back-compat with downstream wiring.)
matched_detectionsarrayDetections from the current frame that passed the target_classes filter when the signal is a detection array; empty list otherwise. Handy to forward into send_alert.metadata so an operator can see what triggered the gate.
first_seen_tsnumber | nullMonotonic seconds when the current active window started; null when the signal is currently inactive. Use it for relative β€œactive for N seconds” math inside the same edge / runner process, not for cross-process correlation.

State machines

State is keyed on node.uuid so two instances on the same workflow run independent timers β€” typical when one zone tracks people and another tracks vehicles.

Sustained

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    signal becomes active    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   IDLE (re-armed)   β”‚ ──────────────────────────▢│  ACTIVE (counting)   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β–²                                                    β”‚
              β”‚                                                    β”‚  elapsed_ms β‰₯ min_duration_s
              β”‚                                                    β–Ό
              β”‚                  cooldown_s seconds        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              └──────────────────── of inactivity ─────────│  COOLDOWN (fired)    β”‚
                                                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Timeout

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   signal goes inactive    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   IDLE (re-armed)   β”‚ ────────────────────────▢│  COUNTING (inactive) β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β–²                                                    β”‚
              β”‚                                                    β”‚  elapsed_ms β‰₯ min_duration_s
              β”‚  next active frame                                 β–Ό
              └──────────────────────────────────────────  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                            β”‚  FIRED (one-shot)    β”‚
                                                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
The next active frame closes the inactive window and re-arms the gate immediately β€” there is no cooldown. Designed for β€œalert if the signal stays absent” patterns (occupancy timeout, missing heartbeat, stale telemetry).

Debounce

   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   signal becomes active    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚   IDLE (re-armed)   β”‚ ─────────────────────────▢│  FIRED               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β–²                                                    β”‚
              β”‚                                                    β”‚  cooldown_s elapsed
              β”‚                                                    β–Ό
              β”‚                                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              └────────────────────────────────────────────│  SUPPRESSING         β”‚
                                                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Ignores min_duration_s and the signal value during the suppression window β€” re-fires only after cooldown_s has elapsed, on the next active frame.

Authoring

In the workflow editor inspector for the timed_condition node:
  • Mode β€” pick Sustained, Timeout, or Debounce. The other knobs grey out automatically when the selected mode ignores them (e.g. Min Duration is disabled in Debounce, Cooldown is disabled in Timeout).
  • Min Duration (s) β€” how long the signal must persist before the gate fires (active for Sustained, inactive for Timeout). 0 for β€œfire on first frame” semantics; 5–30 for loitering / intrusion alerts; higher for β€œleft unattended” scenarios.
  • Cooldown (s) β€” for Sustained, how long the input must stay inactive after a fire before the gate re-arms. For Debounce, the suppression window after the leading-edge fire. Tune to your alert review cadence.
  • Target Classes β€” optional class allow-list (chips), only used when the upstream signal is a detection array. Leave empty when the upstream spatial_filter is already class-scoped.

Companion nodes

  • spatial_filter β€” the canonical upstream for detection-array signals: scope the gate to a polygon zone.
  • send_alert β€” the canonical downstream: fire an alert with the polygon and elapsed duration baked into the payload.

Edge implementation

timed_condition requires an explicit send_alert node downstream to publish anything. The legacy implicit-alert path on call_model that used to inline a dwell state machine into the generated wf_*.py worker has been retired β€” alert emission now lives entirely in send_alert (see Raising alerts from detections). The dispatcher (EdgeWorkflowCompiler) routes any camera_frame workflow that contains a send_alert node through WorkflowCodeAssembler rather than the single-hop WorkerCodegen path, and the assembler emits the explicit alert calls. A timed_condition wired downstream of call_model without a trailing send_alert is reported as inert at compile time (WorkerCodegen.warnings carries the inert-timer text), so the editor can surface the dead-timer condition before the worker is deployed. The cloud workflow runner executes timed_condition via _execute_timed_condition_node in src/lib/workflow_utils.py, which is the canonical implementation of all three modes (sustained, timeout, debounce) and the source of the min_duration_s / cooldown_s defaults. State is keyed on node.uuid and lives in-process β€” restart the runtime and the timer resets to IDLE.

End-to-end alert pipeline

A common pattern is β€œpolygon β†’ timed condition β†’ email”:
  1. Edge perception workflow (camera_frame trigger): camera_frame β†’ call_model β†’ anonymize β†’ spatial_filter β†’ timed_condition. The edge worker emits one cw.publish_alert(...) per gate-fired intrusion event.
  2. Cloud reactive workflow (alert trigger): alert_trigger(twin=…, alert_type=detection) β†’ send_email. The cloud workflow runner wakes on every edge-emitted alert and sends the email β€” no per-frame cloud cost, only the dwell-gated one-shots.
The timed_condition cloud executor exists too and behaves identically, but it is intended for cloud-only graphs where detections (or boolean / number signals) are produced inside a single WorkflowUtils.execute() call (e.g. fed by a non-camera trigger). It is not designed for state to persist across separate alert-triggered cloud executions.