Where tflo fits

tflo is an embeddable temporal event-processing engine. It runs alone where JVM CEP engines can't reach, and it composes with the engines you already operate where they do. The framing is win-win composition, not substitution.

This page also serves as an example application of the tflo engine via its WASM binding — specifically the React integration built on top of it. That integration is an example application, not the product; the product is the engine. See the bindings docs for the full WASM / JS surface, and the examples index for all available example applications (including the framework-agnostic browser-events example).

Declare it, don't manage it.

No useEffect, no useRef, no timers to wire up and remember to clear. You write a pattern — when A, followed by B, unless C within T — and the engine owns the rest. Your React component stays a pure function of what the engine tells it.

Stateless components, stateful (replayable) engine.

Your components hold no temporal state — not "did A arrive before B within 5 s?" The engine owns that, in an explicit, typed projection. And because the engine is deterministic and event-driven, that state is always just a replay away: pipe the same event sequence through and you get exactly the same signals back.

Eleven proven scenarios — real, runnable, green

Each scenario below is a live demo in tflo-react/validation/scenario-*. React, Signals, RxJS, and XState can each reach the same output; the difference is that they get there only by hand-writing the event-time logic themselves. tflo expresses it once, declaratively. All eleven are self-asserting — every variant (tflo, React, Signals, RxJS, XState) passes the same expectations.

Abandoned flow

Requirement: A started, not completed within 5 s of event-time.

// Abandoned flow — A started, not completed within 5 s of event-time.
// React / Signals / RxJS / XState reach the same output only by
// hand-writing the event-time logic themselves.
Pattern::new("abandoned_flow")
  .when(|e| e.kind == "flow_started")
  .not_then(|e| e.kind == "flow_completed")
  .within(Duration::from_millis(5_000))

Temporal join

Requirement: join payment to order within a 3 s event-time window.

// Temporal join — payment lands in the engine;
// if an order with the same key arrived in the last 3 s (event-time), emit.
// RxJS can zip two streams, but event-time key-alignment is hand-rolled.
window_join_keyed(
  payment_stream,
  order_stream,
  |p, o| p.order_id == o.id,
  Duration::from_secs(3),
)

Out-of-order windows

Requirement: a late event still lands in its correct event-time window.

// Out-of-order windows — late event still lands in its correct bucket.
// XState tracks "which state am I in," not "which event-time bucket owns this."
tumbling_window(
  stream,
  Duration::from_secs(60),
  AllowedLateness::from_millis(2_000), // late events re-open the correct bucket
)

Scenarios 4–11 — new, all green

Eight additional demos validate more advanced tflo shapes. Each lives in tflo-react/validation/scenario-{4..11}-* with all four framework variants plus the tflo reference. One-liner requirement and the tflo shape used:

Scenario Requirement (one-liner) tflo shape
4 — Rage click ≥5 clicks within 2 s (repeat-window) .when().then().times().within()
5 — Card-testing 2 payment fails with no success between (interior negation) .when().notBetween().then().within()
6 — Hesitation ≥3 field edits within 5 s of focus (repeat-window) .when().then().times().within()
7 — Beaconed analytics Out-of-order tumbling window over batched beacons tumbling_window + AllowedLateness
8 — Multi-device Keyed event-time join across devices window_join_keyed
9 — Undo grace Action committed unless undo arrives within 10 s (negation window) .when().notThen().within()
10 — Progressive disclosure Repeated fails with no success between (interior negation) .when().notBetween().then().within()
11 — SLA / friction No next step within 8 s of trigger (negation window) .when().notThen().within()

Tool boundaries — alternatives, not a ranking

Each tool has a native vocabulary. Using the right one means your code reads like the problem, not like a workaround:

Tool Native vocabulary
tflo "when X, followed by Y, unless Z within T" — event-time pattern detection across an ordered sequence of events.
RxJS "transform / combine / cancel async streams" — reactive operators over observables.
XState "the UI is in one of these states; events transition between them" — finite-state machines and statecharts.
React state / Signals "render the current value" — reactive rendering from a snapshot of application state.

