A raw webhook is a diff without context, and every provider fires one differently: different payload shapes, different signature schemes, different retry semantics. Relayfile collapses all of that into a single event model. A provider webhook becomes a normalized file event pointing at a canonical path — the same shape whether it came from Linear, GitHub, Notion, or Slack.
The pipeline
provider webhook
→ provider layer verifies + receives (Nango / Composio / Pipedream)
→ adapter normalizes payload → canonical path
→ core server materializes the file, revision++
→ normalized file event emitted to subscribersThe load-bearing word is materializes. Relayfile doesn't forward the webhook — it applies it. By the time the event reaches your agent, the file at path already holds current state, and the surrounding context (by-state/, related records, the provider's LAYOUT.md) is already on disk. The event creates urgency; the materialized tree is what makes acting on it possible without an API call. See Adapters and providers for how normalization is implemented per integration.
The normalized event
Every change — webhook, sync, or another agent's write — surfaces as the same object:
{
"eventId": "evt_01HQ8K7M2YV3R0XW9F4ZB6T2QA",
"type": "file.updated",
"path": "/linear/issues/AGE-16__87389837-62b1-4e1a-a237-59218bab2974.json",
"revision": "rev_42",
"provider": "linear",
"origin": "provider_sync",
"timestamp": "2026-05-13T14:32:01Z"
}typeis one offile.created,file.updated,file.deleted.pathis the canonical file that changed. The path itself carries context — you know which provider and which record without parsing a payload.revisionmonotonically increases per file. Use it to order events and to fetch the prior state for a diff.origindistinguishesprovider_sync(a webhook or sync from the provider),agent_write(another agent wrote the file), andapi(a direct API write). This is how an agent avoids reacting to its own writes.
Consuming events
Filter by path glob and event type so an agent only wakes for what it cares about. From the CLI:
relayfile listen \
--path "/linear/issues/by-state/triage/**" \
--event file.created \
--run "claude --print 'Triage this: {{path}}'"Or from the SDK with onWrite, which subscribes over the same WebSocket stream and dispatches by pattern:
import { onWrite } from '@relayfile/sdk';
onWrite('/linear/issues/**', async (event) => {
if (event.source === 'agent') return; // ignore our own writes
await agent.handle(event);
}, { client, workspaceId, operations: ['create', 'update'] });See Agents for the framework helpers built on this.
Delivery semantics
- At-least-once. Events can repeat. Deduplicate on
eventId; treat handlers as idempotent. - Ordering. Per file,
revisionis the source of truth — wall-clocktimestampcan be close together under bursty traffic. - Catch-up. A subscriber that connects with a cursor receives the events it missed while disconnected, so a restart doesn't drop changes. If the WebSocket can't open, the SDK degrades to HTTP polling rather than going silent.