# Predicate reference

> Part of the Cowliss docs. Markdown for every page: append `index.md` to its URL. Site map: /llms.txt

The lookup. [Segments](/segments/) is the story.

## The definition

| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `match` | `"all"` \| `"any"` | no (`all`) | How the list combines. Deliberately flat: one list, no nested groups. |
| `predicates` | array | yes | Trait and event predicates, in any order. |

A definition names no app. The segment row carries the one app it belongs to, and both sides of every rule read that app's profiles and events. There is no filter on the source an event arrived through either.

## Trait predicates

```json
{ "kind": "trait", "name": "plan", "op": "eq", "value": "free" }
```

| Op | Reads `value` | Matches |
| --- | --- | --- |
| `eq`, `neq` | yes | Equal, or not. |
| `gt`, `gte`, `lt`, `lte` | yes | Ordered comparison. |
| `contains` | yes | Membership in an array trait, substring in a string. |
| `exists`, `notExists` | no | Whether the trait is set at all. |
| `before`, `after` | yes | A date trait earlier or later than now, or than now offset by a duration. |

Comparison coerces form-friendly values first: `"150"` matches `150` numerically, and `" TRUE "` matches `true` for `eq` and `neq`, trimmed and case-insensitive. Ordering never coerces booleans.

### Dates compared with now

`before` and `after` read a trait that holds a date, as an ISO 8601 string or epoch milliseconds, and compare it with now. The `value` is `"now"`, `{ "ago": "365d" }` or `{ "ahead": "3d" }`, with the durations an automation waits by (`"12h"`, `"3d"`, `"1w"`, or milliseconds). A trait that is not a date never matches.

- **Trial ended**: `{ "kind": "trait", "name": "trialEndsAt", "op": "before", "value": "now" }`
- **Trial ends in less than 3 days**: two predicates under `"match": "all"`, `"after"` with `"now"`, and `"before"` with `{ "ahead": "3d" }`.
- **Renewed more than a year ago**: `{ "kind": "trait", "name": "renewedAt", "op": "before", "value": { "ago": "365d" } }`

Send the date your app already knows on `identify`, and send it again when it changes: a trial extended by a week is one new `trialEndsAt`. Like a window, the comparison changes with time, so Cowliss re-checks the person when it flips: see [Windows lapse on their own](#windows-lapse-on-their-own).

## Event predicates

```json
{ "kind": "event", "name": "order.placed", "op": "performed", "withinDays": 30, "atLeast": 2 }
```

| Field | Required | Meaning |
| --- | --- | --- |
| `name` | yes | An event name, a pattern, or a list of them. |
| `op` | yes | `performed` or `notPerformed`. |
| `atLeast` | no (`1`) | How many times. At most 20 when `withinDays` is set. |
| `withinDays` | no | A rolling window relative to evaluation time. Absent means all history. |

A name matches the way an automation trigger does: `*` matches any run of characters, so `order.*` covers every order event and `*` is any activity at all. A pattern reaches the events Cowliss writes itself only when it starts with `system.`, so `*` never counts them. A list matches any of its names. When several names match, their counts add up: `"name": ["order.placed", "order.refunded"]` with `"atLeast": 2` holds for one of each.

A window reads the 20 most recent times of each event name, so `atLeast` inside a window is at most 20, and a definition asking for more is refused. Over all history there is no cap: the count is exact at any size.

History counts for as long as the profile exists. Your event retention setting decides how long the event feed and exports keep raw events, not who is in a segment: someone who ordered once stays in "ordered at least once" after that order has left the feed.

Events merged in from an anonymous identity count, so a visitor who signed up keeps the history they built before they had a name. See [Identity](/tracking/identity/).

## Windows lapse on their own

A `withinDays` window, and a date compared with now, move with time, not only with writes. Each evaluation notes when the answer would change if nothing else happened, such as a customer's last order plus 120 days or a trial's end minus 3 days, and Cowliss re-checks the person at that moment, to the minute. Someone who stops ordering enters "no order in the last 120 days" on day 120 without sending you anything, and an automation that enrolls from that segment starts for them then.

Only a lapse that comes due, or a live write, starts an automation. Recomputing history starts nobody: creating or editing a segment, and importing events, correct membership quietly on the timeline. An import still arms the lapses that follow it: a customer whose 120th day falls next week is reached next week. People who were already in a segment enter its automation when somebody [turns the automation on](/automations/#enable-disable-observe).

Anything that has to happen to the second belongs in an automation's `sleep`, not in a window.
