Crux
API Reference@use-crux/core

Workspaces

API reference for durable, path-addressed agent workspaces.

For workflow guidance, start with the Workspaces guide. This page is the API reference for imports, methods, option shapes, defaults, and behavior notes.

Import from @use-crux/core/workspace:

import {
  retrieverWorkspaceMountSource,
  workspace,
  workspaceToolNames,
} from "@use-crux/core/workspace";

import type { AssetStore } from "@use-crux/core/storage";

workspace()

const ws = workspace({
  id: "research",
  namespace: ({ input }) => `thread:${input.threadId}`,
  storage,
  retention: { ttlMs: 1000 * 60 * 60 * 24 },
  limits: {
    maxFileBytes: 1_000_000,
    maxNamespaceBytes: 25_000_000,
  },
});

Default mounts:

/workspace
/outputs

Common guide pages:

Methods

await ws.list("/workspace/**/*.md");
await ws.read("/workspace/notes.md");
await ws.write("/outputs/report.md", "# Report");
await ws.edit("/outputs/report.md", { find: "draft", replace: "final" });
await ws.exists("/outputs/report.md");
await ws.stat("/outputs/report.md");
await ws.append("/outputs/report.md", "\nAppendix");
await ws.rename("/outputs/report.md", "/outputs/final-report.md");
await ws.move("/outputs/final-report.md", "/workspace/final-report.md");
await ws.copy("/workspace/final-report.md", "/workspace/report-copy.md");
await ws.grep("revenue", { path: "/workspace/**/*.md", ignoreCase: true });
const handle = ws.watch("/outputs", { recursive: true });
await ws.history("/outputs/report.md");
await ws.read("/outputs/report.md", { version: 1 });
await ws.diff("/outputs/report.md", { from: 1, to: 2 });
await ws.undo("/outputs/report.md");
await ws.finalize("/workspace/final-report.md", { kind: "report" });
await ws.artifacts({ status: "final" });
await ws.transaction(async (tx) => {
  await tx.write("/outputs/report.md", "# Report");
  await tx.write("/outputs/data.csv", "name,value\nalpha,1\n");
});
await ws.delete("/outputs/report.md");

Every method accepts a { namespace } option. Use it for direct calls or manually created tools when the workspace namespace normally resolves from prompt input:

await ws.write("/workspace/notes.md", "# Notes", { namespace: "thread:123" });

const tools = ws.asTools({ namespace: "thread:123", prefix: "research" });

deleteWorkspaceFile and undoWorkspaceFile are not auto-injected unless tools.delete or tools.undo are enabled.

Prompt Injection

prompt({
  id: "agent",
  use: [ws],
  system: "Use the workspace for durable files.",
});

Manual control:

use: [ws.asContext()];
tools: ws.asTools({ prefix: "research" });

Injected tools include:

listWorkspace
readWorkspaceFile
writeWorkspaceFile
editWorkspaceFile
renameWorkspaceFile
grepWorkspace

exists, stat, append, copy, and move are programmatic methods by default. There is no separate moveWorkspaceFile; use renameWorkspaceFile for model-driven renames or moves.

Tool Names

const names = workspaceToolNames({ prefix: "research" });

names.list; // listResearchWorkspace
names.writeFile; // writeResearchWorkspaceFile
names.renameFile; // renameResearchWorkspaceFile
names.grep; // grepResearchWorkspace
names.undoFile; // undoResearchWorkspaceFile

Use this helper when matching workspace tools in middleware.

Watching Changes

watch() subscribes to durable workspace changes through the configured Runtime Engine event log:

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,
  onError: (watchError) => {
    console.warn("workspace watch retrying", watchError.error);
  },
});

Events are delivered as:

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;
    };

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.

watch() requires config({ runtime }). Direct workspace reads and writes still work without a runtime, but watch() throws RUNTIME_REQUIRED because delivery depends on durable event cursors. Store handle.cursor after processing events and pass it back as { cursor } to resume after the last delivered change.

Pass onError to observe retryable namespace or event-read failures. The watch stays alive and continues polling with backoff. Transactions emit only committed live-namespace changes; private staging writes are not delivered.

Transactions

const artifact = await ws.transaction(async (tx) => {
  await tx.write("/outputs/report.md", "# Report", { status: "draft" });
  await tx.write("/outputs/data.csv", "name,value\nalpha,1\n");

  return tx.finalize("/outputs/report.md", { kind: "report" });
});

transaction() stages changes in a private namespace, gives the callback read-your-own-writes behavior through tx, and commits only touched paths when the callback returns. If the callback throws, staged changes are discarded. If commit fails part-way through, Crux rolls back the touched live paths it changed.

The transaction-scoped surface is:

type WorkspaceTransaction = Pick<
  Workspace,
  | "list"
  | "read"
  | "write"
  | "edit"
  | "delete"
  | "exists"
  | "stat"
  | "append"
  | "rename"
  | "move"
  | "copy"
  | "grep"
  | "artifacts"
  | "finalize"
>;

Transactions are namespace-local and accept the same { namespace } override. Transaction mutations are local-workspace only. Source-backed mounts are rejected before their provider write() or delete() hooks run.

The implementation uses the generic RecordStore contract for local workspace mounts, so it works with in-memory stores, Convex record stores, Upstash Redis record stores, and custom conforming stores. Crash-proof multi-key durability depends on the backing store/runtime; transaction() is not a distributed transaction across stores.

Source-Backed Mounts

WorkspaceMount.source turns a mount into a provider-backed virtual root.

