This guide is for agents (and the skill loaders that initialize them). It covers everything needed to discover, read, write, and safely writeback through the synced mirror.
Context budget warning: This guide is self-contained. Low-context agents should read it top-to-bottom without skipping sections. Every section is load-bearing.
The Relayfile VFS is a synced mirror of provider APIs (GitHub, Notion, Linear, Slack) materialized as ordinary files on disk. An agent reads and writes those files normally. The sync daemon handles authentication, webhooks, and writeback to the provider.
Key facts:
- Files are ordinary local files. Use file tools, not provider APIs.
- The mirror lags the source of truth by up to 30 s (default poll interval).
- Writes are eventually propagated. A write is not committed to the provider until the sync daemon acknowledges it.
mtimeis local write time, not source-of-truth event time. Do not sort bymtime.- The
.relay/directory is reserved. Do not write to it.
Find the local mirror directory in this priority order:
echo $RELAYFILE_LOCAL_DIRIf set, use that path. Done.
relayfile status --json | jq -r '.localDir'For multi-workspace setups, the default workspace localDir is returned. To target a specific workspace:
relayfile status my-project --json | jq -r '.localDir'Search parent directories for the first one containing .relay/state.json. That directory is the mount root.
If none of the above resolves, fail with a clear message and stop:
relayfile mount not found. Run 'relayfile setup' or set RELAYFILE_LOCAL_DIR.
Do not guess paths. Do not proceed without a confirmed mount root.
Before any non-trivial read or any write, read .relay/state.json:
cat <mount>/.relay/state.jsonExample:
{
"workspaceId": "ws_abc",
"mode": "poll",
"lastReconcileAt": "2026-05-02T18:34:11Z",
"lastEventAt": "2026-05-02T18:34:08Z",
"intervalMs": 30000,
"providers": [
{ "provider": "notion", "status": "ready", "lagSeconds": 4,
"deadLetteredOps": 0, "lastError": null }
],
"pendingWriteback": 0,
"pendingConflicts": 0,
"deniedPaths": 0
}Rules:
- If the provider you need has
status != "ready", wait or warn. Do not assume files are current. - If
lagSeconds > 60, treat data as potentially stale. Note it in your response. - If
pendingConflicts > 0, check.relay/conflicts/before writing to affected paths. - Do not use
mtimefor ordering. UselastReconcileAtandlagSeconds.
<mount>/github/ # GitHub
<mount>/github/repos/<owner>/<repo>/pulls/<n>/
metadata.json # PR metadata (read-only)
files.json # changed files (read-only)
reviews/<timestamp>.json # posted reviews (read-only)
reviews/review.json # write here to post a review
<mount>/notion/ # Notion pages
<mount>/notion/pages/<page-id>/content.md # page body (check _PERMISSIONS.md)
<mount>/linear/ # Linear issues
<mount>/linear/issues/<issue-id>/metadata.json # issue metadata
<mount>/linear/issues/<issue-id>/comments/new.json # write to add a comment
<mount>/slack/ # Slack
<mount>/slack/channels/<channel-id>/messages/ # channel messages (read-only)
<mount>/slack/channels/<channel-id>/messages/new.json # write to send a message
Each provider publishes its own rules:
<mount>/<provider>/_PERMISSIONS.md
Always read _PERMISSIONS.md before writing to a provider subtree. It lists exactly which paths trigger writeback and the schema each writeback path expects. Writing to a read-only path will be denied by the Cloud and logged to .relay/permissions-denied.log.
<mount>/.relay/state.json # sync state (read-only for agents)
<mount>/.relay/conflicts/ # conflict staging area (read-only for agents)
<mount>/.relay/dead-letter/ # dead-lettered ops (read-only for agents)
<mount>/.relay/permissions-denied.log # denial log (read-only for agents)
<mount>/.relay/mount.pid # daemon PID
<mount>/.relay/mount.log # daemon log
Do not write to any path under .relay/. Do not delete or rename top-level provider directories (github/, notion/, etc.).
Use standard file read operations. No special API needed.
cat <mount>/github/repos/acme/api/pulls/42/metadata.jsonWhen to prefer file reads over provider APIs:
- Always. The VFS exists so agents never need provider credentials or API knowledge.
- Provider APIs require OAuth tokens the agent does not have.
- File reads are simpler, cacheable, and work offline from the last sync.
Staleness check: If freshness matters, verify lagSeconds in state.json for the relevant provider. If lag is acceptable, proceed. If not, run relayfile pull <path> to force a refresh before reading.
Writes trigger writeback to the provider. Follow this exact sequence:
cat <mount>/github/repos/acme/api/pulls/42/reviews/review.jsonCapture the current content. Note the path for later.
Verify the target path is a writeback path, not read-only. Check the required schema.
cat <mount>/github/_PERMISSIONS.mdFor JSON writeback paths, validate locally before saving. Example for a GitHub review:
{
"body": "Looks good to me.",
"event": "APPROVE"
}event must be one of APPROVE, REQUEST_CHANGES, COMMENT. Writing an invalid schema causes a schema validation error: the Cloud rejects the write, moves the file to .relay/conflicts/<path>.invalid.<ts>, and restores the previous version. Fix and retry from that conflict file.
# Write the file
cat > <mount>/github/repos/acme/api/pulls/42/reviews/review.json << 'EOF'
{"body": "Looks good to me.", "event": "APPROVE"}
EOFThe mount's file watcher picks up the change within 100 ms and queues the upload.
Poll .relay/state.json until pendingWriteback == 0 or the file appears in .relay/conflicts/ or .relay/dead-letter/.
# Simple poll loop (30 s timeout)
timeout=30
elapsed=0
while [ $elapsed -lt $timeout ]; do
pending=$(jq '.pendingWriteback' <mount>/.relay/state.json)
conflicts=$(ls <mount>/.relay/conflicts/ 2>/dev/null | grep -c "review.json" || true)
[ "$pending" -eq 0 ] && [ "$conflicts" -eq 0 ] && echo "ack" && break
sleep 2
elapsed=$((elapsed + 2))
doneThe SDK provides waitForWriteback(path, { timeoutMs }) for TypeScript callers.
If a conflict appears:
- Read both versions: the current path (remote) and
.relay/conflicts/<path>.<rev>.local(your edit). - Merge the intent into the current file at the original path.
- Save again (step 4).
- Wait for ack again (step 5).
- Do not delete the conflict file yourself — the system moves it to
resolved/automatically after a successful write.
A write is not confirmed until:
pendingWritebackreturns to 0, AND- No new entry for the path appears under
.relay/conflicts/or.relay/dead-letter/
A write that lands in .relay/dead-letter/ has permanently failed. Inspect the dead-letter JSON:
cat <mount>/.relay/dead-letter/<opId>.jsonFields include: path, code, message, attempts, replayUrl. Fix the underlying issue (schema, permissions, provider auth) then replay:
relayfile ops replay <opId>| Provider status | Meaning for agents |
|---|---|
pending |
Provider just connected; no files yet. Wait. |
cataloging |
Listing remote objects. Files will appear soon. Wait. |
syncing |
Files populating. Partial data. Read with caution. |
ready |
Fully synced. Safe to read and write. |
error |
Last sync attempt failed. Check lastError. Reads may be stale. |
degraded |
Sync lag > 10 min. Reads serve stale data. Note in response. |
When a provider is cataloging or syncing, do not delete local files under that subtree. The mount will not delete them either — it waits for ready before applying remote-delete semantics.
When two agents share a mount:
- Treat
.relay/conflicts/as a coordination signal. If you see a conflict on a path another agent owns, back off. Do not overwrite it. - Prefer disjoint paths. Agents working on different files never conflict. Structure parallel work to use separate paths.
- Use agent-relay channels for handoffs when two agents need to coordinate on a shared file. One agent can post "done editing
github/.../review.json" before the next one picks it up. - Do not use file locking. The VFS does not support locks across processes. Coordinate through channels or by convention.
Paste this into a low-context agent's initial context if the full guide is too large:
RELAYFILE VFS QUICK REFERENCE
Mount discovery (in order):
1. $RELAYFILE_LOCAL_DIR
2. relayfile status --json | jq -r '.localDir'
3. Walk upward from CWD for .relay/state.json
Before reading or writing:
- Read .relay/state.json
- provider.status must be "ready"
- provider.lagSeconds should be < 60
Path rules:
- Files are at <mount>/<provider>/...
- Read _PERMISSIONS.md before writing to any path
- Never write to .relay/
- Never delete top-level provider dirs
Safe write sequence:
1. Read current file
2. Check _PERMISSIONS.md for schema
3. Validate JSON payload locally
4. Write file
5. Poll state.json until pendingWriteback == 0
6. If .relay/conflicts/ contains your path: merge and retry
7. If .relay/dead-letter/ contains your path: inspect and replay
Do NOT:
- Trust mtime for ordering (use lastReconcileAt)
- Call provider APIs directly (use files)
- Write to .relay/
- Ignore lagSeconds > 60
The @relayfile/sdk WorkspaceHandle exposes these helpers:
import { WorkspaceHandle } from "@relayfile/sdk";
const handle = await WorkspaceHandle.connect({ workspaceId: "ws_abc" });
// Read sync state
const state = await handle.readState();
// { lastReconcileAt, providers: [{ provider, status, lagSeconds }], ... }
// Wait for a provider to be ready
await handle.enforceProviderReady("notion", { timeoutMs: 60_000 });
// Write a file and wait for writeback ack
await handle.writeFile("github/repos/acme/api/pulls/42/reviews/review.json",
JSON.stringify({ body: "LGTM!", event: "APPROVE" })
);
await handle.waitForWriteback("github/repos/acme/api/pulls/42/reviews/review.json",
{ timeoutMs: 30_000 }
);
// Refresh the workspace token (called automatically after 55 min)
await handle.refreshToken();| Symptom | Likely cause | Fix |
|---|---|---|
File missing from <mount>/<provider>/ |
Provider still syncing | Check state.json status; wait for ready |
Write moves to .relay/conflicts/*.invalid.* |
Schema validation failed | Read _PERMISSIONS.md, fix payload, retry |
Write moves to .relay/conflicts/*.local |
Concurrent edit conflict | Merge remote + local, re-save |
Write moves to .relay/dead-letter/ |
Permanent API error | Inspect JSON, fix root cause, relayfile ops replay <id> |
lagSeconds > 60 |
Webhook unhealthy or network lag | Note staleness in response; run relayfile pull if needed |
status == "error" |
Provider auth or API failure | Run relayfile integration list and relayfile integration connect <provider> |
| Permission denial in log | Path is read-only | Read _PERMISSIONS.md for writable paths |