Read and write files from code
urai:fs is the uraijs library for the thread filesystem.
Import fs and call its methods. All methods that touch a file are async.
import { fs } from "urai:fs";
// First script: save the result.await fs.writeJSON("/work/customers.json", customers);
// A later script, in the same turn or in a later turn:const saved = await fs.readJSON("/work/customers.json");The library asks for no API key and no configuration. Urai attaches the thread’s workspace to the execution before the script starts.
Check that a filesystem is attached
Section titled “Check that a filesystem is attached”Not every execution has a filesystem. Call fs.available() one time at the start
of the script, and do not catch an error on each call. “No filesystem here” is a
different condition from “this write failed”.
import { fs } from "urai:fs";
if (!fs.available()) { throw new Error("This run has no thread filesystem.");}This check is most useful in a library, because the same library can run in an
agent script, which has a filesystem, and in a tool or a workflow, which does
not. When no filesystem is attached, every file method throws NoWorkspace.
fs.workspace() returns the id of the attached workspace, which is the thread
id.
- A path starts at the root of the thread’s workspace.
/out/report.mdandout/report.mdare the same file. - A path cannot contain
... The library refuses it. - Each part of a path can have at most 255 bytes.
There is no current directory and no way to reach the host’s filesystem.
Write files
Section titled “Write files”await fs.writeText("/out/summary.md", markdown);await fs.writeJSON("/work/rows.json", rows);await fs.write("/out/logo.png", pngBytes); // Uint8ArrayA write creates the parent folders, and it replaces the file if the file exists.
writeJSON writes indented JSON, so a person can read the file too.
To add to a file, append:
await fs.appendLine("/work/progress.log", `fetched page ${page}`);await fs.append("/work/export.csv", csvChunk);appendLine adds the newline when the line has none. append takes a string or
a Uint8Array. Both create the file when it does not exist.
Read files
Section titled “Read files”const text = await fs.readText("/uploads/notes.txt");const rows = await fs.readJSON<Row[]>("/work/rows.json");const bytes = await fs.read("/uploads/photo.jpg"); // Uint8ArrayreadText decodes UTF-8 and replaces invalid bytes with U+FFFD. When a file is
not valid JSON, the readJSON error names the file.
Each read loads the whole file into memory. An agent script has a limited heap,
so read a large file only when the script needs all of it. Use fs.decode(bytes)
to decode a Uint8Array that you already have.
Reading a file that does not exist throws an error. Use fs.exists() when a
missing file is a normal result:
const seen = (await fs.exists("/work/seen.json")) ? await fs.readJSON<string[]>("/work/seen.json") : [];Find files
Section titled “Find files”list returns the direct children of a folder, sorted by name:
for (const entry of await fs.list("/uploads")) { console.log(entry.name, entry.kind); // "sales.csv" "file"}glob searches the whole workspace with a gitignore-style pattern. It returns
files only, not folders:
const csvs = await fs.glob("/uploads/*.csv");const allJson = await fs.glob("**/*.json");
for (const hit of csvs) { console.log(hit.path, hit.size, new Date(hit.mtime));}A pattern can start with / or not. stat returns the size, the kind, the
modification time, and the mode of one path.
Remove files
Section titled “Remove files”await fs.remove("/work/page-1.html");await fs.remove("/work/pages", { recursive: true });remove without recursive refuses a folder. Nothing removes the root folder.
mkdir creates a folder and its parents. A write creates the parents too, so
you seldom need mkdir.
Stay inside the quota
Section titled “Stay inside the quota”The thread has a limit on total size, on the size of one file, and on the number of files. The limits are shared with the user’s uploads.
Urai checks the limit before it writes. When a write goes over, it throws
QuotaExceeded, writes nothing, and leaves the file as it was. Catch that error
when the script can do something useful, such as write less:
import { fs, QuotaExceeded } from "urai:fs";
try { await fs.writeJSON("/out/full-export.json", rows);} catch (e) { if (!(e instanceof QuotaExceeded)) throw e; await fs.writeJSON("/out/export-first-1000.json", rows.slice(0, 1000));}fs.usage() returns the current usage and the limits:
const u = fs.usage();// { bytes, files, maxBytes, maxFileBytes, maxFiles }if (u && u.bytes + estimate > u.maxBytes) { // write less}usage() returns null when no filesystem is attached.
Make the output durable
Section titled “Make the output durable”Urai copies writes to durable storage all the time, and it saves everything when the script ends. If the host fails during a script, the most recent writes can be lost.
Call fs.flush() after the script makes something that you must keep, for
example before a long step that follows:
await fs.writeJSON("/out/results.json", results);await fs.flush();flush also confirms that this execution still owns the workspace. One call is
enough. Do not call it after every write.
Handle errors
Section titled “Handle errors”| Error | Cause | What to do |
|---|---|---|
NoWorkspace |
No filesystem is attached to this execution. | Check fs.available() at the start. |
QuotaExceeded |
The write goes over a limit. Nothing was written. | Write less, or remove files you do not need. |
WorkspaceLost |
Another process now owns the workspace, and Urai is stopping this execution. | Do not retry. |
Error |
Any other failure, for example a missing file, a path with .., or remove on a folder without recursive. |
Read the message. |
Import the error classes from urai:fs and test them with instanceof.
Rules for agent scripts
Section titled “Rules for agent scripts”Put these rules in the agent’s instructions or in a skill when the defaults are not enough:
- Return paths, not contents.
await urai.complete({ report: "/out/report.md" })keeps the file out of the model’s context. - Use clear names. The user sees the file names in Browse files.
- Never write a secret to a file. The file stays after the execution ends.
- Put files for the user in
/out/and intermediate files in/work/.
Workflows
Section titled “Workflows”Workflow steps have no filesystem at this time. fs.available() returns false
in a workflow, and every file method throws NoWorkspace.
Develop locally
Section titled “Develop locally”uraijs eval attaches a local workspace, so a script that uses urai:fs runs
on your machine without changes:
uraijs eval write.tsuraijs eval read.tsThe files stay between commands. The second script reads what the first one
wrote. The CLI keeps the workspace in .urai/workspace/ in the working
directory. uraijs init adds .urai/ to .gitignore.
| Task | Command |
|---|---|
| Use a different workspace | uraijs eval script.ts --workspace charts |
| Use a different working directory | uraijs eval script.ts -d ./project |
| Start again with no files | rm -rf .urai/workspace |
The local workspace has the same limits as a thread, so a script that stays
inside the limits on your machine also stays inside them in a conversation.
Only uraijs eval attaches a workspace. uraijs call, uraijs dev,
uraijs test, and uraijs workflow run do not.
To make a folder look like a thread, put test files in /uploads/ first:
import { fs } from "urai:fs";
await fs.writeText( "/uploads/sales.csv", "region,amount\nnorth,1200\nsouth,950\n",);await urai.complete({ seeded: "/uploads/sales.csv" });