---
title: Quickstart
description: 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](/functions/runtime#the-session-volume), where the uploaded document sits at `/volume/data/uploads/q3-review.docx`. So let the tool work there:

1. **The model names a file**

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

2. **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.

3. **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](https://github.com/firecrawl/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](https://github.com/subako-ai/subako-function-examples/tree/main/rust/anydoc) wraps it in one function with Subako's [Rust SDK](/functions/rust). This is its core, simplified from the example's source:

```rust src/main.rs
#[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.

```bash
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](/blume-assets/content/docs/_images/en/function-upload.webp)

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](/blume-assets/content/docs/_images/en/function-package.webp)

## Grant it to an agent

Open the agent — or create one, as in the [quickstart](/quickstart#publish-an-agent) — 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](/blume-assets/content/docs/_images/en/agent-editor-functions.webp)

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](/blume-assets/content/docs/_images/en/agent-editor-volume.webp)

Granting the platform's [`files`](/functions/platform-packages#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](/blume-assets/content/docs/_images/en/session-volume-upload.webp)

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](/blume-assets/content/docs/_images/en/session-volume.webp)

Your own app can put files there too, with `PUT /v1/sessions/{session_id}/volume/data/{path}` — see [Sessions over the API](/build/sessions-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](/blume-assets/content/docs/_images/en/session-function-calls.webp)

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](/blume-assets/content/docs/_images/en/session-function-result.webp)

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`](/functions/platform-packages#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

**[Write your own](/functions/write-a-function)**

Start a package from the Rust, TypeScript, or Go template.

**[Granting functions](/functions/granting)**

Network access, credentials, timeouts, versions, and approvals.

**[The runtime](/functions/runtime)**

The volume, assets, HTTP, images, and the limits a call runs under.

**[Examples](/functions/examples)**

Python, Bash, JavaScript, Typst, PowerPoint, and more, as packages.