These are alternatives and composable layers, not a head-to-head ranking. RxJS and XState are excellent at what they do; tflo fills the specific gap of declarative event-time temporal logic that none of them model natively. You can use tflo inside an RxJS pipeline or feed its signals into an XState machine.

What tflo is good at, standalone

tflo's primary story is the one no JVM CEP engine can tell. If your need matches one of these, tflo runs alone:

How tflo composes with Flink, Esper, and Kafka Streams

Where users already operate a mature CEP or streaming engine, tflo fills the layers the host can't reach. Three concrete patterns:

1. tflo at the edge, mature engine in the center

A common shape: tflo runs on sensor gateways and in browsers, emitting typed Signal events with already-conditioned data. Those signals flow into Flink, Esper, or Kafka Streams for cross-source aggregation, enrichment, and storage.

[sensor gateways]    [browser dashboards]    [embedded services]
       │                     │                       │
       └── tflo signal emission (typed Signal events) ──┐
                                                        │
                                            [Kafka / NATS / MQTT][Flink / Esper /
                                                  Kafka Streams]
                                                        │
                                                 [warehouse, alerts,
                                                  user-facing apps]

What tflo contributes that the host engine cannot: typed Absent, typed Signal, deployment in shapes the host can't reach (browsers, ARM gateways, embedded services).

2. tflo as the per-key detector inside a host engine

The host engine owns watermarks, exactly-once, scaling, savepoints. tflo runs inside a per-key processing slot (Flink KeyedProcessFunction, Beam DoFn, Kafka Streams Processor) and provides the signal logic plus typed-absence semantics that the host doesn't model natively.

                    [Flink job]
                        │
                ┌───────┴────────┐
                │  KeyedProcess  │
                │  Function      │  ← Flink owns watermarks,
                │                │     exactly-once, scaling
                │   ┌────────┐   │
                │   │ tflo   │   │  ← tflo owns signal logic,
                │   │ graph  │   │     typed Absent, detector
                │   └────────┘   │     composition
                └────────────────┘

The integration crates (tflo-flink, tflo-beam, tflo-kstreams) are designed and deferred until concrete user demand surfaces — see interop backlog.

3. tflo as backfill / replay over historical data

The same detectors that run in production also run against archived events. Iterator::tflo(...) works identically on Vec<Event> and Stream<Event>, so a regression test, a what-if simulation, or a historical re-emit uses the same code as live.

// Live (Tokio stream)
let signals: impl Stream<Item = Signal<_, _>> = live_stream.tflo(|t| { /* ... */ });

// Backfill (Vec) — same closure, same detectors, deterministic output
let signals: Vec<Signal<_, _>> = historical_events.into_iter()
    .tflo(|t| { /* ... */ })
    .collect();

Picking tflo

tflo is the right tool when

It probably isn't the right standalone tool when

In both cases, the composition patterns above usually apply: tflo can still emit typed signals into your existing stack, or run inside it.

Boundaries

tflo deliberately doesn't do everything. The full list, with reasoning, is on the non-goals page — short version: streaming SQL, a JobManager, exactly-once via 2PC, a global watermark, savepoints, and processing-time mode are out. Each is a deliberate trade for the embeddable / WASM / edge primary story. Pattern composition itself isn't a non-goal — the closure DSL, custom nodes, timers, and CEL / Rhai / Rego let you compose multi-event patterns from primitives today.

Designed-but-deferred integrations live in the interop backlog: tflo-flink, tflo-beam, and tflo-kstreams for hosting tflo inside a JVM streaming runtime. Promoted on concrete user demand.

Try it where the JVM can't reach

Quick Start runs on iterators and streams in five lines. The reference deployment glues MQTT, Kafka, Influx, and Parquet together.