DeepFellow DOCS

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:

Fielddescription
authornull
vendor_nameThe name of the company that issued the invoice.
invoice_numberThe invoice or reference number printed on the document.
issue_dateThe date the invoice was issued, in YYYY-MM-DD format.
total_amountThe 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.

  1. Embedded properties. For pdf, docx, and pptx files, DeepFellow reads the file's own metadata directly, without calling an LLM. This step fills only fields named author, title, or subject, matched case-insensitively.
  2. 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 null for 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:

LimitValue
Documents per jobFrom 1 to 1000, each ID unique
Fields per jobFrom 1 to 50, each name unique
Field nameLetters, digits, underscores, and hyphens only (^[A-Za-z0-9_-]+$)
Field descriptionAt 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 statusMeaning
pendingDeepFellow queued the job. No document started processing yet.
processingAt least one document is being processed.
pausedYou paused the job. Its documents stop advancing until you resume it.
completedEvery document reached a final state, and at least one of them completed. Some documents still show failed.
failedEvery document in the job failed.
cancelledYou cancelled the job. It never resumes.

Each entry in documents reports its own state independently of the job.

Document statusMeaning
pendingQueued, not started yet.
processingDeepFellow is extracting metadata for this document.
completedExtraction finished. extracted holds the result.
failedExtraction 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.

  1. Read the extracted values from GET /v1/files/{file_id}.
  2. 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());
  1. 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 16 attributes. A request that would exceed this returns 400 Bad Request.
  • Keys are at most 64 characters, and string values at most 512 characters.
  • 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}/pause stops the job's documents from advancing. A document already in processing finishes its current step, then returns to pending. Pausing a completed, failed, or cancelled job returns 409 Conflict.
  • POST /v1/metadata/jobs/{job_id}/resume re-queues every pending document of a paused job. Resuming a job that is not paused returns 409 Conflict.
  • POST /v1/metadata/jobs/{job_id}/retry resets the job's failed documents to pending and re-queues them, each with a fresh attempt budget. Retrying a paused job, a cancelled job, or a job with no failed documents returns 409 Conflict.
  • POST /v1/metadata/jobs/{job_id}/cancel stops the job permanently and sets its status to cancelled. Cancelling a completed, failed, or cancelled job returns 409 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.

SettingDescriptionDefault
metadata_expansion_modelModel used for the LLM fallback step.gemma3:1b
metadata_expansion_max_attemptsNumber of times DeepFellow retries a document before marking it failed.3
metadata_expansion_concurrencyNumber 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.