> ## Content Index
> Fetch the complete content index at: https://www.sfrt.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Snowflake's AI_PARSE_DOCUMENT got an agentic mode
- URL: https://www.sfrt.io/snowflakes-ai_parse_document-got-an-agentic-mode/
- Published: 2026-11-04T06:45:00.000Z
- Updated: 2026-11-04T06:45:00.000Z
- Description: 12x the price of OCR per page, and it earned that on one file type
- Author: Martin Seifert
- Tags: Lakehouse

TL/DR: Snowflake's `AI_PARSE_DOCUMENT` now has an `AGENTIC` mode. It classifies the charts, diagrams, tables and infographics inside a document, writes a summary of each, and hands that back alongside extracted text. I ran it over my SharePoint library that feeds one of my Cortex Search services. Measured cost: 8.18 credits per 1,000 pages (OCR at 0.68). Slide decks were the only place it earned the difference, my PDFs had nothing worth describing 😅 

I learned two things not mentioned in the docs: image summaries live outside the page `content`, and `mode` has to be a literal. Putting a `CASE` expression there returns NULL with no error at all.

---

## The question

The new mode was released (to public preview) on October 28th, 2026\. Would it help me with any of the document pipelines already running? 

[Parsing documents with AI\_PARSE\_DOCUMENT | Snowflake Documentation![](https://storage.ghost.io/c/7d/94/7d942fe1-7868-4a1a-b2c9-4eb415b1a546/content/images/icon/favicon-177376bc-bc94-4d81-9608-b1cadcc123ab.png)Snowflake Documentation![](https://storage.ghost.io/c/7d/94/7d942fe1-7868-4a1a-b2c9-4eb415b1a546/content/images/thumbnail/example-scientific-paper-image-21ba4d3e-3e5b-4966-a9b4-be0884bed42f.png)](https://docs.snowflake.com/en/user-guide/snowflake-cortex/parse-document?ref=sfrt.io#label-parse-document-agentic-mode)

I load many docs from SharePoint into ❄️, parse them there and feed a search service with the content. So that's my obvious playground to test that new mode...

## What the pipeline looks like

I use a dlt pipeline to load the docs into Azure Blob Storage. For the docs themselves I wouldn't really need dlt, but I also load the SharePoint permissions along with the documents (using Azure Graph + SharePoint REST APIs) for obvious reasons (not every user should read every doc). So I bundled those two components into one pipeline running on dltHub.

And this happens downstream:

1. An external stage sits on top of the blob container.
2. Every time the dlt pipeline load is done, a task (triggered as a last step at the end of the dlt pipeline) lists the stage directory, MD5s it against a metadata table, and tags each file `NEW`, `MODIFIED` or `DELETED`.
3. `NEW` and `MODIFIED` docs get fed to `AI_PARSE_DOCUMENT` with page split enabled, and land in a pages table.
4. Chunking then cuts pages into 1,400-char pieces with a 200-char overlap (neighbouring pages ride along as context) and writes them to a chunks table.
5. And finally, a Cortex Search service on top of those chunks is what the agent queries.

0:00 

/0:06 

1× 

What the stage held:

- 18,352 .pdf, 64 GB
- 6,459 .docx, 1.3 GB
- 800 .aspx, 3 MB
- 115 .pptx, 1.8 GB

Every one of those files was previously parsed with `{'mode': 'OCR', 'page_split': true}`.

## What the docs say about agentic mode

Three modes are available: `LAYOUT` (text with structure, tables and headers), `OCR` (plain fast extraction) and `AGENTIC`. Using the agentic mode, Snowflake looks at the visual elements in a file (charts, diagrams, screenshots, forms, images), classifies them, decides which processing each of them needs, and returns semantic summaries plus structured data: chart series and their values, table headers and cells, diagram components and how they relate, all next to the usual text and layout.

Of course, that mode has its own limits:

- Agentic mode caps at 2,000 images per document.
- `extract_images` doesn't work here... set it to TRUE and the function hands back NULL.
- Accepted types are PDF, PPTX, DOCX, JPEG, JPG, PNG, TIFF, TIF, HTML, TXT. A file can be at most 100 MB and 2,000 pages.
- The image schemas describe what the model is expected to output, with no guarantee that it conforms. Any field I depend on needs validation.
- `page_filter` limits the work to selected pages and implies `page_split`. That's the cheapest way to play with the new mode 😜

Billing is quoted per page with no mention of a separate agentic rate. Which meant I had to measure it myself...

## A first comparison on just a few files

I randomly picket three PPTX and three PDFs between 1.5 and 10 MB, each parsed twice (OCR and AGENTIC). Then I compared pages, characters and image counts.

| File                     | Pages | OCR chars | AGENTIC chars |
| ------------------------ | ----- | --------- | ------------- |
| PDF, insurance statement | 34    | 39,174    | 43,677        |
| PDF, invoice             | 3     | 2,171     | 2,522         |
| PDF, single page         | 1     | 2,041     | 2,197         |
| PPTX, 28 slides          | 28    | 10,566    | 10,842        |
| PPTX, 23 slides          | 23    | 12,586    | 12,125        |
| PPTX, 6 slides           | 6     | 5,674     | 5,917         |

Pages came back equal and characters landed within roughly 10% of each other, so agentic doesn't extract more text. However, the reading order is changed: On a two-column slide, OCR interleaves the columns line by line, whereas agentic gives back clean markdown with headings and `| table |` rows.

For all six files the `images` column came back NULL. I initially suspected the sample probably had no visuals in it. That was wrong, though...

## Where the image data actually is

Turn `page_split` on and the images aren't at the top level of the response anymore. Instead, every page object carries its own `images` array:

```
{"metadata": {...},
 "pages": [
   {"index": 0, "content": "# Title\n\n![img-0.jpeg](img-0.jpeg)", "dimensions": {...},
    "images": [{"id": "img-0.jpeg", "top_left_x": ..., "results": {...}}]}
 ]}
```

Two notes on how to use this:

- `content` only holds placeholders (`![img-0.jpeg](img-0.jpeg)`), the summaries are not inlined - so there is no visual interpretation here.
- The shape of `results` follows `image_type`: Photos carry `description` and `visible_text`; an app screenshot carries `app_or_page`, `purpose`, `state_signals` and `visible_text`; charts carry `chart_type` and `key_values`; diagrams carry a `components` array of labels and roles.

## What kinds of images exist in practice

The second sample: 8 random PPTX plus 8 random PDFs, first 8 pages of each (`page_filter: [{'start': 0, 'end': 8}]`). I flattened every image and grouped by `image_type`:

| image\_type       | Images | Files | Typical content                                                                |
| ----------------- | ------ | ----- | ------------------------------------------------------------------------------ |
| other             | 39     | 14    | logos, stock illustrations                                                     |
| photo\_with\_text | 13     | 7     | photos with cropped text                                                       |
| diagram           | 14     | 4     | swimlane process flows, quarterly roadmaps, a risk framework drawn as a temple |
| infographic       | 6      | 1     | posters with sections and key facts                                            |
| chart             | 5      | 2     | bar charts                                                                     |
| table             | 3      | 1     | wide process/control matrices                                                  |

The `chart` output is most interesting here. One of them returned this: "most frequent topic across all periods, peaking at 4,754 in the fourth period; all categories except one show a continuous upward trend \[...\]". A roadmap diagram came back with every initiative label and its status. None of that appeared in the OCR text of the same slides.

However, image descriptions are mostly noise 😅 The first four types in the above list (logos and stock photos) would just dump junk into a search index. The filter I ended up with keeps `chart`, `diagram`, `table` and `infographic`, and throws away `other` and `photo_with_text`. But that decision presumably is very dependent on the use case.

## Do PDFs have the same payoff?

Since 18,000 of the files are PDFs, those would be the major cost drivers. Another experimental run: 12 random PDFs sized 1 to 20 MB, first 8 pages, agentic only.

| image\_type                        | Images | Files | Content                               |
| ---------------------------------- | ------ | ----- | ------------------------------------- |
| other                              | 19     | 8     | Swiss QR-bill codes, one grainy photo |
| photo\_with\_text                  | 11     | 4     | signature blocks                      |
| chart, diagram, table, infographic | 0      | 0     | none                                  |

Invoices, statements and contracts make up most of my PDF library. Nothing from the sample would have survived the previously mentioned filter. Now, twelve files can easily miss a rare type, and a few chart-heavy reports probably exist somewhere in there (I didn't look at each PDF and its content, obviously), but that i.m.h.o. doesn't justify paying a significantly higher price across every PDF.

## What it costs

Credits and a `metrics` array with the page count sit in `SNOWFLAKE.ACCOUNT_USAGE.CORTEX_AI_FUNCTIONS_USAGE_HISTORY`, one row per query and model:

| Mode    | Pages                    | Credits | Per 1,000 pages |
| ------- | ------------------------ | ------- | --------------- |
| OCR     | 95                       | 0.0646  | 0.68            |
| AGENTIC | 95 (same six files)      | 0.7771  | 8.18            |
| AGENTIC | 528 (20-file run)        | 4.319   | 8.18            |
| AGENTIC | 64 (three separate runs) | 0.5235  | 8.18            |

Every agentic query landed on a flat 8.18 credits per 1,000 pages, no matter how many images a page carried. A page of plain text bills the same as a page full of charts. That's roughly 12 times what OCR costs.

Projected onto my SharePoint catalog: 115 public PPTX covering 1,559 pages (the exact page count came straight from the already existing pages table), so about 12.8 credits under agentic - compared to roughly 1.1 for OCR. When I apply the same math on the PDFs, it's a few hundred credits above OCR... for a result that (presumably!) adds value only on rare occasion.

## The pipeline change

What I settled on: `AGENTIC` for PPTX files, OCR for everything else. And image summaries only for the four useful types appended to the page text. The (non-ASPX) parse step of my task turned into:

```sql
cte_content as (
    select p.blob_file_path, p.blob_last_modified
        , ai_parse_document(
            to_file('@raw.sharepoint.s_documents', p.blob_file_path),
            {'mode': 'AGENTIC', 'page_split': true}
          )                                             as parsed_pages
    from cte_to_process p
    where p.file_extension ilike '.pptx'
    union all
    select p.blob_file_path, p.blob_last_modified
        , ai_parse_document(
            to_file('@raw.sharepoint.s_documents', p.blob_file_path),
            {'mode': 'OCR', 'page_split': true}
          )                                             as parsed_pages
    from cte_to_process p
    where p.file_extension not ilike '.pptx'
),
cte_pages as (
    select c.blob_file_path, c.blob_last_modified
        , f.value:index::integer        as page_index
        , f.value:content::varchar      as page_text
        , f.value:images                as page_images
    from cte_content c
        , lateral flatten(input => c.parsed_pages:pages) f
),
cte_image_text as (
    select pg.blob_file_path, pg.page_index
        , listagg(
            array_to_string(array_construct_compact(
                  concat('[', im.value:results:image_type::varchar, ']')
                , im.value:results:summary::varchar
                , im.value:results:details:chart_type::varchar
                , nullif(array_to_string(coalesce(
                      im.value:results:details:key_values
                    , im.value:results:key_values), '; '), '')
                , nullif(array_to_string(transform(
                      im.value:results:details:components
                    , c -> c:label::varchar), ', '), '')
                , nullif(array_to_string(
                      im.value:results:details:columns, ', '), '')
              ), ' ')
            , '\n')                     as image_text
    from cte_pages pg
        , lateral flatten(input => pg.page_images) im
    where im.value:results:image_type::varchar
              in ('chart', 'diagram', 'table', 'infographic')
    group by pg.blob_file_path, pg.page_index
)
select ...
    , nullif(array_to_string(array_construct_compact(
          pg.page_text, it.image_text), '\n\n'), '')    as page_content
from cte_pages pg
    join _public_docs pd on pd.blob_file_path = pg.blob_file_path
    left join cte_image_text it
        on it.blob_file_path = pg.blob_file_path
        and it.page_index = pg.page_index
```

A few side notes on this:

- My first attempt used `concat_ws` and produced empty strings where the image text should have been... `array_construct_compact` handles NULL differently.
- Empty pages stay NULL thanks to the outer `nullif(..., '')`. The chunker skips NULLs, but a blank string would have created empty chunks.
- Scalar subqueries can't contain a correlated `flatten` ("Unsupported subquery type cannot be evaluated"), so the image text sits in its own CTE and joins back on its file's page.

In the index, a page containing a chart now looks like this:

```
<normal slide text>

[chart] A grouped bar chart showing the frequency of five psychological topics ...
bar "suicidal thoughts" is the most frequent topic across all periods, peaking at 4,754 ...
```

## The run that succeeded and did nothing at all

The first manual run took 488 seconds, then `SUCCEEDED`. All 115 decks sat in the metadata table marked as processed. `t_documents` and `t_document_chunks` held zero PPTX rows, though 😅

Looking into `ACCOUNT_USAGE.QUERY_HISTORY` on the task's query tag: the insert burned 447 seconds and inserted 0 rows, while the later metadata merge inserted 115 rows. Seven minutes of parsing, nothing to show for it, no error anywhere.

I investigated the syntax:

- `ai_parse_document(..., {'mode': 'AGENTIC', ...})` with a literal returns the object just fine.
- the same call with `{'mode': case when p.file_extension ilike '.pptx' then 'AGENTIC' else 'OCR' end, ...}` returns NULL. No error, no warning.

My intention here was to pick the mode per row with a `CASE` because it looked tidy. But the function wants a constant. Hence the two-branch `union all` above.

## And the real run

The same statements again took slightly over 20 minutes this time, against 8 for the empty run. Straight off the tables:

|                                              | Before | After |
| -------------------------------------------- | ------ | ----- |
| PPTX files in the pages table                | 115    | 115   |
| PPTX pages                                   | 1,559  | 1,559 |
| NULL pages                                   | 0      | 0     |
| Pages with appended chart/diagram/table text | 0      | 380   |
| Chunks                                       | 3,877  | 4,274 |
| Chunks containing image text                 | 0      | 1,110 |
| Non-PPTX page rows                           | 5,968  | 5,968 |

Roughly a quarter of the pages carry visual content worth indexing (plus the corrected read order mentioned earlier), the chunks grew about 10%. The task's final step refreshed the Cortex Search service. From now on only new or modified PPTX pay the higher rate, so in steady state the gap to OCR stays small 😎