Skip to main content
Databricks reads documents natively with ai_parse_document and ai_extract. This guide routes that workload to Unsiloed for a confidence score and a source citation for every grounded value, so you can triage the values worth checking instead of trusting them all equally.Document processing runs inside Databricks. Unity Catalog governs the pipeline, and you can trigger or schedule it like any other Databricks workload. The only local tool this guide uses is the Databricks CLI for one-time secret setup.

Why Call Unsiloed from Databricks

PDFs in a Unity Catalog volume are just bytes. You can list them and count them, but you can’t join them to anything, because none of what matters is in a column yet. This guide turns them into a table. Every document becomes a row, and every extracted field carries a value and two confidence scores. When Unsiloed can ground a value in the source document, the field also includes a citation pointing to the region of the page it came from. That confidence signal is the reason to route the work through Unsiloed. High- confidence values can flow into downstream analysis, while lower-confidence values can enter a review queue. Both land in the same table, so you can define a review policy with a SQL filter.

How the Pipeline Works

Documents land in a Unity Catalog volume. An ETL pipeline reads them, sends each one to Unsiloed for extraction, and writes the results back as Delta tables in the same schema, so the documents and the data pulled out of them stay together.
An ETL pipeline reads new documents from a Unity Catalog volume with Auto Loader, extracts each one through Unsiloed, and writes extractions, extracted fields, and extraction errors to three Delta tables

What We’ll Build

An ETL pipeline with three tables:
  • extractions: one row per document, holding the complete Unsiloed result object in a VARIANT column
  • extracted_fields: one row per successfully extracted field, including its confidence scores and citation
  • extraction_errors: one row per document that could not be extracted
The pipeline reads the volume with Auto Loader, so a normal run discovers document paths that haven’t already committed. We’ll build the code up a piece at a time below. If you’d rather skip the walkthrough, take the whole file from the dropdown.
This is the complete file. Paste it into the pipeline’s starter file in Step 4, changing VOLUME and FIELDS to match your own volume and the fields you want.
my_transformation.py

Prerequisites for the Databricks Pipeline

You need four things before starting:
  • A Databricks workspace with Unity Catalog and access to a SQL warehouse
  • Permission to use your target catalog and create a schema, volume, pipeline, streaming tables, and materialized views
  • An Unsiloed API key from the Unsiloed dashboard
  • The Databricks CLI, authenticated with databricks auth login
You also need at least one supported document to test. The extractor accepts PDFs, images, and Office documents. Invoice-like documents work with the example fields in this guide; change FIELDS if you use another document type. You only need the CLI once, in Step 1, to store your API key. Everything after that happens in the Databricks UI.

Step 1: Store Your API Key as a Secret

The pipeline reads your key from a Databricks secret scope, so it never appears in the code.
1

Create the secret scope

Databricks has no menu entry for this page, so open it directly, replacing <your-workspace> with your own workspace URL:
Enter unsiloed as the scope name, leave Manage Principal set to Creator, and click Create. Limiting management to the creator prevents other workspace users from changing the API key or the scope permissions.
The Create Secret Scope page with the scope name set to unsiloed and the Create button highlighted
2

Add your key to the scope

You set the value itself through the Databricks CLI, the only part of this guide that needs a terminal:
Paste your Unsiloed API key when the CLI prompts for the secret value. The interactive prompt keeps the key out of your shell history and process list.Confirm it saved:

Step 2: Put Your Documents in a Volume

Documents go in a Unity Catalog volume, which is what Databricks uses for files. They don’t go in a table, and the pipeline reads them straight out of the volume.
1

Create a schema and volume

In a SQL editor, run:
Substitute your own catalog if you aren’t using workspace.
2

Upload your documents

In the sidebar, click Catalog, then expand your catalog and schema. Volumes sit under their own Volumes node, separate from Tables, so open that and select docs. Then click Upload to this volume.The catalog browser needs a running SQL warehouse to list anything. If the tree spins, start your warehouse and try again.
The docs volume in Catalog Explorer showing five uploaded PDF invoices, with the Upload to this volume button highlighted
Drop your files in and click Upload. Any mix of PDFs, images, and Office documents works.
The upload dialog with the file drop zone and the Upload button highlighted

Step 3: Write the Pipeline

A Databricks ETL pipeline is a Python file that declares tables. You don’t write the orchestration. You write a function per table, decorate it with @dp.table, and Databricks works out the dependency order and what to refresh.
This is all one file. The four blocks below are consecutive parts of a single Python file, not alternatives. Append each one to the end of the last, in order.That file is transformations/my_transformation.py, which Databricks creates for you when you make the pipeline in Step 4. Draft it in your own editor as you read, then paste the finished file in when you get there. If you’d rather build it up in place, create the pipeline first and come back to this step.

