Skip to content

Event Patterns

The Pattern class provides a composable algebra for matching events. Patterns are used everywhere in PyRapide: in @when handlers, connections, and constraints. They range from simple name matching to complex temporal and causal compositions.

Basic Patterns

The three foundational patterns match events by name, source, or payload content:

Pattern.match(name)

Matches events whose name equals or starts with the given string.

match.py python
from pyrapide import Pattern

# Exact match
p1 = Pattern.match("user.login")

# Prefix match (matches "api.request.get", "api.request.post", etc.)
p2 = Pattern.match("api.request")

Pattern.source(name)

Matches events from a specific source.

source.py python
# Only events from the worker process
p = Pattern.source("worker-1")

Pattern.payload(key, value)

Matches events where a specific payload key has a specific value.

payload.py python
# Events with status code 500
p = Pattern.payload("status_code", 500)

Composite Operators

Patterns compose through operators that build complex event specifications from simpler ones. Here is the full operator table:

OperatorSyntaxMeaning
SequenceA >> BA must occur before B (causally ordered)
JoinA & BBoth A and B must occur (in any order)
IndependenceA | BA and B must occur independently (no causal link)
DisjunctionA.or_(B)Either A or B (or both) must match
GuardA.where(fn)A must match and the predicate function must return True
TimedA.timed(max_dt)A must match within the given time window
RepeatA.repeat(n)A must match exactly n times

Sequence (>>)

The sequence operator requires that the left pattern matches before the right pattern in causal order. This is the most common composition for modeling workflows.

sequence.py python
# Request must be followed by a response
workflow = Pattern.match("api.request") >> Pattern.match("api.response")

# Three-step pipeline
pipeline = (
    Pattern.match("ingest.start")
    >> Pattern.match("transform.complete")
    >> Pattern.match("load.complete")
)

Join (&)

The join operator requires that both patterns match, in any order. It models synchronization points where multiple prerequisites must be satisfied.

join.py python
# Both the database and cache must respond before proceeding
ready = Pattern.match("db.ready") & Pattern.match("cache.ready")

# Use in a sequence: wait for both, then proceed
startup = ready >> Pattern.match("system.serve")

Independence (|)

The independence operator asserts that two events occurred without any causal relationship. This is a strong constraint that verifies genuine concurrency.

independence.py python
# These two events must be causally independent
concurrent = Pattern.match("sensor.a.reading") | Pattern.match("sensor.b.reading")
Important
Independence (|) is not the same as disjunction (.or_). Independence requires both events to exist but be causally unrelated. Disjunction requires at least one to match.

Disjunction (.or_)

The disjunction operator matches if either (or both) patterns match. Use it for branching logic.

disjunction.py python
# Match either a success or a failure
outcome = Pattern.match("task.success").or_(Pattern.match("task.failure"))

# Route based on whichever event arrives
handler = Pattern.match("request") >> outcome

Guard (.where)

The guard operator adds an arbitrary predicate that must return True for the pattern to match. This allows payload inspection, threshold checks, and custom logic.

guard.py python
# Only match high-priority errors
critical = Pattern.match("system.error").where(
    lambda e: e.payload.get("severity") == "critical"
)

# Only match requests with large payloads
large_request = Pattern.match("api.request").where(
    lambda e: len(e.payload.get("body", "")) > 10_000
)

Timed (.timed)

The timed modifier constrains matching to a time window. If the pattern does not match within the specified duration, it fails.

timed.py python
from datetime import timedelta

# Request must be followed by response within 5 seconds
sla = (
    Pattern.match("api.request")
    >> Pattern.match("api.response").timed(max_dt=timedelta(seconds=5))
)

Repeat (.repeat)

The repeat modifier requires the pattern to match a specific number of times. This is useful for detecting repeated events like retries or bursts.

repeat.py python
# Three consecutive failures
triple_fault = Pattern.match("system.error").repeat(3)

# Detect retry storms: 5 retries followed by a circuit-breaker trip
retry_storm = (
    Pattern.match("http.retry").repeat(5)
    >> Pattern.match("circuit_breaker.open")
)

Combining Operators

The real power of patterns emerges when you combine operators. Because every operator returns a new Pattern, compositions are fully nestable.

combined.py python
1# Complex monitoring pattern:
2# 1. User logs in
3# 2. Then, concurrently: profile loads AND preferences load
4# 3. Then dashboard renders within 2 seconds
5user_flow = (
6    Pattern.match("auth.login")
7    >> (Pattern.match("profile.loaded") & Pattern.match("prefs.loaded"))
8    >> Pattern.match("dashboard.render").timed(max_dt=timedelta(seconds=2))
9)
10
11# Alert pattern: 3 errors from any source, unless a recovery occurs
12alert = (
13    Pattern.match("error").repeat(3)
14    .where(lambda e: e.payload.get("recoverable") is False)
15)
💡 Tip
Build patterns incrementally. Start with a simple Pattern.match(), verify it works, then layer on guards, sequences, and temporal constraints. Each operator is independently testable.

Patterns become even more powerful when used inside Constraints for runtime monitoring and invariant checking.