Event Listeners
How a skill's long-running listener subscribes to the Cremind Connect relay and drops external changes as markdown events.
Some skills don't just respond to your messages — they react to the outside world. A new email arrives, a calendar event changes, a Jira issue is updated, and the agent runs immediately. The piece that makes this possible is the skill's event listener: a long-running process declared in the SKILL.md, registered with Cremind, and connected to the Cremind Connect relay.
Cremind is version 0.0.1 and community-driven. The listener pattern described here is exactly how the built-in gmail, gcalendar, and jira skills work — read their SKILL.md for the concrete implementations.
Declaring the listener
A skill makes itself event-driven by declaring a long_running_app in its metadata:
metadata: {
events: {"event_type": [{"name": "new_email", "description": "A new email arrived in the INBOX"}]},
long_running_app: {
command: "uv run scripts/event_listener.py",
description: "Persistent listener. Subscribes to the relay and drops new messages as markdown."
}
}Two things matter here:
eventsdeclares the event types the skill can emit (each maps to anevents/<name>/folder).long_running_app.commandis the exact command line that launches the listener.
When a skill carrying a long_running_app first lands in a profile, Cremind pushes a high-priority "register required" notification (titled Set up <skill>) inviting you to start the background process. It fires once per skill, the first time it appears — reboots that merely re-validate an already-present skill do not re-fire it.
The listener flow
The listener is the long-lived bridge between an external system and your skill's drop-zone. The flow, using the built-in skills as the model:
- Subscribe. The listener connects a WebSocket to the Cremind Connect relay and proves it controls the account (the OAuth skills present a short-lived ID token).
- Arm the source. It registers the provider's change feed — Gmail
users.watch()into Pub/Sub, a Google Calendar watch channel, a Jira dynamic webhook — so the relay learns when something changes. - Receive a nudge. When a change occurs, the relay sends a content-free
resyncnudge over the WebSocket. The relay never carries your data — only the signal that something changed. - Pull the delta locally. On the nudge, the listener calls the provider's API from your machine with your local token to fetch only what changed (for example an incremental
history.list()). - Drop a markdown file. It writes each change as a markdown file into
events/<event_type>/, named like a timestamp plus a title (<YYYY-MM-DDTHH-MM-SS> <subject>.md). - Fan-out. From there the event flows into Cremind's event pipeline and reaches every conversation subscribed to that event type.
external system → relay (resync nudge) → listener → API pull (local token)
→ events/<event_type>/<file>.md
→ subscribed conversationsA useful consequence of the token-less relay: the same account linked in two Cremind apps receives events in both, because the relay fans the nudge out to every connected app for that account.
The drop-zone and the event markdown
Each event type the skill declares gets a folder under events/. The listener writes one markdown file per change there. The file is itself a small document: YAML frontmatter with the structured fields, then a plain-text body. The gmail skill, for example, writes:
---
id: "1923abc..."
from: "Alice <alice@example.com>"
to: "you@gmail.com"
subject: "Lunch?"
date: "Fri, 06 Jun 2026 09:00:00 +0000"
event_type: "new_email"
received_at: "2026-06-06T09:00:05+00:00"
---
<plain-text body>When a file lands in the drop-zone, it fires the event. This is also why testing is easy: writing a file into the folder yourself simulates a real event without the upstream trigger. See Event Subscriptions for the simulate command.
Listener behavior to design for
Real listeners have to survive restarts and downtime gracefully. The built-in skills establish conventions worth following:
- Baseline on first run. Record the current cursor (a
historyId, a sync token) and emit nothing for the backlog — you don't want a full-mailbox dump on day one. - Catch-up on startup. Sync anything that arrived while the listener was offline, bounded so it never replays an unbounded history.
- Renew the watch. Provider watches expire (Google's Gmail watch is capped at 7 days); re-arm well within the limit.
- Persist state. Keep the cursor in a gitignored state file (the built-ins use
scripts/.listener_state.json). - Shut down cleanly. Handle
SIGINT/SIGTERM.
Running the listener
You start a skill's listener as an autostart process so it relaunches at server boot:
cremind skill-events listener-start <skill>Check whether it's alive via its heartbeat:
cremind skill-events listener-status <skill>These commands, and how events route into conversations, are covered in Event Subscriptions.
A note on Cremind Connect
The relay that carries the resync nudges is Cremind Connect, a companion service that lets skills receive real-time events without each user standing up their own cloud project. The OAuth skills authorize through it, but tokens are exchanged and stored on your machine — the relay never sees them. Cremind Connect is its own sub-product with its own documentation.