3.1 Imports and Configuration

Start the file with the imports and the configuration block. The pyspark.pipelines module declares the tables, and it’s available inside a pipeline without installing anything. We call Unsiloed with requests. Four constants near the top control the pipeline. VOLUME is where your documents live, and FIELDS defines what to extract. MAX_FILE_BYTES rejects files that are too large to copy safely through a Python UDF, while MAX_CONCURRENT_EXTRACTIONS bounds how many partitions submit work in parallel. Start with four concurrent extractions and raise the value only after checking your Unsiloed rate limit.
my_transformation.py
The dbutils.secrets.get call reads the key you stored in Step 1, so the key itself never appears in the file. The SCHEMA constant turns FIELDS into the JSON Schema the API expects, which means adding a field later is a one-line change.
Use string for amounts and dates. Real invoices carry currency symbols, thousands separators, and European decimal commas that fail numeric coercion. Cast them in SQL later, where a bad value stays visible instead of being silently dropped.

3.2 Call Unsiloed from a UDF

Append this below the configuration. The extraction is an ordinary Python function wrapped in @udf, which turns it into something Spark can apply to every row. It runs in three phases: check the document, submit it, then poll until the job finishes. The three blocks below are one continuous function, so paste them one after the other.

Reject documents before spending a call

An empty file or one over the size limit can never extract, so the function returns an error for those immediately rather than paying for a request that is certain to fail.
my_transformation.py
Both guards return the same _error shape used everywhere else, which is what puts the document in the extraction_errors table later.

Submit the document

The mimetypes lookup sets the content type from the file extension, so the same function handles PDFs, images, and Office documents without special cases. A requests.Session reuses one connection for the submit and every poll that follows.
my_transformation.py
Checking submit.ok rather than calling raise_for_status() keeps a rejected upload as a returned error instead of an exception, so the document lands in extraction_errors with the HTTP status and response body attached.

Poll until the job finishes

Extraction is asynchronous, so the submit only returns a job_id. This loop asks for the result until it arrives or the deadline passes.
my_transformation.py
The deadline is measured with time.monotonic(), which doesn’t jump if the cluster clock changes, and every timeout is clamped to what’s left of it so a slow response can’t run past the budget. A 429 means Unsiloed is rate limiting, so the loop waits and retries rather than treating it as a failure. Two more things about the function as a whole:
  • enable_citations must be true. With citations off, the alpha, beta, and delta tiers return a legacy flat shape ({"value": ..., "score": <number>}) with no citation, and the extracted_fields table would have nothing to read.
  • The UDF is marked nondeterministic. Unsiloed scores can vary between calls, so this stops Spark optimizing the function as though the same input always gives the same output.

3.3 The extractions Table

Append this below the UDF. The spark.readStream call with the cloudFiles format is Auto Loader. It tracks committed files, so later runs normally pick up only new document paths. repartition bounds the number of partitions that can submit documents concurrently.
my_transformation.py
The binaryFile format gives us the raw bytes of each document plus its path. We derive file_name from the path, hand both to the UDF, and store the response with try_parse_json so it lands as a VARIANT column. That last part matters. A VARIANT column has no fixed shape, so adding a field to FIELDS later needs no migration.

3.4 The Output Tables

Append this at the end of the file. The extractions table is one row per document, which is awkward for finding the values you should check. The next table pivots successful results to one row per field, while the final table keeps failed documents separate.
my_transformation.py
The variant_explode function walks whatever is inside result without naming a single field, so extracted_fields keeps working unchanged when you revise FIELDS. Each successful row includes the value, both confidence scores, and the full citation geometry when Unsiloed could ground the value. extraction_errors keeps API and file failures out of confidence-review queries. That’s the whole file. Compare it against the full pipeline above if you want to check you have every piece in the right order.

Step 4: Create and Run the Pipeline

With the code written, the rest happens in the Databricks UI. Creating an ETL pipeline gives you a starter file to paste it into.
1

Create an ETL pipeline

In the sidebar, click Jobs & Pipelines, then click ETL pipeline under Create new. Databricks creates the pipeline immediately and opens its editor, with a Python file already in place.
The Jobs and Pipelines page with the ETL pipeline card highlighted under Create new
2

Set the default location

The editor opens with an empty starter file at transformations/my_transformation.py.Before anything else, click the catalog and schema shown at the top right to open the Default location panel, and set Default catalog and Default schema to match your volume (workspace and unsiloed). The schema defaults to default, so if you leave it there your tables land in the wrong place.
The pipeline editor with the starter transformation file, the catalog and schema selector, and the Run pipeline button highlighted
3

