Metadata Expansion
Extract structured metadata fields, such as author or title, from documents already in your project.
Metadata Expansion pulls structured metadata out of documents you already uploaded to a
project through POST /v1/files. You define which fields to extract, for example author,
title, or any custom field, and DeepFellow returns a value for each one per document.
DeepFellow reads embedded file properties first. For anything it cannot find that way, it falls back to an LLM. Extraction runs asynchronously on the Nightshift background worker: you start a job, then poll it until it finishes.
Metadata Expansion requires RabbitMQ and a running Nightshift worker. Without them, no document is processed. See Configuration.
Why Use Metadata Expansion
Imagine that your accounting team uploads several hundred supplier invoices to a project as pdf and docx
files. To match each invoice against a payment, you need its vendor name, invoice number,
issue date, and total amount as structured fields rather than free text.
You start a single Metadata Expansion job with the following fields:
| Field | description |
|---|---|
author | null |
vendor_name | The name of the company that issued the invoice. |
invoice_number | The invoice or reference number printed on the document. |
issue_date | The date the invoice was issued, in YYYY-MM-DD format. |
total_amount | The total amount due, including currency. |
DeepFellow fills author from each file's embedded properties where present, and asks the LLM
for the remaining fields. When the job completes, each invoice's extracted_metadata holds the
values, and your payment-matching script reads them through GET /v1/files/{file_id}. An invoice
that does not show a value, for example a missing issue date, returns null for that field
instead of a guessed value.
How Extraction Works
DeepFellow extracts each field in two steps.
- Embedded properties. For
pdf,docx, andpptxfiles, DeepFellow reads the file's own metadata directly, without calling an LLM. This step fills only fields namedauthor,title, orsubject, matched case-insensitively. - LLM fallback. For any field the first step could not fill, including every field on
other file types, DeepFellow extracts the document text with Doc Chunker and
asks an LLM to extract the remaining fields. The LLM returns
nullfor a field it cannot find rather than guessing.
DeepFellow merges the results of both steps into a single extracted object per document,
keyed by field name.
For a document longer than 20,000 characters, the LLM receives a sample instead of the full text: the first 12,000 characters and the last 8,000 characters, with a marker in place of the omitted middle. Fields such as author or title usually appear in the front matter or the back matter, so the sample covers both. A value that appears only in the middle of a long document will not reach the LLM.
DeepFellow caches each LLM result for 30 days. The cache key covers the model, the sampled text,
and the requested fields with their descriptions. A later job that sends the same request reuses
the cached result instead of calling the LLM again. Changing the model, a field, or a field
description produces a new request.
Start a Metadata Expansion Job
To start a job, send the IDs of documents already uploaded to the project, along with the
fields to extract, to POST /v1/metadata/expand.
curl -X 'POST' \
'https://deepfellow-server-host/v1/metadata/expand' \
-H "Authorization: Bearer DEEPFELLOW_PROJECT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"document_ids": ["68a1c2f0e4b0f1a2b3c4d5e6"],
"fields": [
{"name": "author", "description": null},
{"name": "invoice_number", "description": "The invoice or reference number printed on the document."}
]
}'import requests
response = requests.post(
"https://deepfellow-server-host/v1/metadata/expand",
headers={"Authorization": "Bearer DEEPFELLOW_PROJECT_API_KEY"},
json={
"document_ids": ["68a1c2f0e4b0f1a2b3c4d5e6"],
"fields": [
{"name": "author", "description": None},
{"name": "invoice_number", "description": "The invoice or reference number printed on the document."},
],
},
)
print(response.json())const response = await fetch('https://deepfellow-server-host/v1/metadata/expand', {
method: 'POST',
headers: {
Authorization: 'Bearer DEEPFELLOW_PROJECT_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
document_ids: ['68a1c2f0e4b0f1a2b3c4d5e6'],
fields: [
{ name: 'author', description: null },
{ name: 'invoice_number', description: 'The invoice or reference number printed on the document.' },
],
}),
});
console.log(await response.json());Each field takes a name and an optional description. Write a clear description for
fields that do not map to a common document property, since DeepFellow passes it to the LLM as
extraction guidance when the field is not found among the document's embedded properties.
The request also takes an optional image_processing_mode, which applies to every document in
the job. Set it to description to have a vision model describe images when DeepFellow converts
a document for the LLM step. The default value, ignore, skips image descriptions. See
Doc Chunker for how the conversion cache affects this setting.
The response returns the created job, with every document in pending status.
{
"id": "68a1c3a1e4b0f1a2b3c4d5e7",
"project_id": "689f...",
"status": "pending",
"fields": [
{"name": "author", "description": null},
{"name": "invoice_number", "description": "The invoice or reference number printed on the document."}
],
"documents": [
{
"document_id": "68a1c2f0e4b0f1a2b3c4d5e6",
"status": "pending",
"extracted": null,
"error": null,
"attempts": 0
}
],
"describe_images": false,
"created_at": 1798800000,
"finished_at": null
}Request Limits
DeepFellow validates every new job against the following limits:
| Limit | Value |
|---|---|
| Documents per job | From 1 to 1000, each ID unique |
| Fields per job | From 1 to 50, each name unique |
Field name | Letters, digits, underscores, and hyphens only (^[A-Za-z0-9_-]+$) |
Field description | At most 500 characters |
A request that breaks any of these limits, or that contains a document ID that does not exist in
the project, returns 400 Bad Request. If DeepFellow Server is not connected to RabbitMQ, the
request returns 503 Service Unavailable with a Retry-After header.
Check Job Status
Poll GET /v1/metadata/jobs/{job_id} until status reaches completed, failed, or
cancelled. A job that does not exist in the project returns 404 Not Found.
Job status | Meaning |
|---|---|
pending | DeepFellow queued the job. No document started processing yet. |
processing | At least one document is being processed. |
paused | You paused the job. Its documents stop advancing until you resume it. |
completed | Every document reached a final state, and at least one of them completed. Some documents still show failed. |
failed | Every document in the job failed. |
cancelled | You cancelled the job. It never resumes. |
Each entry in documents reports its own state independently of the job.
Document status | Meaning |
|---|---|
pending | Queued, not started yet. |
processing | DeepFellow is extracting metadata for this document. |
completed | Extraction finished. extracted holds the result. |
failed | Extraction failed after metadata_expansion_max_attempts attempts, or on an error that a retry will not fix. error holds the reason. |
A retry does not fix every error, so DeepFellow marks the document failed right away, without
spending further attempts, in the following cases:
- Doc Chunker rejects the file as too large, unsupported, or unprocessable
- The LLM backend rejects the request, for example because the context is too long
- The LLM response exceeds its output token limit
Results on the File
DeepFellow also writes each completed document's results to the extracted_metadata field of the
file, so GET /v1/files/{file_id} returns them without a reference to the job. A later job for
the same file overwrites the fields it requests. A field the later job returns as null is
removed from extracted_metadata. Fields the later job does not request keep their values.
Search by Extracted Metadata
Vector store search does not read extracted_metadata. Its filters match only the attributes
of a file in the vector store. To search by an extracted value, copy it into the file's attributes,
then filter on it.
- Read the extracted values from
GET /v1/files/{file_id}. - Send the values you want to search by to
POST /v1/vector_stores/{vector_store_id}/files/{file_id}. DeepFellow merges them into the file's existing attributes and updates every vector of the file.
curl -X 'POST' \
'https://deepfellow-server-host/v1/vector_stores/VECTOR_STORE_ID/files/68a1c2f0e4b0f1a2b3c4d5e6' \
-H "Authorization: Bearer DEEPFELLOW_PROJECT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"attributes": {"vendor_name": "Acme Corp", "invoice_number": "INV-2041"}
}'import requests
base_url = "https://deepfellow-server-host/v1"
headers = {"Authorization": "Bearer DEEPFELLOW_PROJECT_API_KEY"}
file_id = "68a1c2f0e4b0f1a2b3c4d5e6"
extracted = requests.get(f"{base_url}/files/{file_id}", headers=headers).json()["extracted_metadata"]
response = requests.post(
f"{base_url}/vector_stores/VECTOR_STORE_ID/files/{file_id}",
headers=headers,
json={"attributes": {key: extracted[key] for key in ("vendor_name", "invoice_number") if key in extracted}},
)
print(response.json())const baseUrl = 'https://deepfellow-server-host/v1';
const headers = { Authorization: 'Bearer DEEPFELLOW_PROJECT_API_KEY', 'Content-Type': 'application/json' };
const fileId = '68a1c2f0e4b0f1a2b3c4d5e6';
const file = await (await fetch(`${baseUrl}/files/${fileId}`, { headers })).json();
const { vendor_name, invoice_number } = file.extracted_metadata;
const response = await fetch(`${baseUrl}/vector_stores/VECTOR_STORE_ID/files/${fileId}`, {
method: 'POST',
headers,
body: JSON.stringify({ attributes: { vendor_name, invoice_number } }),
});
console.log(await response.json());- Search the vector store with a filter on the attribute.
curl -X 'POST' \
'https://deepfellow-server-host/v1/vector_stores/VECTOR_STORE_ID/search' \
-H "Authorization: Bearer DEEPFELLOW_PROJECT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"query": "payment terms",
"filters": {"key": "vendor_name", "type": "eq", "value": "Acme Corp"}
}'Only chunks of files whose vendor_name attribute equals Acme Corp appear in the results. To
combine conditions, use a compound filter with type set to and or or. See
Using Vector Stores.
File attributes have the following limits:
- A file holds at most
16attributes. A request that would exceed this returns400 Bad Request. - Keys are at most
64characters, and string values at most512characters. - While DeepFellow is still ingesting the file, the request returns
409 Conflict. Retry after ingestion finishes.
Attributes do not follow later changes to extracted_metadata. After you run a new Metadata
Expansion job for a file, copy the values again.
Pause, Resume, Retry, and Cancel
POST /v1/metadata/jobs/{job_id}/pausestops the job's documents from advancing. A document already inprocessingfinishes its current step, then returns topending. Pausing acompleted,failed, orcancelledjob returns409 Conflict.POST /v1/metadata/jobs/{job_id}/resumere-queues everypendingdocument of a paused job. Resuming a job that is not paused returns409 Conflict.POST /v1/metadata/jobs/{job_id}/retryresets the job'sfaileddocuments topendingand re-queues them, each with a fresh attempt budget. Retrying a paused job, a cancelled job, or a job with no failed documents returns409 Conflict.POST /v1/metadata/jobs/{job_id}/cancelstops the job permanently and sets itsstatustocancelled. Cancelling acompleted,failed, orcancelledjob returns409 Conflict.
Every endpoint returns the updated job, or 404 Not Found if the job does not exist in the
project. Resume and retry return 503 Service Unavailable if DeepFellow Server is not connected to
RabbitMQ.
You cannot undo a cancellation. To stop a job temporarily, pause it instead.
Configuration
Metadata Expansion jobs run only on the Nightshift worker, which reads them
from the metadata_expansion RabbitMQ queue. RabbitMQ and the Nightshift worker are mandatory:
there is no in-process fallback. Point the server at your RabbitMQ instance as described in
RabbitMQ Connection, then run the worker alongside the
server as described in Run Nightshift. The DeepFellow Server
installer sets up both.
The following settings are dynamic runtime config, editable through GET/PUT /admin/config
without a restart. The Nightshift worker picks up a change and reloads itself. See
Configuration Reload.
| Setting | Description | Default |
|---|---|---|
metadata_expansion_model | Model used for the LLM fallback step. | gemma3:1b |
metadata_expansion_max_attempts | Number of times DeepFellow retries a document before marking it failed. | 3 |
metadata_expansion_concurrency | Number of documents each Nightshift worker processes at once, from 1 to 64. | 4 |
We use cookies on our website. We use them to ensure proper functioning of the site and, if you agree, for purposes such as analytics, marketing, and targeting ads.
Nightshift Background Jobs
The background worker that processes vector store files, Metadata Expansion, Document Summarization, Knowledge Graph, webhook, Fine-Tuning Pipelines, and migration jobs.
Document Summarization
Batch-summarize your uploaded documents, with configurable length, style, and language.