# Examples

Everything [Writing Rules](./) covers, applied to complete rulesets: first the demo's real, runnable ruleset file by file, then two patterns you'll reach for as your own ruleset grows—labels as state, and a multi-signal rule organized across files. The examples use the same small social network as the rest of these docs.

## The demo ruleset, file by file

The [demo](../development/) runs the actual ruleset in [`example_rules/`](https://github.com/roostorg/osprey/tree/main/example_rules), which is small enough to read in one sitting: two models, one rule, and one label. The platform being moderated has a single (admittedly strict) policy: users may never say "hello."

Events arrive as JSON like this—an event name (`action_name`), an ID, and whatever the platform sent:

```json
{
  "action_id": 1,
  "action_name": "create_post",
  "data": {
    "user_id": "user_1923",
    "event_type": "create_post",
    "post": { "text": "hello world" }
  }
}
```

**`main.sml`** is the entrypoint. This ruleset is small enough to skip conditional `index.sml` files entirely: import the base model, require the one rule file.

```python
Import(rules=['models/base.sml'])

Require(rule='rules/post_contains_hello.sml')
```

**`models/base.sml`** defines the features present on every event. `UserId` and `EventType` are declared as entities, so Osprey tracks them as _things_ that persist across events—that's what lets a label stick to a user later. `coerce_type=True` (the default) converts a mismatched value, like a numeric user ID, to the declared type instead of erroring.

```python
UserId: Entity[str] = EntityJson(
  type='User',
  path='$.user_id',
  coerce_type=True
)

EventType: Entity[str] = EntityJson(
  type='EventType',
  path='$.event_type',
  coerce_type=True
)

ActionName=GetActionName()

ActionId=GetActionId()
```

The last two lines pull the event's name and ID from Osprey itself rather than the JSON payload: `GetActionName()` and `GetActionId()` are stdlib UDFs, and exposing them as features makes them queryable in the UI.

**`models/post.sml`** adds the one feature specific to posts:

```python
PostText: Entity[str] = EntityJson(
  type='PostText',
  path='$.post.text',
  coerce_type=True
)
```

**`rules/post_contains_hello.sml`** imports both models, defines the rule, and wires it to effects—the whole loop in one file:

```python
Import(
  rules=[
    'models/base.sml',
    'models/post.sml',
  ]
)

ContainsHello = Rule(
  when_all=[
    EventType == 'create_post',
    TextContains(text=PostText, phrase='hello')
  ],
  description='Post contains the word "hello"',
)

WhenRules(
  rules_any=[ContainsHello],
  then=[
    BanUser(entity=UserId, comment='User said "hello"'),
    LabelAdd(entity=UserId, label='meow'),
  ],
)
```

`TextContains` and `BanUser` aren't stdlib—they're custom UDFs from [`example_plugins/`](https://github.com/roostorg/osprey/tree/main/example_plugins), which is the reference for [writing your own](../integration/integrations.md#writing-udfs). `LabelAdd` is stdlib.

**`config/labels.yaml`** declares the label the rule applies: which entity types it's valid for, and its connotation.

```yaml
labels:
  meow:
    valid_for: [User]
    connotation: positive
    description: testing label
```

[Ruleset Configuration](configuration.md) explains how Osprey loads and validates YAML alongside SML, and what happens when either changes.

**What you'll see in the UI** once events flow: each processed post appears in the Event Stream with `UserId`, `EventType`, `PostText`, and `ContainsHello` among its extracted features; querying `ContainsHello == True` filters to the posts that matched; and clicking through to a matched post's author shows the `meow` label on their `User` entity, with the `BanUser` effect recorded on the event. [Getting Started](../development/) walks through this UI tour on live demo data.

## Labels as state

Rules can't see previous events directly, but labels persist on entities across events—so a label added by one rule becomes a condition another rule checks later. Say you've flagged users who send too many DMs:

```python
WhenRules(
    rules_any=[
        Sent_Too_Many_DMs,
    ],
    then=[
        LabelAdd(entity=UserId, label='likely_spammer')
    ],
)
```

From then on, _every_ event by that user carries the state, and other rules can act on it—here, on a different event type entirely:

```python
Should_Warn_User_Of_Spammer = Rule(
    when_all=[
        HasLabel(entity=UserId, label='likely_spammer'),
        This_Is_A_New_DM,
    ],
    description=f'Likely spammer {UserId} started a new DM',
)
```

Labels also show on the entity in the UI, where they can be added and removed by hand. One asymmetry to know about: `HasLabel()` works in rules but not in the query bar, because queries search events rather than current entity state. To find events where a label was applied, query `DidAddLabel(entity_type="User", label_name="likely_spammer")` instead; see [Query Syntax](../user/investigate/query-syntax.md) for details.

## A multi-signal rule, organized across files

The following is a complete walkthrough of writing a rule using the project structure described in [Writing Rules § Rule Structuring](./#rule-structuring). The goal is to flag accounts whose first post mentions at least one user and includes a link—three signals that are individually innocent but suspicious together.

### Writing the rule

We'll create `rules/record/post/first_post_link.sml` for the rule logic. This file defines both the conditions that cause the rule to evaluate to `True` and the actions to take when it does.

```python
# First, import the models that you will need inside of this rule
Import(
    rules=[
        'models/base.sml',
        'models/record/post.sml',
    ],
)

# Next, define a variable that uses the `Rule` UDF
FirstPostLinkRule = Rule(
    # Set the conditions in which this rule will be `True`
    when_all=[
        PostCount == 1, # if this is the user's first post
        EmbedLink != None, # if there is a link inside of the post
        ListLength(list=MentionIds) >= 1, # if there is at least one mention in the post
    ],
    description='First post for user includes a link embed',
)

# Finally, set which effect UDFs will be triggered
WhenRules(
    rules_any=[FirstPostLinkRule],
    then=[
        # This is a custom effect UDF that we have implemented
        ReportRecord(
            entity=PostId,
            comment='This was the first post by a user and included a link',
            severity=3,
        ),
    ],
)
```

### Wiring up the rule

We want this rule to run _only_ when the event is a post event. Using the project structure described above, this involves three files.

First, `main.sml` at the project root includes a single `Require` statement pointing to the top-level rules index:

```python
Require(
    rule='rules/index.sml',
)
```

Next, `rules/index.sml` conditionally requires the post rules when the event type matches:

```python
Import(
    rules=[
        'models/base.sml',
    ],
)

Require(
    rule='rules/record/post/index.sml',
    require_if=EventType == 'userPost',
)
```

Finally, `rules/record/post/index.sml` requires the new rule:

```python
Import(
    rules=[
        'models/base.sml',
        'models/record/post.sml',
    ],
)

Require(
    rule='rules/record/post/first_post_link.sml',
)
```

---

From here, try modifying the demo ruleset while it's running—add a phrase to `TextContains`, or a second rule over `PostText`—or head to [Integrations & Plugins](../integration/integrations.md) when a pattern needs a UDF that doesn't exist yet.
