Rules

A rule is a condition evaluated against a completed session. When it matches, it writes a tag. Tags are what you count, filter and get notified about.

Rules live in a YAML file in your repository. plan shows the difference between that file and what is deployed. apply reconciles them. Your definition of a conversion is reviewed like any other change, and its history is in your version control.

A rules file

origin: https://example.com
project: myapp/home

rules:
  - rule_id: any_session
    tag: visited
    logic: {">=": [{"var": "session.num_events"}, 1]}

  - rule_id: bounce
    tag: bounce
    description: One action and nothing further
    logic: {"==": [{"var": "session.num_events"}, 1]}

  - rule_id: engaged
    tag: engaged
    description: Three or more actions
    logic: {">=": [{"var": "session.num_events"}, 3]}

  - rule_id: pricing_visitor
    tag: pricing
    notification_target: email
    requires_events: true
    description: Visited pricing, the clearest purchase intent signal
    logic:
      {"some": [{"var": "events"},
                {"==": [{"var": "payload.path"}, "/pricing.html"]}]}
Field Required Description
rule_id yes Stable identifier. Changing it deletes one rule and creates another.
tag yes The label written on a match. Several rules may write the same tag.
logic yes A JSONLogic expression returning true or false.
description no Shown in listings and in the digest. Write these.
notification_target no Send an immediate message on match. Currently email.
requires_events no Load the session’s full event list before evaluating.

Deploying rules

clog plan --file rules.yaml --origin https://example.com --project myapp/home
example.com/myapp/home
comparing rules.yaml against deployed rules

  ~ any_session         [modified]
      notification_target: null -> "email"

  = engaged             [unchanged]
  = bounce              [unchanged]

  + pricing_visitor     [new - will be created]
      tag: pricing
      logic: {"some": [...]}

  - old_rule            [removed - will be deleted]

summary: 1 modified, 1 added, 1 removed, 2 unchanged
clog apply --file rules.yaml --origin https://example.com --project myapp/home

apply shows the plan and prompts before writing. --auto-approve skips the prompt, --dry-run writes nothing.

Exit codes make this usable in a pipeline:

Code Meaning
0 No differences
1 Differences found
2 Validation error in the YAML
3 Authentication or API error

--strict treats any drift as a failure, for pipelines where the deployed state must always match the file.

The session context

A rule evaluates against one session. These fields are available under session:

Field Type Description
origin string Site URL
project string Project identifier
session string Session identifier
start_time int First event, milliseconds since epoch
end_time int Last event, milliseconds since epoch
duration int Milliseconds between the two
num_events int Total events in the session
unique_actions int Distinct action names
actions list Flat list of action names, for existence checks
action_map list Ordered [action, offset_ms] pairs
geo object countryCode, region, city, lat, lon, timezone, org
ua object browser, os, device_type, is_mobile, is_bot

With requires_events: true, the full event list is loaded and available as events at the top level. Each entry carries action, time and payload.

Loading events costs a database read per session, so use requires_events only when the rule reads a payload.

Patterns

A named action occurred. The cheapest form, no event load required.

logic: {"in": ["user.signup.success", {"var": "session.actions"}]}

A payload field matched. Requires events.

requires_events: true
logic:
  {"some": [{"var": "events"},
            {"==": [{"var": "payload.plan"}, "annual"]}]}

Reading depth. The scroll milestone payload.

requires_events: true
logic:
  {"some": [{"var": "events"}, {">=": [{"var": "payload.percent"}, 75]}]}

Time on page. From the page_exit payload.

requires_events: true
logic:
  {"some": [{"var": "events"}, {">=": [{"var": "payload.dwell_ms"}, 60000]}]}

Arrived from a specific referrer. The first event’s payload, without loading the full list.

logic:
  {"in": ["news.ycombinator.com",
          {"var": "session.events.0.payload.referrer"}]}

Several conditions. and, or and ! compose.

requires_events: true
logic:
  {"and": [
    {">=": [{"var": "session.num_events"}, 3]},
    {"==": [{"var": "session.geo.countryCode"}, "GB"]},
    {"some": [{"var": "events"},
              {"==": [{"var": "payload.path"}, "/pricing.html"]}]}
  ]}

Human traffic only. Bot classification is available to rules.

logic: {"==": [{"var": "session.ua.is_bot"}, false]}

Testing before you deploy

Evaluate a rule against a real session:

clog test --file rules.yaml --rule pricing_visitor --session 1782693098872-7xzwuxbzd

Or against a file of known examples, which is the form to run in CI:

clog test --file rules.yaml --rule pricing_visitor --examples fixtures/sessions.json

Build the fixture file from sessions you have already seen:

clientlog --origin https://example.com --project myapp/home --format json \
  sessions get --sessions 1782693098872-7xzwuxbzd > fixtures/sessions.json

A rule that has never been tested against a session that should match it, and one that should not, is a rule you are guessing about.

Notifications

notification_target: email sends a message the moment a matching session is classified.

Reserve it for signals worth interrupting yourself for: a completed purchase, a demo request, a visit from a named account. A rule matching a third of your traffic produces a mailbox nobody reads and hides the signals that mattered. Everything else belongs in the digest.

Rules as accumulated knowledge

Rules are a record of what you have learned about your own traffic. A rule that isolated a bot pattern, a referrer worth watching, a page that predicts purchase: each one is a finding, encoded so it applies to every future session without you looking again.

That is the argument for holding them in version control alongside the site they measure. The rule and the page it depends on change together, and the commit says why.

Next: reading the digest.