Skip to content
Subako Docs
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Quickstart

Let an agent read Word and PDF files with the anydoc package.

Someone uploads q3-review.docx to a session and asks the agent what they need to decide. The answer is in the file. This page is about getting it out — and why a function is the right way to do it.

The problem: the agent cannot read the file

A .docx is not text. It is a ZIP archive of XML files — the paragraphs in one, the styles in another, tables and numbering spread across both — and a model that is handed its bytes sees noise. The same goes for .pdf, .xlsx, and .pptx: almost every document people actually work with is a binary format.

The obvious ways around this all go through the model’s context:

  • Paste the file into the conversation. Base64-encoding a 2 MB document into a message costs more tokens than most context windows hold — for bytes the model cannot parse anyway.
  • Hand it to an MCP server. An MCP server is a separate service. It cannot see the session’s files, so the document would still have to travel through the model — as the arguments of a tool call — to reach it, with the same cost and a context window as the size limit.
  • Ask the user to convert it first. That pushes the work back onto the person the agent was meant to help.

None of these is what we want. The document should never pass through the model at all.

The idea: a tool call is an operation on files

The session already has a filesystem: its Session Volume, where the uploaded document sits at /volume/data/uploads/q3-review.docx. So let the tool work there:

The model names a file

It calls the tool with a path — a few dozen tokens — not with the document.

The tool computes, next to the file

It reads the bytes from the volume, and does the work a model cannot: unpacking the archive, parsing the XML, laying out tables as Markdown.

The result is a file, or a small answer

As its side effect, the tool can write its output back to the volume — q3-review.md, ready for the next tool — and it answers the model with only what the model needs: the Markdown of a short document, or the path and size of a long one.

The model decides what to do; the computation and the bytes stay on the filesystem. That is what a Subako Function is: code that runs beside the session’s volume, in an environment Subako hosts, with no server of your own.

anydoc, as a function

anydoc is an open-source Rust library that does exactly the conversion we need: given a document’s bytes, it detects the format — Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, or text-based PDF — and returns the text as GitHub-Flavored Markdown. On its own it is just a library: something has to run it, next to the file.

The anydoc example wraps it in one function with Subako’s Rust SDK. This is its core, simplified from the example’s source:

#[derive(Deserialize, JsonSchema)]
struct ConvertInput {
    /// Path of the document on the session's Volume, under `/volume/tmp` or `/volume/data`.
    path: String,
    /// Where the Markdown goes: back in the reply (the default), or into a Volume file.
    #[serde(default)]
    output: MarkdownOutput,
}

/// Convert a document on the Volume to GitHub-Flavored Markdown. Reads Word
/// (doc, docx), PowerPoint (ppt, pptx), Excel (xls, xlsx, xlsb), OpenDocument
/// (odt, ods, odp), RTF, EPUB, CSV and text-based PDF. …
#[function]
fn convert_to_markdown(input: ConvertInput) -> Result<Converted, ToolError> {
    // Read the document from the session's volume…
    let bytes = std::fs::read(&input.path)?;
    // …let anydoc do the conversion…
    let format = Format::from_bytes(&bytes).ok_or("unrecognized file")?;
    let markdown = anydoc::to_markdown_bytes(&bytes, format)?;
    // …and answer with the text, or write it to a file and answer with where it is.
    let markdown = match input.output {
        MarkdownOutput::Inline => Markdown::Inline { text: markdown },
        MarkdownOutput::File { path } => {
            std::fs::write(&path, &markdown)?;
            Markdown::File { bytes: markdown.len(), path }
        }
    };
    Ok(Converted { format: format_name(format).into(), markdown })
}

package! {
    name: "anydoc",
    description: "Converts office documents, EPUB, CSV and text-based PDFs to Markdown with anydoc.",
    functions: [convert_to_markdown],
}

That is all the integration there is:

  • #[function] turns an ordinary Rust function into a tool. Its doc comment is the description the model reads, and ConvertInput becomes the input schema the model calls it with.
  • The file access is plain std::fs, because the volume is mounted in the function’s filesystem.
  • package! makes the crate a function package: built for WebAssembly, it is one module.wasm that Subako can run anywhere, for any session, with nothing to deploy.

The rest of this page builds that package, uploads it, and lets an agent use it.

Build anydoc

It takes Rust to build the package.

git clone https://github.com/subako-ai/subako-function-examples
cd subako-function-examples/rust/anydoc
./package.sh        # writes dist/anydoc-function.tar.gz

package.sh builds the module for WASI preview 1 — the example’s rust-toolchain.toml has rustup install that target — and packs it with its subako-package.json into a bundle, the archive Subako takes.

Upload it

In the console, open the workspace’s Functions page and choose Upload package. Choose Archive, pick dist/anydoc-function.tar.gz, and Upload.

Upload a function package: the anydoc archive chosen

Subako runs the module once to ask which tools it offers, then opens the package’s page: the tool, its description, and its parameters, exactly as the model will see them.

The anydoc package’s page: version 1, its build, and the convert_to_markdown tool with its parameters

Grant it to an agent

Open the agent — or create one, as in the quickstart — and Publish new version. Under Tools › Function packages, Add package, choose This workspace, and pick anydoc.

A function package grant: the workspace’s anydoc package, pinned to version 1

The defaults suit anydoc: it needs no network, and a call finishes well within 60 seconds. Then, under Session Volume, switch on Give functions Data (/volume/data), so files uploaded to a session are kept for as long as the session is.

Session Volume: Give functions Data (/volume/data), switched on

Granting the platform’s files package beside it lets the agent find a document it was not given the path of, and read a long conversion a page at a time. Publish version when you are done.

Give it a document

Start a session on the agent, open its Session Volume tab, and choose Upload file. Upload the document into Data (/volume/data), under a folder such as uploads.

Upload a file: q3-review.docx into Data (/volume/data), in the folder uploads

The volume is where the agent’s functions read and write: the file is now at /volume/data/uploads/q3-review.docx.

Session Volume: the Data area, holding uploads/q3-review.docx

Your own app can put files there too, with PUT /v1/sessions/{session_id}/volume/data/{path} — see Sessions over the API.

Ask about it

Back on Conversation, ask something only the document can answer. The agent calls convert_to_markdown on the file, reads the Markdown, and answers from it.

The conversation: the question, the convert_to_markdown call on the function anydoc, done, and the agent’s answer listing three decisions

Open the call to see exactly what happened: the arguments the model sent, and the Markdown anydoc answered, table and all.

The convert_to_markdown call opened: its path argument, and the document as Markdown

A short document comes back inline, as here. For a long one — a 300-page manual — the model asks for "output": {"type": "file", "path": "/volume/data/uploads/manual.md"} instead: anydoc writes the Markdown to the volume and answers with only its path and size, and the agent reads the parts it needs with the platform files package. Either way, the document’s bytes never pass through the model.

That is the whole loop: a package uploaded once, granted to an agent, and called by every session that needs it — with no server of your own to run.

Next

Was this page helpful?