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

# CEL expression

> Functions, operators, and Channel references for writing expressions using Sift's supported subset of CEL in Calculated Channels, Rules, and User-Defined Functions

export const MintTable = ({columns = [], rows = [], columnWidths = []}) => {
  const pushTextWithLineBreaks = (parts, text, keyBase) => {
    const segments = String(text).split(/\\n|\n/);
    segments.forEach((segment, idx) => {
      if (segment) {
        parts.push(<span key={`${keyBase}-text-${idx}`}>{segment}</span>);
      }
      if (idx < segments.length - 1) {
        parts.push(<br key={`${keyBase}-br-${idx}`} />);
      }
    });
  };
  const parseMarkdown = text => {
    if (text === null || text === undefined) return "";
    const str = String(text);
    const parts = [];
    let lastIndex = 0;
    const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[([^\]]+)\]\(([^)]+)\))/g;
    let match;
    while (true) {
      match = pattern.exec(str);
      if (match === null) {
        break;
      }
      if (match.index > lastIndex) {
        pushTextWithLineBreaks(parts, str.substring(lastIndex, match.index), `before-${lastIndex}`);
      }
      const fullMatch = match[0];
      if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
        parts.push(<code key={match.index}>{fullMatch.slice(1, -1)}</code>);
      } else if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
        parts.push(<strong key={match.index}>{fullMatch.slice(2, -2)}</strong>);
      } else if (fullMatch.startsWith("*") && fullMatch.endsWith("*")) {
        parts.push(<em key={match.index}>{fullMatch.slice(1, -1)}</em>);
      } else if (fullMatch.startsWith("[")) {
        const linkText = match[2];
        const linkUrl = match[3];
        parts.push(<a key={match.index} href={linkUrl} className="text-black-600 dark:text-black-400">
            {linkText}
          </a>);
      }
      lastIndex = pattern.lastIndex;
    }
    if (lastIndex < str.length) {
      pushTextWithLineBreaks(parts, str.substring(lastIndex), `tail-${lastIndex}`);
    }
    if (parts.length > 0) {
      return parts;
    }
    const plainParts = [];
    pushTextWithLineBreaks(plainParts, str, "plain");
    return plainParts.length ? plainParts : str;
  };
  const safeColumns = Array.isArray(columns) ? columns : [];
  const safeRows = Array.isArray(rows) ? rows : [];
  const safeColumnWidths = Array.isArray(columnWidths) ? columnWidths : [];
  const hasColumnWidths = safeColumnWidths.some(w => w !== null && w !== undefined && w !== "");
  const toCssWidth = width => typeof width === "number" ? `${width}px` : String(width);
  const getColumnStyle = idx => {
    const rawWidth = safeColumnWidths[idx];
    if (rawWidth === null || rawWidth === undefined || rawWidth === "") {
      return undefined;
    }
    const width = toCssWidth(rawWidth);
    return {
      width,
      minWidth: width
    };
  };
  const containerStyle = hasColumnWidths ? undefined : {
    overflowX: "auto"
  };
  const tableStyle = hasColumnWidths ? {
    tableLayout: "fixed",
    width: "100%"
  } : {
    width: "max-content",
    minWidth: "100%"
  };
  if (!Array.isArray(columns) || !Array.isArray(rows) || !Array.isArray(columnWidths)) {
    console.warn("MintTable received invalid props:", {
      columns,
      rows,
      columnWidths
    });
  }
  if (!safeColumns.length && !safeRows.length) {
    return null;
  }
  return <div className="mint-table-container" style={containerStyle}>
      <table style={tableStyle}>
        {hasColumnWidths && <colgroup>
            {safeColumns.map((_, idx) => {
    const style = getColumnStyle(idx);
    return <col key={idx} style={style} />;
  })}
          </colgroup>}
        <thead>
          <tr>
            {safeColumns.map((col, idx) => <th key={idx} className="text-left" style={getColumnStyle(idx)}>
                <b>{parseMarkdown(col)}</b>
              </th>)}
          </tr>
        </thead>
        <tbody>
          {safeRows.map((row, rIdx) => {
    const safeRow = Array.isArray(row) ? row : [];
    return <tr key={rIdx}>
                {safeRow.map((cell, cIdx) => <td key={cIdx} style={getColumnStyle(cIdx)}>
                    {parseMarkdown(cell)}
                  </td>)}
              </tr>;
  })}
        </tbody>
      </table>
    </div>;
};