Paste in the code and run

Open transformations/my_transformation.py. The file is empty, so the grey text and the Create with Genie Code and Use sample code buttons are editor prompts rather than file content, and there is nothing to delete. Paste in the file you assembled in Step 3, then click Run pipeline.The first run starts compute, reads every document in the volume, and creates all three tables. Processing time depends on document length, model load, and the concurrency limit. Databricks submits up to four partitions in parallel with the configuration used here.

Step 5: Query the Results

All three outputs are ordinary Delta tables, so query them from the SQL editor or any connected tool:
That returns one row per document:
A grounded field arrives as an object carrying its value, both scores, and the region it came from:
The bbox array is [x0, y0, x1, y1] in points, measured against the page_width and page_height in the same object, so you can scale it to whatever size you render the page at.

Review the Uncertain Values

The extracted_fields table gives one row per field, so the values worth checking sort to the top:
Query results in the SQL editor showing extracted fields sorted by confidence, with the lowest extraction score highlighted
The two scores mean different things:
  • extraction_score: confidence in the value that was read
  • grounding_score: confidence that the value was located in the document, at the region the citation points to
They come apart usefully. A value the model inferred rather than read off the page scores high on extraction and low on grounding, which is exactly what a human should confirm. If you ask for a normalized value, such as an ISO currency code when the invoice prints a symbol, grounding collapses toward zero on every document, because there is no literal text to cite. Ambiguity in the document is one cause worth knowing about, because it catches people out. An invoice printing both Subtotaal €717,97 (including VAT) and Exclusief BTW €593,36 will score its subtotal low whichever one it picks, even when the value is correct. So treat a flagged value as worth checking rather than wrong, and note the page it came from.
Scores are not deterministic. Re-extracting the same document with the same schema can return a different score. Use them to rank and triage, not as a fixed threshold to assert against.

Add More Documents

Upload another file under a new path and run the pipeline again. Auto Loader tracks what it has already committed, so the normal case extracts only the new document and existing rows keep their original extracted_at:
Open Schedule on the pipeline and set a trigger. A run with no newly discovered files doesn’t intentionally submit work to Unsiloed, although Databricks compute and task retries still have their own cost implications. Auto Loader ignores overwritten files by default. To process a corrected document, upload it under a new path or run a full refresh. To re-extract everything after changing FIELDS, open the Run pipeline menu and select Run pipeline with full table refresh.

Troubleshoot the Pipeline

Most problems on a first run come from the schema selector or from network access rather than from the code itself.
The Default schema in the pipeline’s Default location panel is default until you change it, so tables land there instead. Set it to the schema you want before the first run, or change it in Settings and run a full refresh.
You changed a table between a materialized view and a streaming table, for example by removing spark.readStream. Drop the existing table and run again.
Check the volume path in VOLUME, and that the files are really there. Also avoid eager actions such as spark.read.table("...").collect() inside a table function. Those run while Databricks plans the pipeline graph, before any table holds data, so they return nothing and give no error.
The row still lands in extractions, the error is also written to extraction_errors, and the rest of the batch continues. Query the error table:
The error includes the HTTP status or job failure detail returned by Unsiloed. If one document fails repeatedly while its siblings succeed, verify its format and open it locally before retrying.
Pipeline compute needs outbound access to prod.visionapi.unsiloed.ai. Some workspaces restrict serverless egress at the account level. If requests time out or fail DNS resolution, ask your account admin about the serverless network policy.

What to Know Before Running at Scale

Five things are worth settling before you increase the file or concurrency limits:
  • Billing is per page processed. A full refresh re-extracts every document in the volume, so check what that represents before you trigger one.
  • Model choice changes cost and accuracy. The gamma tier used here is the thorough one and suits most work. Use delta for complex contracts and dense tables, and alpha when speed matters more than accuracy on clean documents.
  • Documents leave your workspace. The pipeline sends each file over HTTPS to prod.visionapi.unsiloed.ai. Confirm that’s acceptable for your data before running this at scale.
  • External submissions are not exactly once. Auto Loader makes the committed Delta output incremental, but Spark can retry an HTTP side effect. Use a durable, idempotent submission service when duplicate jobs are unacceptable.
  • Files are loaded into UDF memory. The 50 MB limit in this guide leaves room for Spark, Python, and multipart-upload copies. Raise it only after testing the memory behavior of your pipeline environment.

See Also

Extract Quickstart

The /v2/extract call on its own, with schema options explained.

Extraction API Reference

Every parameter, model tier, and the full response format.