Cremind
Agent Skills

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:

  • events declares the event types the skill can emit (each maps to an events/<name>/ folder).
  • long_running_app.command is 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:

  1. 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).
  2. 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.
  3. Receive a nudge. When a change occurs, the relay sends a content-free resync nudge over the WebSocket. The relay never carries your data — only the signal that something changed.
  4. 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()).
  5. 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).
  6. 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 conversations

A 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.

On this page