Crux
GuidesWorkspaces

Watching Changes

Subscribe to workspace changes with durable cursors and update app views outside the model loop.

Use watch() when an app needs to react to workspace file changes outside the model loop, such as refreshing a file tree, syncing generated outputs, or showing a live artifact panel.

watch() uses the Runtime Engine durable event log, so configure a runtime first:

import { config } from "@use-crux/core";
import { node } from "@use-crux/core/runtime";

config({ runtime: node() });

const handle = ws.watch("/outputs", {
  recursive: true,
  pollIntervalMs: 250,
});

const off = handle.on((event) => {
  if (event.type === "rename") {
    console.log(event.from, "->", event.path);
    return;
  }

  console.log(event.type, event.path);
});

off();
handle.stop();

Events have type, workspaceId, namespace, path, at, and cursor. Rename events also include from:

type WorkspaceChangeEvent =
  | {
      type: "create" | "update" | "delete";
      workspaceId: string;
      namespace: string;
      path: string;
      at: number;
      cursor: string;
    }
  | {
      type: "rename";
      workspaceId: string;
      namespace: string;
      from: string;
      path: string;
      at: number;
      cursor: string;
    };

Scope

Omit the path to watch the whole workspace tree, or pass a specific path for exact-path matching. Set recursive: true to include descendants. Rename events match either the source path or destination path.

ws.watch(); // whole workspace
ws.watch("/outputs"); // exact path unless recursive is true
ws.watch("/outputs", { recursive: true }); // outputs tree

Resume With Cursors

Store handle.cursor after processing events and pass it back as { cursor } to resume after the last delivered change:

const handle = ws.watch("/outputs", {
  recursive: true,
  cursor: savedCursor,
});

handle.on(async (event) => {
  await updateFileTree(event);
  await saveCursor(handle.cursor);
});

Runtime Requirement

Without a configured runtime, direct workspace reads and writes still work, but watch() throws RUNTIME_REQUIRED because delivery depends on durable event cursors.

Pass onError to observe retryable namespace or event-read failures. The watch stays alive and continues polling with backoff:

const handle = ws.watch("/outputs", {
  recursive: true,
  onError: (error) => {
    console.warn("workspace watch retrying", error);
  },
});

Transaction staging writes are internal. Watchers see only committed live-namespace changes.

Devtools And Traces

Devtools include a Workspaces view that reconstructs a filesystem-like tree from workspace operations:

research
|- /workspace
|  `- notes.md
`- /outputs
   `- report.pdf

OTel receives privacy-safe operation metadata only. Raw file contents and full paths are not emitted to OTel. Workspace OTel spans use crux.workspace.operation and crux.workspace.path_hash; devtools use the same path hash as a stable file label when no local-only raw path is available. Finalized artifacts expose bounded metadata such as status, artifact kind, MIME type, size, and download ref.

The file inspector also has a Versions tab showing a file's revision timeline: version number, operation, timestamp, and trace link. Each version is announced by a single privacy-safe marker emitted when the version is recorded, so the timeline counts one entry per content change even though an edit or undo internally performs a nested write.

Project Index data-access facts preserve exact workspace operations, including grep, watch, history, diff, undo, artifacts, rename, move, copy, and finalize.

On this page