{
  "topic": "predicates",
  "path": [
    "predicates"
  ],
  "title": "predicates — search & criteria condition semantics",
  "synopsis": "predicates — operator catalog and evaluation semantics for the `Condition` DSL used by search (`cyoda help search`) and workflow/transition criteria (`cyoda help workflows`). Both consume the same kernel, so everything here applies identically to both.",
  "body": "# predicates\n\n## NAME\n\npredicates — operator catalog and evaluation semantics for the `Condition` DSL used by search (`cyoda help search`) and workflow/transition criteria (`cyoda help workflows`). Both consume the same kernel, so everything here applies identically to both.\n\n## OPERATORS\n\n**Unary** (test presence, no operand type constraint):\n\n- `IS_NULL` — the leaf is absent or JSON `null`.\n- `NOT_NULL` — the leaf is present and non-null.\n\n**Range** (operand is a two-element `[low, high]` array):\n\n- `BETWEEN` — exclusive bounds.\n- `BETWEEN_INCLUSIVE` — inclusive bounds.\n\n**Binary — comparison** (same-type `compareTo`):\n\n- `EQUALS`, `NOT_EQUAL`, `GREATER_THAN`, `GREATER_OR_EQUAL`, `LESS_THAN`, `LESS_OR_EQUAL`.\n- `IEQUALS` (case-insensitive equals), `INOT_EQUAL` (case-insensitive not-equal).\n\n**Binary — string** (case-sensitive; `I*`/`INOT_*` fold both sides):\n\n- `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, and their `NOT_*` inverses.\n- `ICONTAINS`/`INOT_CONTAINS`, `ISTARTS_WITH`/`INOT_STARTS_WITH`, `IENDS_WITH`/`INOT_ENDS_WITH`.\n\n**Binary — pattern:**\n\n- `LIKE` — anchored glob, see LIKE GRAMMAR below.\n- `MATCHES_PATTERN` — anchored regular expression, see MATCHES_PATTERN below.\n\n**Not supported:** `IS_CHANGED`/`IS_UNCHANGED` are change-generation operators, not search predicates — cyoda-go does not implement them.\n\n## TYPE-DIRECTED COMPARISON\n\nA condition's operand is compared against the target field's **declared type(s)** (a leaf may carry more than one, e.g. a field seen as both an integer and a string across entities). The operand is treated as a string and parse-tested against every declared type — a numeric-looking string operand (`\"30\"`) and a JSON number operand (`30`) are parsed identically and are **treated the same**. There is no cross-type coincidental matching: an operand that parses as a number only compares against numerically-stored values; an operand that parses only as a string compares against string-stored values. Numbers compare precisely (arbitrary-precision, not `float64` — correct beyond 2^53). Comparing against `MATCHES_PATTERN`/`LIKE` and string ops evaluate against textual stored values only; a string op against a non-textual stored value is a non-match, not an error.\n\n## NULL SEMANTICS\n\nA missing (absent) or JSON-`null` leaf **never matches any binary operator — including negatives**. `NOT_EQUAL`, `NOT_CONTAINS`, `INOT_*`, and every other negated op are **null-guarded to non-match**, not `!positive` — a null/absent field does not satisfy a negative condition just because it fails the positive one. `IS_NULL` / `NOT_NULL` are the only operators that test presence directly.\n\n## LIKE GRAMMAR\n\n`LIKE` compiles its operand to an anchored regular expression:\n\n- `%` — matches any sequence of characters (including empty).\n- `_` — matches exactly one character.\n- `\\` — escapes a following `%`, `_`, or `\\` to its literal form.\n- The match is **whole-string anchored** (the entire stored value must match, not a substring) and **case-sensitive**.\n\n## MATCHES_PATTERN\n\n`MATCHES_PATTERN` compiles the operand as a Go RE2 regular expression, whole-string anchored (equivalent to Java's `Pattern.matches`), case-sensitive. RE2 and the Java regex dialect diverge on some constructs (e.g. backreferences, some lookaround); this is an accepted, bounded divergence — not reconciled.\n\n## VALIDATION\n\nValidation is **parse-based**, evaluated at request time against the target model:\n\n- `400 CONDITION_TYPE_MISMATCH` — the operand parses into **none** of the field's declared types (comparison and range operators only; string operators and unary presence tests carry no operand-type constraint and are always accepted).\n- `400 INVALID_FIELD_PATH` — the field path is unknown to the model, **or** it names a pure-container (object) path: a scalar operator cannot compare against structure — navigate to a scalar leaf sub-path instead. (A path observed as both an object and a scalar across entities remains searchable via its scalar type — see `cyoda help search`.) `IS_NULL`/`NOT_NULL` are exempt from the container-path rejection since they test presence, not a value.\n- `400 INVALID_CONDITION` — the operand is `null` on a binary/range operator, a range operator's value is not a two-element array, or the operand is an object/complex value.\n\nThere is no operator-versus-field-type rejection: `CONTAINS` on a numeric field or `GREATER_THAN` on a boolean field are accepted requests — they parse and simply evaluate to a (non-)match.\n\n## SEE ALSO\n\n- search\n- workflows\n- errors.CONDITION_TYPE_MISMATCH\n- errors.INVALID_FIELD_PATH\n- errors.INVALID_CONDITION\n",
  "sections": [
    {
      "name": "NAME",
      "body": "predicates — operator catalog and evaluation semantics for the `Condition` DSL used by search (`cyoda help search`) and workflow/transition criteria (`cyoda help workflows`). Both consume the same kernel, so everything here applies identically to both."
    },
    {
      "name": "OPERATORS",
      "body": "**Unary** (test presence, no operand type constraint):\n\n- `IS_NULL` — the leaf is absent or JSON `null`.\n- `NOT_NULL` — the leaf is present and non-null.\n\n**Range** (operand is a two-element `[low, high]` array):\n\n- `BETWEEN` — exclusive bounds.\n- `BETWEEN_INCLUSIVE` — inclusive bounds.\n\n**Binary — comparison** (same-type `compareTo`):\n\n- `EQUALS`, `NOT_EQUAL`, `GREATER_THAN`, `GREATER_OR_EQUAL`, `LESS_THAN`, `LESS_OR_EQUAL`.\n- `IEQUALS` (case-insensitive equals), `INOT_EQUAL` (case-insensitive not-equal).\n\n**Binary — string** (case-sensitive; `I*`/`INOT_*` fold both sides):\n\n- `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`, and their `NOT_*` inverses.\n- `ICONTAINS`/`INOT_CONTAINS`, `ISTARTS_WITH`/`INOT_STARTS_WITH`, `IENDS_WITH`/`INOT_ENDS_WITH`.\n\n**Binary — pattern:**\n\n- `LIKE` — anchored glob, see LIKE GRAMMAR below.\n- `MATCHES_PATTERN` — anchored regular expression, see MATCHES_PATTERN below.\n\n**Not supported:** `IS_CHANGED`/`IS_UNCHANGED` are change-generation operators, not search predicates — cyoda-go does not implement them."
    },
    {
      "name": "TYPE-DIRECTED COMPARISON",
      "body": "A condition's operand is compared against the target field's **declared type(s)** (a leaf may carry more than one, e.g. a field seen as both an integer and a string across entities). The operand is treated as a string and parse-tested against every declared type — a numeric-looking string operand (`\"30\"`) and a JSON number operand (`30`) are parsed identically and are **treated the same**. There is no cross-type coincidental matching: an operand that parses as a number only compares against numerically-stored values; an operand that parses only as a string compares against string-stored values. Numbers compare precisely (arbitrary-precision, not `float64` — correct beyond 2^53). Comparing against `MATCHES_PATTERN`/`LIKE` and string ops evaluate against textual stored values only; a string op against a non-textual stored value is a non-match, not an error."
    },
    {
      "name": "NULL SEMANTICS",
      "body": "A missing (absent) or JSON-`null` leaf **never matches any binary operator — including negatives**. `NOT_EQUAL`, `NOT_CONTAINS`, `INOT_*`, and every other negated op are **null-guarded to non-match**, not `!positive` — a null/absent field does not satisfy a negative condition just because it fails the positive one. `IS_NULL` / `NOT_NULL` are the only operators that test presence directly."
    },
    {
      "name": "LIKE GRAMMAR",
      "body": "`LIKE` compiles its operand to an anchored regular expression:\n\n- `%` — matches any sequence of characters (including empty).\n- `_` — matches exactly one character.\n- `\\` — escapes a following `%`, `_`, or `\\` to its literal form.\n- The match is **whole-string anchored** (the entire stored value must match, not a substring) and **case-sensitive**."
    },
    {
      "name": "MATCHES_PATTERN",
      "body": "`MATCHES_PATTERN` compiles the operand as a Go RE2 regular expression, whole-string anchored (equivalent to Java's `Pattern.matches`), case-sensitive. RE2 and the Java regex dialect diverge on some constructs (e.g. backreferences, some lookaround); this is an accepted, bounded divergence — not reconciled."
    },
    {
      "name": "VALIDATION",
      "body": "Validation is **parse-based**, evaluated at request time against the target model:\n\n- `400 CONDITION_TYPE_MISMATCH` — the operand parses into **none** of the field's declared types (comparison and range operators only; string operators and unary presence tests carry no operand-type constraint and are always accepted).\n- `400 INVALID_FIELD_PATH` — the field path is unknown to the model, **or** it names a pure-container (object) path: a scalar operator cannot compare against structure — navigate to a scalar leaf sub-path instead. (A path observed as both an object and a scalar across entities remains searchable via its scalar type — see `cyoda help search`.) `IS_NULL`/`NOT_NULL` are exempt from the container-path rejection since they test presence, not a value.\n- `400 INVALID_CONDITION` — the operand is `null` on a binary/range operator, a range operator's value is not a two-element array, or the operand is an object/complex value.\n\nThere is no operator-versus-field-type rejection: `CONTAINS` on a numeric field or `GREATER_THAN` on a boolean field are accepted requests — they parse and simply evaluate to a (non-)match."
    },
    {
      "name": "SEE ALSO",
      "body": "- search\n- workflows\n- errors.CONDITION_TYPE_MISMATCH\n- errors.INVALID_FIELD_PATH\n- errors.INVALID_CONDITION"
    }
  ],
  "see_also": [
    "search",
    "workflows",
    "errors.CONDITION_TYPE_MISMATCH",
    "errors.INVALID_FIELD_PATH",
    "errors.INVALID_CONDITION"
  ],
  "stability": "stable",
  "actions": []
}
