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
/outputsCommon guide pages:
- Files and artifacts
- Tools and safety
- Sources and mounts
- Versions and transactions
- Watching changes
- Storage and limits
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
grepWorkspaceexists, 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; // undoResearchWorkspaceFileUse 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.