---
title: Derived rules
description: Define custom "entered / exited a segment" events that fire automatically from your existing event stream, without changing your integration.
---

A derived rule watches a subject (an identified user or an account/tenant) for a
condition on its subscribed anchor events, and emits a brand-new event of your own
naming when that condition is met — a `high_value_order` event, say, fired the moment an
order's own `amount` property crosses a threshold, with no client-side code change
required.

The condition is evaluated against the anchor event that just arrived: its name, its
timestamp, its own properties, and a few profile facts. A rule cannot sum or count
across a subject's past events, so a threshold has to be one the triggering event
carries by itself — see [Worked example](#worked-example) and
[Guardrails](#guardrails).

## Prerequisites

- The anchor event(s) the rule subscribes to already flowing through
  [Sending events](/docs/sending-data/events) — a rule can only watch events that exist.

## Authentication

Every endpoint on this page is a **Management API** call — host `https://retidal.com`,
authenticated with the `trackly_session` cookie from a logged-in console user and your
project role. There is no API-key path here; see [Authentication](/docs/authentication)
for the full session-vs-key model.

## Listing rules

```
GET https://retidal.com/api/projects/{projectId}/derived-rules
```

Returns every derived rule configured for the project. Console → Projects → Derived
Rules shows the same list, with rule type, subject, and enabled/disabled state.

## Creating a rule

```
POST https://retidal.com/api/projects/{projectId}/derived-rules
```

<ParamField name="ruleType" in="body" type="string" required>
  `event_condition` (fires when a subscribed event's condition matches) or `absence`
  (fires when a subject has produced none of the subscribed events for longer than
  `absenceThresholdSeconds`).
</ParamField>
<ParamField name="subjectType" in="body" type="string" required>
  `identified_user` or `account`.
</ParamField>
<ParamField name="subscribedEvents" in="body" type="array" required>
  The anchor event names this rule watches. A derived event itself cannot be subscribed
  to — Retidal rejects a rule that would create a cycle.
</ParamField>
<ParamField name="outputEventName" in="body" type="string" required>
  The name of the new event this rule emits. Must not collide with an existing
  non-derived event name in the project.
</ParamField>

`POST /api/projects/{projectId}/derived-rules` returns `409` if the project's rule-count
cap is reached, and `422` if the body fails validation, creates a subscription cycle, or
the output event name conflicts with an existing one.

## Rule detail and updates (optional)

```
GET https://retidal.com/api/projects/{projectId}/derived-rules/{id}
PATCH https://retidal.com/api/projects/{projectId}/derived-rules/{id}
```

`GET` returns one rule's full detail. `PATCH` updates mutable fields only — `ruleType`,
`subjectType`, and `outputEventName` are immutable after creation; create a new rule
instead of trying to change them.

## Enabling and disabling a rule (optional)

```
POST https://retidal.com/api/projects/{projectId}/derived-rules/{id}/enable
```

Starts or stops a rule. A disabled rule stops evaluating incoming events entirely — it
does not queue or backfill events that arrived while disabled.

## What this unlocks

A derived rule turns a pattern in your existing event stream into a first-class event
name you can subscribe [Event mappings](/docs/sending-data/event-mappings) or an
automation to, without instrumenting a new client-side call for it.

## Verify it worked

<Steps>
  <Step title="Confirm the rule is enabled">
    Console → Projects → Derived Rules shows the rule with its enabled/disabled state.
    Equivalently, from a browser session already logged into the console (so the
    `trackly_session` cookie is sent automatically):
    ```bash
    curl -s https://retidal.com/api/projects/{projectId}/derived-rules/{id} \
      -H "Cookie: trackly_session=$SESSION_COOKIE"
    # expect: 200 with "enabled": true
    ```
  </Step>
  <Step title="Send an event that satisfies the rule's condition">
    Send one of the rule's `subscribedEvents` via [Sending events](/docs/sending-data/events),
    with a property value that makes the condition true (for the worked example below,
    a `chat.completed` event with `tokens` at least 2000) — the condition is evaluated
    only against that event's own properties, not a running total.
  </Step>
  <Step title="Confirm the output event appears">
    Check [Event catalog](/docs/sending-data/event-catalog) (or `GET
    /api/projects/{projectId}/project-events`) for the rule's `outputEventName`
    appearing with recent volume.
  </Step>
</Steps>

## If it doesn't work

If the rule never fires — the derived event itself never arrives — work through
[Events not arriving](/docs/troubleshooting/events-not-arriving); the rule not firing is
the same "expected event never showed up" symptom that page diagnoses.

## Worked example

A rule that emits `user.heavy_session` whenever an identified user sends a
`chat.completed` event whose own `tokens` property is at least 2000 — the rule engine
only ever sees the anchor event's own name, timestamp, and properties (plus a few
profile facts), not a cumulative count across past events, so the condition must be
evaluated against a fact the triggering event actually carries:

```json title="POST /api/projects/{projectId}/derived-rules"
{
  "name": "Heavy sessions",
  "ruleType": "event_condition",
  "subjectType": "identified_user",
  "subscribedEvents": ["chat.completed"],
  "condition": {
    "all": [
      { "fact": "event.tokens", "op": "gte", "value": 2000 }
    ]
  },
  "outputEventName": "user.heavy_session",
  "outputEventCategory": "Engagement",
  "enabled": true
}
```

## Guardrails

`outputEventName` is checked against every existing non-derived event name in the
project at creation time, but **not** re-checked if a matching event name is introduced
later — naming a derived event too generically (`completed`, `active`) risks a silent
future **naming collision** once that name is claimed elsewhere. Prefer a namespaced
output name (`user.power_user`, not `power_user`). Separately, `subscribedEvents` cannot
include another derived event — `POST /api/projects/{projectId}/derived-rules` rejects
that at creation with `422`, preventing an **infinite derivation loop** where rule A's
output feeds rule B whose output feeds rule A.

## Next steps

<CardGroup cols={2}>
  <Card title="Event catalog" href="/docs/sending-data/event-catalog">
    See a derived event's volume and downstream consumers once it starts firing.
  </Card>
  <Card title="Event mappings" href="/docs/sending-data/event-mappings">
    Map a derived event onto a standard event for ad-platform reporting.
  </Card>
</CardGroup>
