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.
What We’ll Build
An ETL pipeline with three tables:extractions: one row per document, holding the complete Unsiloed result object in aVARIANTcolumnextracted_fields: one row per successfully extracted field, including its confidence scores and citationextraction_errors: one row per document that could not be extracted
Show the Full Pipeline
Show the Full Pipeline
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
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
Enter 
<your-workspace> with your own workspace URL: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.
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 
Drop your files in and click Upload. Any mix of PDFs, images, and Office
documents works.
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.

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. Thepyspark.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
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.
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
_error shape used everywhere else, which is what
puts the document in the extraction_errors table later.
Submit the document
Themimetypes 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
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 ajob_id. This loop asks
for the result until it arrives or the deadline passes.
my_transformation.py
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_citationsmust betrue. With citations off, thealpha,beta, anddeltatiers return a legacy flat shape ({"value": ..., "score": <number>}) with no citation, and theextracted_fieldstable 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
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. Theextractions 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
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.

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.
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: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
Theextracted_fields table gives one row per field, so the values worth checking
sort to the top:

extraction_score: confidence in the value that was readgrounding_score: confidence that the value was located in the document, at the region the citation points to
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.
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 originalextracted_at:
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 tables landed in the wrong schema
The tables landed in the wrong schema
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.'CANNOT_CHANGE_DATASET_TYPE' when running the pipeline
'CANNOT_CHANGE_DATASET_TYPE' when running the pipeline
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.The pipeline succeeds but the table is empty
The pipeline succeeds but the table is empty
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.A document failed
A document failed
The row still lands in 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.
extractions, the error is also written to
extraction_errors, and the rest of the batch continues. Query the error table:The extraction cannot reach Unsiloed
The extraction cannot reach Unsiloed
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
gammatier used here is the thorough one and suits most work. Usedeltafor complex contracts and dense tables, andalphawhen 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.

