> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyberwave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Timed Condition node

> Generic time-threshold gate. Fire when a signal stays active for at least N seconds, then re-arm after a cooldown. Works with detection arrays, booleans, and numeric thresholds.

`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.

<Note>
  **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.
</Note>

## 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:

| Node                                     | When 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

| Mode        | Behaviour                                                                                                                                                                        |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sustained` | Fires once the signal has been continuously active for ≥ `min_duration_s`. Re-arms after `cooldown_s` of inactive signal.                                                        |
| `timeout`   | Watchdog. Fires once the signal has been continuously *inactive* for ≥ `min_duration_s`. Re-arms automatically on the next active frame (cooldown is unused).                    |
| `debounce`  | Leading-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

| Field                           | Type                       | Description                                                                                                                                                                                                                                                           |
| ------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `signal` (legacy: `detections`) | array \| boolean \| number | The 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. |
| `mode`                          | string                     | One of `sustained` (default), `timeout`, or `debounce`. See the **Modes** table above.                                                                                                                                                                                |
| `min_duration_s`                | number                     | Continuous "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_s`                    | number                     | Inactive 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_classes`                | array                      | Optional class allow-list applied when the signal is a detection array. Empty / `null` matches any class. Ignored for boolean and number signals.                                                                                                                     |
| `threshold`                     | number                     | Numeric threshold used when the signal is a number. The signal is active when value ≥ threshold. Default `0`. Ignored for boolean and array signals.                                                                                                                  |

## Outputs

| Field                | Type           | Description                                                                                                                                                                                                                       |
| -------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `condition_met`      | boolean        | `true` on the frame the time threshold is first crossed; `false` otherwise.                                                                                                                                                       |
| `dwell_ms`           | number         | How 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_detections` | array          | Detections 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_ts`      | number \| null | Monotonic 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`](./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](/feature-reference/edge/workers/overview#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.