type WorkspaceMountSource =
  | {
      kind: "retriever";
      retriever: Retriever;
      query?:
        | string
        | ((
            input: WorkspaceRetrieverMountQueryInput,
          ) => string | Promise<string>);
      limit?: number;
      pathForHit?: (hit: RetrieverHit) => string;
      contentForHit?: (hit: RetrieverHit) => string;
      mimeType?: string | ((hit: RetrieverHit) => string);
    }
  | {
      kind: "custom";
      list(
        path: string,
        options?: WorkspaceMountListOptions,
      ): WorkspaceListResult | Promise<WorkspaceListResult>;
      read(
        path: string,
        options?: WorkspaceMountReadOptions,
      ): WorkspaceReadResult | null | Promise<WorkspaceReadResult | null>;
      grep?(
        query: string,
        options?: WorkspaceMountGrepOptions,
      ): WorkspaceGrepResult | Promise<WorkspaceGrepResult>;
      exists?(
        path: string,
        options?: WorkspaceMountPathOptions,
      ): boolean | Promise<boolean>;
      stat?(
        path: string,
        options?: WorkspaceMountPathOptions,
      ): WorkspaceFile | null | Promise<WorkspaceFile | null>;
      write?(
        path: string,
        content: WorkspaceContent,
        options?: WorkspaceMountWriteOptions,
      ):
        | WorkspaceFile
        | WorkspaceReadResult
        | null
        | void
        | Promise<WorkspaceFile | WorkspaceReadResult | null | void>;
      delete?(
        path: string,
        options?: WorkspaceMountPathOptions,
      ): void | Promise<void>;
    };

Retriever source:

const ws = workspace({
  id: "research",
  namespace,
  storage,
  mounts: [
    { path: "/workspace", access: "readwrite" },
    { path: "/outputs", access: "readwrite" },
    {
      path: "/sources",
      access: "read",
      source: {
        kind: "retriever",
        retriever: docs,
        query: "current task source documents",
      },
    },
  ],
});

Adapter helper:

source: retrieverWorkspaceMountSource(docs, {
  query: "current task source documents",
  pathForHit: (hit) => `${hit.source.id}/${hit.chunkId}.md`,
});

For source-backed paths, list, read, grep, exists, and stat delegate to the provider. If a custom source omits grep, Crux falls back to list + read; if it omits stat, Crux derives file metadata from read.

Mutation operations are provider opt-ins. Retriever sources and custom sources without hooks are read-only. write, edit, append, and provider-destination copy require both access: "readwrite" on the mount and a custom source write() hook. delete requires access: "readwrite" and delete(). edit and append read the provider text first, then call write() with the updated content.

Version history, undo, diff, finalize, and artifact manifests remain local workspace features. copy() can materialize readable virtual text and JSON files into writable local mounts. Binary provider resources need a custom app-side copy flow until the provider exposes readable bytes.

Artifacts

Artifacts are file records with lifecycle metadata. They are not stored in a separate table.

await ws.write("/outputs/report.md", "# Report", {
  status: "draft",
  kind: "report",
});

const artifact = await ws.finalize("/outputs/report.md");
const finals = await ws.artifacts({ status: "final" });

finalize() pins the current version as the published artifact (artifact.version). Editing the file afterward creates new draft versions, but artifacts() and the manifest keep surfacing the pinned revision until you finalize() again.

Small inline artifacts expose a workspace-inline://... reference. Asset-backed artifacts expose the asset URI.

Asset Stores

Use AssetStore for binary and oversized workspace files. Asset-backed text and JSON read back as text or json; binary files return a URI.

Custom stores implement:

interface AssetStore {
  put(asset: Asset, options?: AssetPutOptions): Promise<StoredAsset>;
  get(ref: AssetRef): Promise<StoredAsset>;
  delete(ref: AssetRef): Promise<void>;
}

Use @use-crux/convex for Convex storage:

import { storage } from "@use-crux/core/storage";
import { convexAssetStore } from "@use-crux/convex";

const ws = workspace({
  id: "research",
  namespace: threadId,
  storage: storage({
    records,
    assets: convexAssetStore({ ctx }),
  }),
});

See Storage for the difference between RecordStore, VectorStore, and AssetStore, and AssetStore for custom S3/R2/GCS-style asset stores.

Versioning And History

Every content change (write, edit, append, undo) appends an immutable version. History is always recorded. diff defaults to the most recent change and applies to text files; binary files throw. undo restores the previous content as a new version.

Versioning is on by default with unlimited retention. Bound it per file with versioning.maxVersions, which garbage collects the oldest snapshots and their assets:

workspace({
  id: "research",
  namespace,
  storage,
  versioning: { maxVersions: 20 },
});

Quota (limits.maxNamespaceBytes) counts live files only, not historical snapshots.

Retention And Limits

workspace({
  id: "research",
  namespace,
  storage,
  retention: { ttlMs: 86_400_000 },
  limits: {
    maxFileBytes: 1_000_000,
    maxNamespaceBytes: 25_000_000,
  },
});

retention.ttlMs is passed to RecordStore.put(..., { ttlMs }) only when the store supports TTL. maxFileBytes rejects a single oversized write, and maxNamespaceBytes rejects writes that would push the namespace over its cap.

Observability and Project Index

Workspace operations emit workspace.operation spans with privacy-safe OTel attributes, including crux.workspace.operation and crux.workspace.path_hash. Raw paths and file contents are not exported to OTel. Local devtools can show artifact metadata and use a stable hash:<pathHash> file label when no local-only raw path is available.

Each recorded version emits one privacy-safe version marker with path hash, version number, and operation. The devtools file inspector reconstructs version history from those markers.

Project Index workspace metadata includes mounts, generated tool names, asset-storage posture, retention.ttlMs, and quota limits. Workspace method calls inside indexed owners are represented through workspace read/write relations, and workspace-specific data-access facts preserve exact operations such as grep, watch, history, diff, undo, artifacts, rename, move, copy, and finalize.

On this page