Read documents from code
urai:knowledge is the uraijs library for reading the documents in your
collections. A tool or a workflow imports it, opens a document by name or by id,
and streams the text.
import { knowledge } from "urai:knowledge";
const doc = await knowledge.open("q3-links.md", { collection: "Sales" });
const domains = new Set<string>();for await (const line of doc.lines()) { for (const m of line.matchAll(/https?:\/\/([^\/\s")]+)/g)) domains.add(m[1]);}The library asks for no API key. It resolves the reserved urai:knowledge
secret, and the host answers with a short-lived token that reaches only the
collections this execution was granted.
Search finds passages, the library reads documents
Section titled “Search finds passages, the library reads documents”POST /search and the search_documents tool return the passages that match a
query, with a citation for each one. That answers a question. urai:knowledge
does a different job. It gives your code the whole text of a document, in
windows, so a script can count, group, filter, or total it.
| You want | Use |
|---|---|
| The passages that match a question | POST /search or the search_documents tool |
| Every row of a 40 MB export | urai:knowledge |
| A quote for a model to read | search_documents, or read_document in agent mode |
| An arithmetic over a set of documents | urai:knowledge inside a script |
An agent can already put a document into its own context with read_document,
but that caps out at about 40,000 characters and bills the text as tokens.
urai:knowledge puts the bytes where the loop is. The model never holds them.
The two work well together. Search first to find the document, then open it by
the id the result carries:
const doc = await knowledge.open(result.document.id);const rows = [];for await (const line of doc.lines()) { if (line.startsWith("| ")) rows.push(line);}List what you can read
Section titled “List what you can read”const docs = await knowledge.list({ collection: "Sales", path: "reports/2026" });| Option | Effect |
|---|---|
nameContains |
Case-insensitive substring match on the document name. |
collection |
One collection, by its name or its slug. |
path |
A folder and everything under it, for example "policies/2026". |
limit |
At most 200, which is also the default. |
Each entry describes one document.
| Field | Notes |
|---|---|
id |
The document id. Open with it when a name is ambiguous. |
name |
The file name. |
collection |
The collection the document belongs to. |
bytes |
The size of the extracted text. This is what read pages through. Absent for a document ingested before the size was recorded, and filled in once the document is read. |
pages |
Absent for a format that has no pages, such as Markdown, text, and spreadsheets. |
paths |
The folders the document is filed in, /-joined. Empty for a document at the collection root. Two entries mean one document filed in two places, not two copies. |
Open a document
Section titled “Open a document”const doc = await knowledge.open("invoice.pdf", { collection: "Finance" });open takes a name, an id, or a name qualified by its folder, such as
"policies/2026/leave.pdf". A qualified name means that folder and not one
below it.
Always pass collection when you open by name. A file name is unique only
inside its collection, so a name that matches in two collections makes open
throw and name both. Opening by id never needs one.
open also throws when there is no such document, and lists what is reachable.
Use knowledge.find() instead when an absence is an ordinary outcome. It returns
null for a missing document, and still throws on an ambiguous name.
Read the text
Section titled “Read the text”Four methods read from an open document. All of them page, because the library never holds a whole file unless you ask it to.
lines()
Section titled “lines()”for await (const line of doc.lines()) { // one line at a time}Line terminators are stripped, and \r\n is handled, so a file written on
Windows leaves no stray \r. A last line without a trailing newline is still
returned.
lines() throws on a file with no line structure, such as minified JSON or a
single-row export. Buffering such a file would look like streaming and would
fill the heap. Use chunks() for it.
chunks()
Section titled “chunks()”for await (const chunk of doc.chunks({ bytes: 32 * 1024 })) { // successive windows, no gaps and no overlap}The default window is 64 KiB.
text()
Section titled “text()”const whole = await doc.text();text() reads a small document whole. It refuses past 8 MiB rather than fill
the heap quietly. Pass { maxBytes } to raise the limit when you mean to, or
use lines() or chunks() when one pass over the text is enough. One pass is
almost always enough.
read()
Section titled “read()”read() is the primitive the other three are built on. It returns one window.
let offset = 0;for (;;) { const w = await doc.read({ offset, length: 64 * 1024 }); process(w.text); if (w.eof) break; offset = w.offset + w.bytes;}| Field | Notes |
|---|---|
offset |
Where the window starts. This is not always what you asked for, because an offset inside a multi-byte character advances to the next character boundary. |
bytes |
The source bytes the window consumed. |
eof |
Whether the window reaches the end of the document. |
text |
The text of the window. |
Continue from offset + bytes of the reply, and never from the offset you asked
for. Those are the only values that land on a character boundary. length is
clamped by the host, currently to 256 KiB.
What the text is
Section titled “What the text is”You get the extracted text, which is the Markdown the knowledge pipeline
produced. For a .md or .txt upload that is the original file byte for byte.
For a PDF, a DOCX, or an XLSX it is the conversion. Tables become Markdown
tables, and the original bytes are not available here.
Grant the collections
Section titled “Grant the collections”The library reads only the collections the execution was granted. An execution with no collections gets nothing, and the error says so.
- An agent. Attach the collections to the assistant. Scripts the agent writes
then read them. This is the same grant that gives the agent its
search_documentsandread_documenttools. - A workflow. Name the collections in
knowledge_collectionswhen you register the workflow over the API, by slug or by id. An empty list grants nothing. See Register and secure it.
A grant is checked when it is spent, not only when it is minted. A collection deleted after the token was minted stops being readable at once.
Rules for workflows
Section titled “Rules for workflows”These are network calls, so they belong inside Step.run. A Workflow.run
coordinator body re-executes on every tick and must stay free of side effects.
const total = await Step.run("total-invoices", async () => { const doc = await knowledge.open("invoices-q3.csv", { collection: "Finance" }); let sum = 0; for await (const line of doc.lines()) sum += Number(line.split(",")[3] ?? 0); return sum;});Develop locally
Section titled “Develop locally”urai:knowledge normally exchanges an execution credential for a short-lived
token, and that exchange needs a live host. The CLI has none. Put an ordinary
API key entitled to the knowledge product in a local secrets file, and the same
code runs unchanged:
{ "URAI_KNOWLEDGE_API_KEY": "sk-urai-...", "URAI_BASE_URL": "https://chat.app.urai.dev"}The key has no effect in production, where the host mints the credential.
The scope differs, and that matters. An API key reaches every collection in its organization. A host-minted capability reaches only the collections the execution was granted. Code that finds a document locally can still get a “not found” in production. When that happens, the document is in a collection the agent or the workflow was not given.
Errors
Section titled “Errors”| Condition | What you get |
|---|---|
| The document does not exist, or it is not in scope | The same “not found” for both, so a script cannot probe for documents it was denied. |
| The document is still being processed | An error that names the processing status. Wait and read it again. |
| The name matches in more than one collection | An error that names the collections. Pass { collection }, or open by id. |
| The execution has no collections | An error that says urai:knowledge is not available to this execution. |