From osprey-rules
Use when writing SML rules for Osprey — syntax questions, type system, naming conventions, labeling patterns, entity extraction, window counting, or label operations
How this skill is triggered — by the user, by Claude, or both
Slash command
/osprey-rules:osprey-sml-referenceThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
SML is a Python-like language for defining moderation rules in Osprey. Rules extract data from events, evaluate conditions, and emit effects (labels, verdicts, list actions).
SML is a Python-like language for defining moderation rules in Osprey. Rules extract data from events, evaluate conditions, and emit effects (labels, verdicts, list actions).
Four fundamental types make up SML:
EntityJson extracts entity IDs from event JSON; Entity constructs them from known values. Labels attach to entities, not raw strings.
# CORRECT — UserId is an Entity; labels attach to it
UserId: Entity[str] = EntityJson(type='UserId', path='$.did', required=False)
# WRONG — JsonData produces str, not Entity[str]. Labels can't attach to str.
UserId: str = JsonData(path='$.did')
EntityJson(type, path, required) → Entity[str] or Entity[int]. Use for IDs: UserId, Handle, AtUri, PdsHost.Entity(type, id) → construct entity from known values (e.g., computed AT-URI).JsonData extracts primitive values from event JSON.
DisplayName: str = JsonData(path='$.eventMetadata.profile.displayName', required=False, coerce_type=True)
PostsCount: int = JsonData(path='$.eventMetadata.profile.postsCount', required=False)
Returns: str, int, float, bool, Optional[T], or List[T].
Use ResolveOptional to unwrap optional values with a default.
AccountAgeSeconds: Optional[int] = JsonData(path='$.eventMetadata.accountAge', required=False)
AccountAgeSecondsUnwrapped: int = ResolveOptional(optional_value=AccountAgeSeconds, default_value=999999999)
Rule(when_all=[...], description=f'...') # → RuleT
Combines multiple conditions with AND logic. All items in when_all must be the same type (all bool or all RuleT).
NewAccountSpam = Rule(
when_all=[
AccountAgeSeconds < Day,
PostsCount > 50,
FollowersCount < 5,
],
description=f'New account spam (age={AccountAgeSeconds}s, posts={PostsCount})',
)
WhenRules(rules_any=[...], then=[...]) # → None
Triggers effects when ANY rule passes. Always use rules_any=, never rules_all=.
WhenRules(
rules_any=[NewAccountSpam, BotBehavior],
then=[
LabelAdd(entity=UserId, label='spam'),
AtprotoLabel(entity=UserId, label='spam', comment='Auto-detected', expiration_in_hours=168),
],
)
Import(rules=['path/to/file.sml']) # Load models/rules from other files
Require(rule='path/to/file.sml', require_if=condition) # Conditional inclusion
All items in when_all must be the same type:
RegexMatch(...), comparisons (X < Y), or/and on bools → boolRule(...) → RuleT; RuleT or RuleT → RuleTor (A or B or C), NOT function-call or(A, B, C)not works on both bool and RuleT# CORRECT — all bool
WhenRules(
rules_any=[
Rule(when_all=[PostText != '', RegexMatch(pattern=r'...', target=PostText)]),
],
then=[...],
)
# WRONG — mixing RuleT and bool in when_all
Rule(when_all=[MyRule, SomeCondition == True]) # Can't mix Rule and bool
Apply effects when rules pass:
LabelAdd(entity, label, apply_if?, expires_after?, delay_action_by?) — add labelLabelRemove(entity, label, ...) — remove labelAtprotoLabel(entity, label, comment, expiration_in_hours) — emit to Bluesky OzoneDeclareVerdict(verdict) — for synchronous callersWhenRules(
rules_any=[SomeRule],
then=[
LabelAdd(entity=UserId, label='flagged', expires_after=TimeDelta(days=7)),
LabelAdd(entity=AtUri, label='violation'),
],
)
| UDF | Parameters | Returns | Purpose |
|---|---|---|---|
RegexMatch | pattern, target, case_insensitive? | bool | Regex test |
IncrementWindow | key, window_seconds, when_all | int | Sliding window counter |
GetWindowCount | key, window_seconds, when_all | int | Read counter without incrementing |
ListContains | list, phrases, case_sensitive?, word_boundaries? | Optional[str] | Match against YAML word list |
CensorizedListContains | list, phrases, plurals?, must_be_censorized? | Optional[str] | Match lookalike/obfuscated text |
HasLabel | entity, label, manual?, status?, min_label_age? | bool | Check if entity has label |
HasAtprotoLabel | entity, label | bool | Check AT Protocol label |
TimeDelta | weeks?, days?, hours?, minutes?, seconds? | TimeDeltaT | Create a duration |
AnalyzeToxicity | text, when_all | Optional[float] | ML toxicity score |
AnalyzeSentiment | text, when_all | Optional[float] | ML sentiment polarity |
CacheSetStr | key, value, when_all, ttl_seconds? | None | Store string in Redis |
CacheGetStr | key, when_all, default? | str | Read string from Redis |
For detailed patterns and implementation examples, see:
references/labeling-patterns.md. Covers all common use cases: content matching, rate limiting, strike systems, ML scoring, cross-entity labeling, caching, and more.references/sml-conventions.md. Variable naming, time constants, RegexMatch rules, IncrementWindow keys, type system pitfalls, and what NOT to do. Also includes a Reviewer Checklist section with structured CONV-prefixed check IDs for systematic convention review.Provides UI/UX resources: 50+ styles, color palettes, font pairings, guidelines, charts for web/mobile across React, Next.js, Vue, Svelte, Tailwind, React Native, Flutter. Aids planning, building, reviewing interfaces.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
npx claudepluginhub skywatch-bsky/claude-skills --plugin osprey-rules