Files & nodes
Working with files & nodes
The operations your apps and engines use most — reading, uploading, updating and listing workspace content.
Most extensions read and write workspace content. This page summarises the operations you'll use and the scopes they require. In apps, prefer the SDK bridge; engines use the language SDK or call the gateway directly.
Common operations
Read / download a filenode.file.readFetch a file node's content (bytes) and its MIME type. Used by viewers, editors and any engine that reads inputs.
Upload / create a filenode.node.createCreate a new file node with content. Upload uses node.node.create, not a node.file.write scope
(which doesn't exist).
Update a filenode.node.updateReplace an existing file node's content, creating a new version.
Delete a filenode.file.deleteRemove a file.
List a foldernode.node.read-allEnumerate the nodes under a path.
Record activitynode.journal.createAppend a journal entry describing a change (history/audit).
Files are versioned
Writing new content to a file creates a new version; the node's head points at the latest. This means:
- An update never silently loses prior content — earlier versions remain addressable.
- Engines that transform files in place produce a clean history of runs.
See Workspaces, nodes & files for the underlying model.
Uploading files (the contract)
File uploads have one supported shape — follow it exactly; the alternatives are rejected on purpose.
POST /v{version}/files/{path} must be multipart/form-data with:
- a
filepart — the content, buffered (so the server can hash and content-sniff it); and - an optional
metadataJSON part —{ "branch": "main", "idempotencyKey": null, "contentHash": null }.
Raw request bodies are rejected
A raw (non-multipart) body returns 400 Bad Request. Node must buffer and seek the content to hash it,
and a non-seekable stream can't be. This is exactly how the workspace UI (useFiles.ts), the
SDK (files.upload) and the platform's own file gateways upload — do
the same and don't invent a new path.
Required scopes. An upload isn't a single write — it also creates parent folders and appends journal
entries, and each of those is authorized against your token. A correct upload token therefore carries the
full set, not just node.node.create:
node.node.create · node.file.read · node.node.update · node.journal.create
Missing node.journal.create is the classic failure: the write starts but the journal entries — whose events
provision the new folder's shard configuration — can't be written, so a later lookup fails. If you assemble the
token yourself (e.g. a service acting on a user's behalf), request all four scopes.
Addressing content
A node can be addressed by its id (stable, location-independent) or by its path (human-readable, changes if moved). Prefer ids when you hold a reference across operations; use paths for discovery/listing.
Example: read then write (engine)
from verentis import engine
ctx = engine.context()
src = engine.files.read(ctx.file.id) # node.file.read
out = transform(src)
engine.files.write( # node.node.create / update
"/outputs/result.json",
out,
mime_type="application/json",
)
Example: read a file over HTTP (standalone)
curl https://api.localtest.me/v1/nodes/<nodeId>/content \
-H "Authorization: Bearer $TOKEN" \
--output result.bin
Asynchronous side-effects
Some workspace activity (like path/journal updates) happens asynchronously after an upload completes. Don't assume every downstream record exists the instant a write returns; design engines and triggers to be idempotent. See Triggers & schedules.
Next
Package and ship your extension to the marketplace.