Files & blobs (data plane)
Read and write files programmatically with the TypeScript SDK — the three-step write with compare-and-swap, streaming reads, directory operations, publishing durable artifacts, and material search.
Records are the coordination layer — small, immutable, content-addressed. Files are the data plane: arbitrary bytes (documents, images, datasets, build outputs) that live in a per-namespace virtual filesystem and are addressed by path. The TypeScript SDK exposes the whole data plane under client.data (available in @syncropel/sdk 0.8.0+).
import { Client, Identity } from "@syncropel/sdk";
const client = new Client({
endpoint: "https://your-instance.syncropel.app",
identity: Identity.static("did:example:my-app"),
apiKey: process.env.SYNCROPEL_TOKEN,
});
// Write a file, read it back.
const { contentHash } = await client.data.write({
path: "/files/notes.md",
data: "# Meeting notes\n\n- ship the data-plane guide",
contentType: "text/markdown",
});
const text = await client.data.readText("/files/notes.md");How writing differs from emitting
There's one important contract difference from the record methods (emit, query): the data plane surfaces errors instead of failing open. A record emit returns { success: false } on transport trouble so a UI never crashes; but a write that hits a quota, a conflict, or a malformed path throws — a dropped write must never look like a success. The data plane raises SyncropelDataError, which you catch and handle.
Writing a file
client.data.write performs the full three-step write — initiate, upload (chunked for large files), commit — in one call, and returns the stored content hash.
const result = await client.data.write({
path: "/files/report.pdf",
data: pdfBytes, // Uint8Array | ArrayBuffer | string
contentType: "application/pdf", // optional; inferred from the path otherwise
});
// result.contentHash — SHA-256 of the stored bytes
// result.sizeBytes — bytes writtendata accepts a string (encoded UTF-8), a Uint8Array, or an ArrayBuffer. Large files are uploaded in chunks automatically; you don't manage the upload session.
Safe edits with compare-and-swap
To avoid silently clobbering a concurrent change (another tab, or an agent writing the same file), pass expectedHash:
// 1. Read the current file + remember its hash.
const { contentHash } = await client.data.stat("/files/notes.md");
// 2. Write only if the file is still what we read.
try {
await client.data.write({
path: "/files/notes.md",
data: edited,
expectedHash: contentHash, // compare-and-swap
});
} catch (e) {
if (e instanceof SyncropelDataError && e.isConflict) {
// Someone wrote first. e.currentHash is the file's actual hash now —
// reload, re-apply the edit, and retry.
}
}expectedHash has three modes:
| Value | Behaviour |
|---|---|
| omitted | Last-writer-wins — unconditional overwrite. |
| a hash string | Compare-and-swap — write only if the path's current hash matches. |
null | Create-only — fail if the path already exists. |
On a conflict the write throws SyncropelDataError with isConflict === true, expectedHash (what you asserted), and currentHash (the file's real hash) — everything you need to drive a reload-or-overwrite prompt.
Reading
const bytes = await client.data.read("/files/report.pdf"); // Uint8Array
const text = await client.data.readText("/files/notes.md"); // UTF-8 string
// Or resolve a short-lived URL (e.g. to hand to an <img> or a download):
const { blobUrl, expiresIn } = await client.data.readUrl("/files/cover.png");Directory operations
const { entries, cursor } = await client.data.list("/files", { limit: 100 });
for (const e of entries) {
console.log(e.kind, e.path, e.size_bytes);
}
const info = await client.data.stat("/files/notes.md");
// info.kind, sizeBytes, contentHash, mtime, contentType, pinState, provenance
await client.data.mkdir("/files/reports");
await client.data.mv("/files/notes.md", "/files/reports/notes.md");
await client.data.rm("/files/reports", { recursive: true });stat returns provenance — who created the file and when (see Provenance below). It's null for files with no recorded author.
Publishing durable artifacts
A file under /files is mutable working storage. Publishing promotes a file to a content-addressed, durable artifact (an immutable snapshot you can reference by hash):
const { contentHash, pinState } = await client.data.publish("/files/report.pdf");
// The artifact is now pinned + content-addressed; the original /files path stays editable.Finding materials
materials is a typed, byte-free view over your files — ideal for a picker or a search box (it returns metadata, not content):
const hits = await client.data.materials({ q: "budget", limit: 20 });
// resolve a single node's metadata without fetching bytes:
const node = await client.data.node("/files/reports/q3.md");Provenance ("made by you")
Every file records who made it and when. A file you create through the SDK or the API reads as authored by you — not "adopted" from somewhere. stat().provenance carries it:
const { provenance } = await client.data.stat("/files/notes.md");
// provenance: { actor, work_thread, goal_summary, clock, content_hash } | nullFor a feed across many files (a "recently made by" view), use client.data.provenance().
Quota & capabilities
const { used, quota } = await client.data.usage(); // bytes
const caps = await client.data.capabilities(); // limits, chunk size, versionError handling
import { SyncropelDataError } from "@syncropel/sdk";
try {
await client.data.write({ path: "/files/x", data, expectedHash });
} catch (e) {
if (e instanceof SyncropelDataError) {
// e.status — HTTP status (e.g. 409, 403, 507)
// e.code — error code (e.g. "precondition_failed", "quota_exceeded")
// e.detail — human-readable detail
// e.isConflict — true for a compare-and-swap / create-only conflict (409)
// e.expectedHash / e.currentHash — set on a conflict
}
}Reads (read, list, stat, …) throw the same SyncropelDataError on a real error — a missing file or a permission denial surfaces, it doesn't silently return empty.
Types
@syncropel/sdk exports the data-plane types: WriteOptions, WriteResult, ListResult, ListEntry, StatResult, MaterialProvenance, ReadUrlResult, PublishResult, UsageResult, and SyncropelDataError.
What's next
- TypeScript SDK — records, threads, folds, and the rest of the client surface.
- Files — the conceptual model behind the virtual filesystem.
TypeScript SDK
Integrate with Syncropel from JavaScript or TypeScript — emit records, query threads, enforce grammar, fail open on transport errors. Works in Node, Deno, Bun, edge runtimes, and modern browsers.
Python SDK
Integrate with Syncropel from Python — emit records, query threads, enforce grammar, fail open on transport errors.