Cremind
Concepts & Architecture

Event-Driven Architecture

How Cremind reacts to external changes in sub-second time using a relay WebSocket, a markdown event log, and a filesystem watcher — instead of polling.

Most assistants only act when you type. Cremind can also act when the world changes. A new email arrives, a calendar event moves, a ticket is updated — and the agent runs on its own, immediately, with that change as if you'd just described it. The whole pipeline is designed for sub-second latency, and it does this without polling, cron jobs, or heartbeat loops.

This page traces an event from the outside world all the way to a response on your screen. The pieces live in the app/events package: EventManager, FileWatcherManager, and runner.py.

The problem with polling

The naive way to "watch" an external service is to ask it over and over: poll an inbox every 30 seconds, run a cron job every minute, ping a heartbeat. Polling is a trade-off you always lose — poll often and you waste requests and hit rate limits; poll rarely and your assistant is stale. None of it is truly event-driven; it's just guessing on a timer.

Cremind takes a different path: it waits to be told.

The pipeline

External service
      │  (change happens)

Cremind Connect relay ──── content-free "resync" nudge ────►  Skill listener
                                                                    │  (holds a long-lived WebSocket)
                                                                    │  pulls the delta with its own local token

                                              events/<event_type>/<new>.md   (markdown file written)

                                                          filesystem watchdog detects new file

                                                      fan-out to every subscribed conversation

                                                          runner.py runs the agent
                                                   (event as a synthetic user message)

                                                              SSE stream ──► your clients

1. A long-lived WebSocket, not a poll loop

A skill's listener holds a long-lived WebSocket connection to the Cremind Connect relay. There's no polling — the connection sits open and idle until something happens.

2. A content-free "resync" nudge

When the external service changes, the relay sends the listener a small, content-free "resync" nudge. It deliberately carries no data — just "something changed, go look." This keeps the relay simple and keeps your data flowing through your own credentials rather than the relay.

3. The listener pulls the delta

On receiving the nudge, the listener pulls the actual change — the delta — directly from the source, using its own local token. It then writes the result as a markdown file into the skill's events/<event_type>/ folder.

4. A filesystem watcher fans it out

A filesystem watchdog notices the new markdown file the instant it lands. It fans the event out to every conversation subscribed to that event type, so multiple conversations can react to the same change.

5. The agent runs immediately

runner.py runs the agent immediately, injecting the event as a synthetic user message — as though you had just pasted the change in yourself. The agent reasons over it and acts using its normal tool plane. The output streams to your clients over SSE, so you watch the response unfold in real time.

Why content-free nudges?

The relay never needs to see your data. It only says "resync"; the listener does the fetching with its own token. That keeps the sensitive content on your machine and the relay's job dead simple — which is also what makes the round trip fast.

Why markdown files on disk?

Writing each event to a file in events/<event_type>/ is a small choice with big benefits:

  • Durability — the event is on disk before the agent runs, so it survives a crash or restart.
  • Auditability — you can read exactly what the agent reacted to, after the fact.
  • Decoupling — the listener (which fetches) and the runner (which reasons) don't have to be in lockstep; the file is the hand-off point, and the watcher bridges them.

Polling vs. event-driven

Polling / cron / heartbeatCremind events
TriggerA timer firesAn actual change happens
FreshnessStale between pollsSub-second
CostRepeated requests, even when nothing changedOne nudge per real change
Data pathOften pulls everything each cyclePulls only the delta, with your token

What you need

The relay side is provided by Cremind Connect, the companion sub-product with its own site. It summarizes to: Connect is the hosted relay that turns external webhooks and change notifications into the resync nudges your listeners wait on. See connect.cremind.io for what Connect covers and how to set it up.

On this page