Use this page to look up CEL expression syntax in Sift. Expressions are written using a subset of Common Expression Language ([CEL](https://cel.dev)) and are used in Calculated Channels, Rules, and User-Defined Functions. Sift supports a subset of CEL data types and operators, not the full CEL specification.

## Channel references

When configuring a Rule, Calculated Channel, or User-Defined Function, you select input Channels by name from the **Input Channels** selector. Sift automatically assigns each selected Channel a shorthand variable based on selection order: `$1` for the first Channel, `$2` for the second, `$3` for the third, and so on. Use these variables to reference the corresponding Channel in your expression.

## Operators

The following table lists the CEL operators supported in Sift. Not all CEL operators are supported.

<MintTable
  columns={['Operator', 'Type', 'Description', 'Example']}
  columnWidths={['5%', '15%', '40%', '30%']}
  rows={[
['`+`', 'Arithmetic', 'Addition', '`$1 + $2`'],
['`-`', 'Arithmetic', 'Subtraction', '`$1 - $2`'],
['`*`', 'Arithmetic', 'Multiplication', '`$1 * $2`'],
['`/`', 'Arithmetic', 'Division', '`$1 / $2`'],
['`>`', 'Comparison', 'Greater than', '`$1 > 100`'],
['`<`', 'Comparison', 'Less than', '`$1 < 100`'],
['`>=`', 'Comparison', 'Greater than or equal to', '`$1 >= 100`'],
['`<=`', 'Comparison', 'Less than or equal to', '`$1 <= 100`'],
['`==`', 'Comparison', 'Equal to', '`$1 == 100`'],
['`!=`', 'Comparison', 'Not equal to', '`$1 != 100`'],
['`&&`', 'Boolean', 'Logical AND', '`$1 > 20 && $1 < 80`'],
['`||`', 'Boolean', 'Logical OR', '`$1 < 20 || $1 > 80`'],
['`!`', 'Boolean', 'Logical NOT', '`!($1 > 100)`'],
['`? :`', 'Ternary', 'Returns one of two values based on a condition: \n\n `condition ? value_if_true : value_if_false`', '`$1 > 30 ? \'High\' : \'Normal\'`'],
]}
/>

## Functions

The **Expression Syntax** panel in the expression editor organizes functions into the following sections:

<MintTable
  columns={['Section', 'Description']}
  columnWidths={['25%', '75%']}
  rows={[
['Math functions', 'Numeric calculations such as trigonometry, rounding, and logarithms.'],
['String functions', 'Operations for evaluating and manipulating string values.'],
['List functions', 'Check whether a value belongs to a list.'],
['Iteration functions', 'Emit expression results only when a condition is met.'],
['[Stateful functions](#stateful-functions)', 'Compute values over a rolling window of preceding data points.'],
['[File functions](#file-functions)', 'Access data from files attached to an Asset or a Run directly in the expression of a Rule or Calculated Channel.'],
['User-defined functions', 'Named, reusable functions created by your organization.'],
['Other', 'Utility functions for timestamps, enums, and grouped time-series aggregations.'],
]}
/>

## Stateful functions

Stateful functions compute values using data from preceding rows within a rolling time window of up to **10 minutes**. Use them to track trends, detect gradual changes, and compute time-based metrics across telemetry data.

The following table lists all available stateful functions with expanded descriptions to help you choose the right function for your use case.

<MintTable
  columns={['Function', 'Description']}
  columnWidths={['30%', '70%']}
  rows={[
['`avg(x, rolling(y))`', 'Computes the average value of `x` over the rolling window `y`. Use this to smooth noisy signals or track a moving average over time'],
['`median(x, rolling(y))`', 'Computes the median value of `x` over the rolling window `y`. More robust than `avg()` when the signal contains outliers'],
['`min(x, rolling(y))`', 'Returns the minimum value of `x` observed within the rolling window `y`'],
['`max(x, rolling(y))`', 'Returns the maximum value of `x` observed within the rolling window `y`'],
['`stdev(x, rolling(y))`', 'Standard deviation of `x` over the rolling window `y` using sample variance. Use this to measure signal variability or detect instability'],
['`sum(x, rolling(y))`', 'Returns the sum of all values of `x` within the rolling window `y`'],
['`count(x, rolling(y))`', 'Returns the number of data points of `x` within the rolling window `y`. Useful for detecting gaps or irregular sampling rates'],
['`all(x, rolling(y))`', 'Returns true if every value of `x` within the rolling window `y` evaluates to true. Use this to confirm a condition has held consistently over a time period'],
['`any(x, rolling(y))`', 'Returns true if at least one value of `x` within the rolling window `y` evaluates to true'],
['`first(x, rolling(y))`', 'Returns the first value of `x` within the rolling window `y`'],
['`last(x, rolling(y))`', 'Returns the most recent value of `x` within the rolling window `y`'],
['`rolling(x)`', 'Defines a rolling window of size `x`, where `x` is a number of seconds or a duration string such as `"5s"` or `"2m"`'],
['`previous(x)`', 'Returns the value of expression `x` from the immediately preceding data point. Use this to compare the current value against the last known value, or to compute time differences using `t - previous(t)`'],
['`delta(x)`', 'Returns the difference between the current value of `x` and its previous value. Equivalent to `x - previous(x)`. Use this to measure how much a signal changed between consecutive data points'],
['`deriv(x)`', 'Returns the rate of change of `x` per millisecond, computed as `delta(x) / delta(t)`. Use this to detect how fast a signal is rising or falling'],
['`persistence(x, y)`', 'Returns true if condition `x` has been continuously true for at least duration `y`, where `y` is a number of seconds or a duration string. Use this to confirm a fault or threshold breach is sustained, not just a momentary spike'],
['`changed(x)`', 'Returns true if the value of `x` has changed compared to the previous data point. Also returns true on the first data point. Use this to detect state transitions or mode changes in a signal'],
['`bucket(duration)`', 'Defines a fixed-size time bucket for use as the second argument to aggregation functions such as `avg()`, `min()`, `max()`, `sum()`, and `stdev()`. Unlike `rolling()`, which uses a sliding window centered on each data point, `bucket()` maps samples to a uniform time grid. \n\nThis normalizes signals sampled at different rates to a single value per interval before aggregation. For example, `avg($1, bucket(1s))` averages the input channel into 1-second buckets. Use `bucket()` when comparing live signal values against [Family statistics](/documentation/reference/families-reference) to avoid sample-rate bias skewing the comparison. \n\n See [Bucketing and aggregation](/documentation/reference/families-reference#bucketing-and-aggregation) for a full explanation of the bucketing pipeline.'],
]}
/>

## File functions

File functions allow Rules and Calculated Channel expressions to access data from files attached to an Asset or a Run. Once a file is loaded, you access specific values using bracket notation. The syntax depends on the file format.

<Warning>
  **Live Rules**: File functions are not supported in live Rule evaluation.
</Warning>

The following table lists the available file functions:

<MintTable
  columns={['Function', 'Description']}
  columnWidths={['30%', '70%']}
  rows={[
['`assetFile("filename")`', 'Reads a file attached to the Asset the Rule or Calculated Channel is configured for. \n\n The file is verified when the expression is created or edited. \n\n Use this for data that stays the same across Runs, such as hardware limits, calibration offsets, or hardware specifications.'],
['`runFile("filename")`', 'Reads a file attached to the Run being evaluated. \n\n The file is resolved at evaluation time, when a Rule is evaluated against a Run or when a Calculated Channel is evaluated against a Run. \n\n Use this for data that changes between Runs, such as test parameters, environmental conditions, or per-session acceptance criteria.'],
]}
/>

### Supported file formats

Files that can be referenced by the file functions must be in one of the following supported formats: **CSV**, **JSON**, **TXT**, or **YAML**.

<Warning>
  **File size**: The maximum file size per file is **10 MB**.
</Warning>

### Bracket notation

Once a file is loaded, use bracket notation to access specific values. The syntax depends on the file format.

#### JSON and YAML

JSON and YAML files resolve to a map. Use bracket notation to index into keys. For nested objects, chain brackets. To access an item in a list, use a numeric index.

<MintTable
  columns={['Format', 'Structure', 'Syntax']}
  columnWidths={['10%', '30%', '60%']}
  rows={[
['JSON', 'Access a key.', '`assetFile("file.json")["key"]`'],
['JSON', 'Access a nested key.', '`assetFile("file.json")["key"]["nested_key"]`'],
['JSON', 'Access an item in a list.', '`assetFile("file.json")["key"][0]`'],
['YAML', 'Access a key.', '`runFile("file.yaml")["key"]`'],
['YAML', 'Access a nested key.', '`runFile("file.yaml")["key"]["nested_key"]`'],
['YAML', 'Access an item in a list.', '`runFile("file.yaml")["key"][0]`'],
]}
/>

#### CSV

CSV files resolve to a column-oriented structure. Use the column name or column index (0-based) as the first bracket and a row number as the second.

<MintTable
  columns={['Format', 'Structure', 'Syntax']}
  columnWidths={['10%', '30%', '60%']}
  rows={[
['CSV', 'Access a column value by name and row number.', '`runFile("file.csv")["column_name"][0]`'],
['CSV', 'Access a column value by index and row number.', '`runFile("file.csv")[2][0]`'],
]}
/>

#### TXT

TXT files resolve to a string literal. To use the value numerically, cast it with a function like `double()`.

<MintTable
  columns={['Format', 'Structure', 'Syntax']}
  columnWidths={['10%', '30%', '60%']}
  rows={[
['TXT', 'Read as a string literal.', '`runFile("max_temp.txt")`'],
['TXT', 'Cast to a numeric value.', '`double(runFile("max_temp.txt"))`'],
]}
/>

## User-Defined Functions

The User-Defined Functions section of the expression editor lists all named, reusable functions created across your organization. Call any function by name directly in your expression. To learn more, see [User-Defined Functions settings](/documentation/reference/user-defined-functions-settings).

## Behavior

The following table describes known constraints and behaviors to be aware of when writing expressions in Sift.

<MintTable
  columns={['Limitation', 'Description']}
  columnWidths={['25%', '75%']}
  rows={[
['Channel references', 'Calculated Channels can be nested as inputs in other Calculated Channels up to 10 levels deep. User-Defined Functions have no such limit.'],
['Bitfield Channels', 'Bitfield Channels are not supported as inputs for Calculated Channels.'],
['Rolling windows', 'Rolling windows can be up to **10 minutes** long and only include rows that come before the current row. Future values are never included.'],
['Aggregations', 'All aggregation functions require an input expression and a `rolling(y)` window definition. For example: `avg($1 + 10, rolling(5))`.'],
['Multiple time series', 'When an expression combines multiple Channels with different timestamps, Sift uses an as-of join strategy. \n\nFor each unique timestamp, it uses the most recent available value from each Channel, allowing meaningful results even when data streams arrive at different rates.'],
['Return types', 'Expressions may return numeric or string results.'],
['File size limit', 'The maximum file size for file functions is 10 MB.'],
['File functions in live Rules', 'File functions are not supported in live Rule evaluation.'],
['Enum comparisons', 'Enum Channels are exposed to CEL as strings. Compare them using a quoted string literal that matches the variant name, such as `$1 == \'FAULT\'`. \n\nComparing to a bare identifier (`$1 == FAULT`) or an integer (`$1 == 3`) saves without error but fails when the expression is evaluated.'],
['Data gaps', 'When a Channel stops reporting, Sift carries forward its last known value indefinitely; there is no staleness cutoff. `previous()` and `delta()` treat the gap as continuous data, and `changed()` does not fire when the Channel resumes reporting. \n\nTo detect whether a Channel is currently reporting data, use `count($1, rolling(y)) > 0`.'],
['Rule evaluation trigger', 'A Rule only evaluates when one of its referenced Channels receives a new data point; evaluation is data-driven, not time-driven. \n\nThis means `count($1, rolling(y)) > 0` only detects a gap in `$1` while something else keeps the Rule evaluating, such as another referenced Channel that is still reporting. \n\nIf a Rule references only the Channel that goes silent, the Rule stops evaluating entirely and cannot detect that the Channel has stopped reporting.'],
['Missing Channels', 'All Channels referenced in an expression must exist on the Run being evaluated. \n\nIf a referenced Channel is absent from a Run, the Rule cannot evaluate and returns an error for that Run. See [Rule errors when a Run is missing a referenced Channel](/documentation/troubleshoot/troubleshooting#rule-errors-when-a-run-is-missing-a-referenced-channel) for a workaround.'],
['Rolling window fragmentation', 'When a Channel\'s sampling rate is close to the duration of the rolling window `y`, a stateful function such as `count(x, rolling(y))` used as a Rule condition can flicker across the window boundary. \n\nThis can fragment what should be one continuous Annotation into many separate Annotations.'],
]}
/>

## Related workflows

* [Detect deviations automatically using Rules](/documentation/review/detect-deviations-automatically-using-rules)
* [Create a derived signal](/documentation/transform/create-a-derived-signal)
* [Reuse expression logic](/documentation/transform/reuse-expression-logic)
