Documents
The hutly documents command group turns an uploaded document into per-page markdown. Use it when a workflow needs to read a document — extract fields from an agreement, pull conditions out of a contract, summarise a report — rather than search it.
Pages come from a shared, content-addressed cache. The markdown is keyed by the document’s content hash, so the second caller to ask for the same bytes is served from the cache instead of parsing again. A knowledge base ingesting a file, an extraction workflow reading it, and a second workflow reading it later all draw on the same pages.
Authenticates with HUTLY_ORG_ID and HUTLY_API_KEY (or .hutlyrc.yaml / ~/.hutly/credentials), like every other command group. Inside a workflow sandbox both are already set.
Subcommands
parse
Parse a document artifact into per-page markdown.
hutly documents parse art_123
hutly documents parse art_123 --out /tmp/parsed
hutly documents parse art_123 --start-page 10 --max-pages 20
hutly documents parse art_123 --json > result.json| Option | Description |
|---|---|
--start-page <n> |
First page to parse. Defaults to 1. |
--max-pages <n> |
How many pages to parse. Defaults to the whole document. |
--out <dir> |
Write 001.md, 002.md, … into this directory, plus an index.json describing them. |
--progress |
Emit one JSON progress object per line on stderr as each window of pages lands. |
--json |
Print the full result — every page’s markdown — as JSON on stdout. |
Reading progress while it runs
A scanned page costs one vision call, so a long document takes minutes. --progress reports each window as it completes, on stderr — stdout carries the result, so a script can pipe the result and still watch progress:
hutly documents parse art_123 --out /tmp/parsed --progress 2> >(while read -r line; do
echo "read $(jq -r .pagesDone <<<"$line") pages so far"
done)Each line looks like:
{"pagesDone": 4, "pagesFromCache": 0, "lastPage": 4, "resolvedStrategy": "llm-vision"}pagesFromCache is how many of those pages were already parsed — a fully cached document reports its pages and finishes in under a second.
The --out directory
/tmp/parsed/
001.md
002.md
index.jsonEnumerate pages from index.json, not by globbing *.md. It is the manifest for the run that just finished. Reusing an output directory for a shorter parse leaves the earlier run’s later pages on disk — the command writes and never deletes, because --out may be a directory you own and no rule for “which files are mine” survives a directory the command did not write. A leftover file is harmless as long as you read the manifest.
index.json records what produced the pages, so a later run can tell a cached read from a fresh parse:
{
"artifactId": "art_123",
"resolvedStrategy": "llm-vision",
"resolvedModel": "gpt5.4",
"pagesFromCache": 0,
"pagesParsed": 8,
"pages": [{ "pageNumber": 1, "file": "001.md", "summary": "…" }]
}Strategy
The strategy is chosen per document. A PDF with a text layer is read from that layer; a scan has no text layer, so each page is transcribed by a vision model. resolvedStrategy reports which was used — text for a digital PDF, llm-vision for a scan.
This matters for timing. A 32-page digital PDF parses in a few seconds; a 8-page scan takes around a minute, because every page is a model call.
Errors
The command exits non-zero and prints the reason. It never prints an empty document in place of a failure:
| Error | Meaning |
|---|---|
artifact_not_found |
No artifact with that ID in this org. |
Caller is not a member of this organisation |
The token is valid but does not belong to HUTLY_ORG_ID. |
ended without a result |
The stream was cut off mid-parse. The parse may still be running server-side — retrying picks up whatever pages were already cached. |