Signals
A signal is a CEL detection rule that Santa evaluates on the host against telemetry. A match does not block anything. Santa reports it back, and the report appears under Signals in Events. Signals need telemetry enabled on the hosts they target.
You manage signals under Rules > Signals. Each signal applies to one operating system and to the hosts in its tags.
The event Variable
A signal's expression reads the current telemetry record through event, and must return a boolean.
Guard each branch with has() for the event type it reads:
has(event.execution) && event.execution.Target.Executable.Path.startsWith("/tmp/")
Workshop checks every field against the telemetry schema of the signal's operating system when you save it. A misspelt field, or a field from the other platform, is rejected rather than silently never matching on the host. See the macOS schema for the fields each event carries.
Functions
Signals can use the standard CEL macros, the CEL string extensions, and these functions:
| Function | Returns | Description |
|---|---|---|
correlate(window, threshold) | bool | True once the preceding condition has matched threshold times within the trailing window |
correlate(window, threshold, groupBy) | bool | The same, with a separate window per groupBy value (a string or a list of strings) |
days(n) | duration | A duration of n days, for windows longer than duration() expresses cleanly |
sequence(stage, of, maxspan, key) | bool | True on the final stage of an ordered sequence of events. See Sequences |
Place correlate() and sequence() after the condition they apply to (cond && correlate(...)), so only matching events count.
Their windows and progress persist across scans.
Sequences
sequence() matches an ordered series of events that share a join key, all within a time span.
Write one branch per stage, each guarded by its event type and predicate, with sequence() last:
(has(event.execution) && event.execution.Target.Executable.Path == "/usr/bin/curl" &&
sequence(1, 2, duration("30s"), event.execution.Target.ID)) ||
(has(event.network_activity) &&
sequence(2, 2, duration("30s"), event.network_activity.Process.ID))
stageis the branch's position in the sequence, starting at 1.ofis the number of stages in the sequence.maxspanis how long the whole sequence has to complete, measured from its first stage.keyjoins the stages: only events with the same key advance the same sequence.
The signal reports when the final stage matches.
Join Keys
The key must be one of these types:
| Key | Example |
|---|---|
| A process identity | event.execution.Target.ID |
| A string | event.execution.Target.Executable.Path |
| A list of strings | [event.execution.Hostname, event.execution.Target.Executable.Path] |
Workshop rejects any other key when you save the signal, because Santa cannot join on it and the signal would never report.
That includes a bare PID such as event.execution.Target.ID.PID: PIDs are reused, so a process identity pairs the PID with a value that tells two processes with the same PID apart.
It also includes a whole process record such as event.execution.Target: pass its ID instead.
Stage Predicates
When two stages match the same event type, make their predicates mutually exclusive. If one event can satisfy both, it restarts the sequence on one stage and is refused by the other as the same event, so the signal may never report.
sequence() requires Santa 2026.8.
Workshop sets a minimum Santa version of 2026.8 on every signal that uses it, so hosts on anything older are not sent the signal.
See Also
- Events: where signal reports appear
- Telemetry: how to enable telemetry collection
- macOS schema: the fields
eventexposes - CEL Guide: CEL in execution rules