Skip to content

Extract the data of one form

Many PDFs that arrive in a document system are forms: the invoices of a supplier, the delivery notes of a warehouse, the freight invoices of a carrier. Each issuer prints its documents with the same layout. The labels and their positions stay the same, and the values change. This course configures the form extraction of the AI Pack for such documents. ADM reads their values into ADM annotations and into a table of your application. It sends a document to a person only when the result fails a check.

A language model does the reading. The course uses the model gpt-5.6-luna of OpenAI, and the model is a setting. You can use another provider, including an Ollama server that you operate. The whole pipeline is part of ADM. You can configure it in two ways, and the lessons show both:

  • The form extraction manager is the page Form Extraction in the ADM application. You fill in forms and press buttons.
  • SQL calls the packages adm_ai_form_config_api (configuration) and adm_ai_form_extract_api (extract, measure, approve) from scripts.

Both ways write the same configuration and give the same results. At each step that configures, tests or measures, the lesson has the two tabs Manager and SQL. Choose one tab. The page remembers your choice for every lesson of the course. Steps that exist only in SQL, such as loading the sample files, are not in tabs.

The course has six lessons:

  1. Extract the data of one form (this lesson).
  2. Check and measure a form: build a test set, measure the form, and add check expressions and field patterns.
  3. Add a form: the steps that you repeat for each of your own forms.
  4. Read images and choose the models for your setup.
  5. Extract the new documents of an inbox folder in the background, copy the accepted invoices into your own table, and approve the documents that need a person.
  6. Apply the workflow to other documents: a summary of a test with five other types, and a checklist for your own forms.

The configuration has three levels. Each level answers one question about a document:

Level Question What it holds Examples in this course
Profile Which documents, and which model? The source folder, the models, and the rules for every form pfx_supplier_forms
Document type Which data comes out? The JSON schema of the fields, the check expressions, the fields that become annotations, and the rules for every form of this type invoice, delivery_note
Form How does one issuer print it? The markers, a description, the field patterns, the line pattern, the number format, and the rules for this layout HARBOUR_INVOICE, NORDWIND_INVOICE, HARBOUR_DELIVERY_NOTE

A profile contains document types and forms. Each form belongs to one document type of the same profile. After lesson 3, the profile of the course has this structure:

profile pfx_supplier_forms folder /users/admin/pfx_samples, model gpt-5.6-luna
├─ document type invoice fields: invoice_number, net_amount, total_amount, line_items, ...
│ ├─ form HARBOUR_INVOICE German number format, a line pattern reads the lines
│ └─ form NORDWIND_INVOICE English number format, the model reads the lines
└─ document type delivery_note fields: delivery_note_number, total_quantity, line_items, ...
└─ form HARBOUR_DELIVERY_NOTE

The figure shows one sample document of each form, with the form and its document type below it:

An invoice of Harbour Office Supplies, a freight invoice of Nordwind Logistics and a delivery note of Harbour Office Supplies
Select the image to open it in full size.

The levels separate what changes from what stays the same. An invoice of Harbour and an invoice of Nordwind look different on the page. Your application needs the same data from both: the invoice number, the amounts and the lines. So the fields and the arithmetic checks belong to the document type, and you write them once for all invoices. The layout belongs to the form: how ADM recognizes the issuer, where the customer is on the page, and how the numbers are printed. Harbour also sends delivery notes. A delivery note returns other fields than an invoice, so it is a second document type, with its own form.

To extract a document, ADM goes down the levels:

  1. The profile selects the document, because the document is in the folder of the profile.
  2. The markers of the forms find the one form that matches the document.
  3. The form names its document type. The document type gives the JSON schema of the answer and the checks.
  4. The model gets the rules of all three levels: first the rules of the profile, then of the document type, then of the form.

Lesson 1 creates one item of each level: the profile, the document type invoice and the form HARBOUR_INVOICE. Lesson 3 adds the form NORDWIND_INVOICE, the document type delivery_note and its form HARBOUR_DELIVERY_NOTE.

The diagram shows the steps for one document, and the results that are possible:

document in the folder of the profile
│
├─ PDF with text ─ the markers of a form match the text ─────────┐
│ no form matches, or more than one ──▶ REVIEW │
├─ PDF without text (a scan) ──▶ REVIEW │
└─ image ─ a vision model picks the form ────────────────────────┤
▼
the model reads the fields, with the rules of the form
a line pattern reads long line tables, without the model
▼
checks: field patterns of the form, check expressions of the document type
├─ every check passes ──▶ OK: annotations in ADM, and your hook
└─ a check fails ──────▶ REVIEW: a tag for a person, who approves the corrected values
  • A JSON schema lists the fields that the answer of the model must have, with their types. The model then returns named values instead of a paragraph.
  • A marker is a regular expression that only the documents of one form match, for example the VAT ID of the issuer.
  • A rule is one sentence of the system prompt. The system prompt tells the model its task before it reads the document.
  • The status of a result is OK when every check passed, REVIEW when a person must look at the document, and FAILED when an error stopped the extraction. Lesson 5 adds APPROVED.
  • An annotation is a key and a value that ADM stores with a document. Programs read annotations. A tag is a label that people see on a document in the file browser.
  • UC AI is the PL/SQL library of the AI Pack that calls language models.
  • A token is a unit of text that the model counts. The context is the number of tokens that the model can hold at one time: the prompt and the answer together.

The Form extraction guide is the reference for every configuration key that the course uses. The Form extraction manager guide describes every part of the manager.

Use a test schema with ADM and the AI Pack. You need these items:

  • The course scripts. Get the folder examples/pdf-field-extraction from United-Codes/adm or from your delivery archive. Connect SQLcl or SQL*Plus as the ADM schema owner, and run every script from that folder. You need the scripts for the manager path too: 00_load_samples.sql loads the sample files.
  • A model that the database can call. The course uses gpt-5.6-luna of OpenAI through an APEX web credential with the static ID OPENAI. The credential holds your API key. In the APEX workspace of your schema, go to Workspace Utilities > Web Credentials. Create a credential of the type HTTP Header with the header Authorization and the value Bearer <your key>. The box “Use another provider” below lists what to change for other providers.
  • An ADM administrator. The scripts act as the ADM user in settings.sql, ADMIN by default, who needs a home folder in ADM. The manager is for administrators too. Log in to the ADM application with an administrator account.
  • A DBA, for an on-premises database. A DBA must allow the schema to connect to the host of the provider, api.openai.com for OpenAI, and the certificate chain of the host must be in the wallet of the database. A missing network permission gives ORA-24247. A missing certificate gives ORA-29024. The UC AI guide Network Setup (ACL & Wallet) lists the steps.

All values that you can change are in settings.sql, and every script reads them from there:

define pfx_user = ADMIN
define pfx_provider = openai
define pfx_text_model = 'gpt-5.6-luna'
define pfx_vision_model = 'gpt-5.6-luna'
define pfx_credential = 'OPENAI'
Use another provider

Change pfx_provider, pfx_text_model, pfx_vision_model and pfx_credential in settings.sql before you run 01_create_profile.sql. pfx_provider is a provider name of UC AI, for example openai, anthropic, google, mistral or ollama. The vision model must be able to read images. In the manager, you choose the same values in the section How documents are read.

For ollama, settings.sql also uses pfx_base_url, the address of the Ollama API without /chat, for example http://localhost:11434/api. Leave pfx_credential empty for a server without authentication. A server behind a proxy that asks for a key needs a credential of the type HTTP Header. For this provider, the profile gets the options temperature 0 and a context of 16,384 tokens. Ollama cuts a prompt that is longer than its context without an error, and its server default can be as low as 4,096 tokens. In the manager, enter the address as Server address under Advanced settings of the text model, and the options in Other provider settings (JSON):

{"g_extra_body": {"options": {"temperature": 0, "num_ctx": 16384}}, "ollama": {"g_use_responses_api": false}}

Lesson 4 shows how to size the context and how to compare models.

The recordings of this course come from SQLcl and the manager on Oracle AI Database 26ai Free (23.26.3), APEX 26.2 and UC AI 26.4, on 2026-11-04. The model gpt-5.6-luna ran at OpenAI through the web credential OPENAI. Document IDs, timings and token counts differ in your run, and a model can return another answer for the same document.

Run the lessons in order and in one schema, because each lesson changes the configuration that the next lesson uses.

The samples come from two invented issuers. Harbour Office Supplies GmbH sends invoices, credit notes and delivery notes with the German number and date format, for example 1.234,56 and 14.03.2026. Nordwind Logistics B.V. sends freight invoices with the English number format and dates such as 14 Mar 2026. Lessons 1 and 2 use the Harbour invoices, and lessons 3 and 4 add the other files.

Files Form What the files add
harbour-invoice-01 to -13 Harbour invoice Ambiguous dates, another delivery address, notes with other numbers, large amounts, a discount, a credit note, a customer name on two lines, and 31 and 120 lines on several pages. File 12 is a scan, as a PDF without text and as an image.
nordwind-invoice-01 to -05 Nordwind invoice Another layout of the same document type, reverse-charge VAT, no order number. File 05 is a photo.
harbour-delivery-01 to -03 Harbour delivery note Another document type from the same issuer, with ordered, delivered and open quantities.
harbour-letter-prices.pdf none A letter from Harbour: the letterhead of the forms, but no form.

Set the values in settings.sql. Then run @00_load_samples.sql. It loads the 23 sample files into the folder pfx_samples in the home folder of the ADM user. A file that is already there stays unchanged.

loaded: nordwind-invoice-04-large.pdf
loaded: nordwind-invoice-05-photo.png
samples loaded: 23 new file(s) in /users/admin/pfx_samples

The profile names the folder with the documents, the model, and the rules for every form. These are its parts:

  • Source folder. The profile reads the documents in this folder and below it. The path is the home folder of the ADM user, for example /users/admin, plus pfx_samples.
  • Text model. It has a provider, a model name and the static ID of the APEX web credential. The model reads the text of a PDF. The credential holds the API key, so the profile never contains the key.
  • Rules. Each rule is one sentence of the system prompt. The rules of the profile apply to every form.
  1. Log in to the ADM application as an administrator and open the page Form Extraction (under Administration). Choose Create.

  2. In the section Name, enter the Profile name pfx_supplier_forms. For Description, enter Course "Extract data from PDF forms": forms of Harbour Office Supplies and Nordwind Logistics.

  3. In the section Which documents, enter the Folder /users/admin/pfx_samples. Use the home folder of the user in settings.sql, or choose Browse… and select pfx_samples.

  4. In the section How documents are read, under Text model, choose the Provider OpenAI, enter the Model gpt-5.6-luna, and enter the APEX web credential OPENAI. Leave the vision model and the OCR switches off.

  5. In the section Instructions for the model, choose Add rule three times. Enter one of these rules in each field:

    Return dates as YYYY-MM-DD.
    If a value is not in the document, return null. Never guess.
    Take every value from this document. Numbers in notes or in the payment text that refer to other documents are not values of this document.
  6. Leave the other sections unchanged and choose Create profile.

The form New profile of the form extraction manager, filled in with the name, the folder, the OpenAI model and three rules
Select the image to open it in full size.

The page of the profile opens. It shows the source, the model and a tab for each part of the profile. The tab Document types & forms is where the next two steps happen.

The page of the profile pfx_supplier_forms with the tabs Results, Document types & forms, Test set and Measurements
Select the image to open it in full size.

The keys of the configuration have these meanings:

  • source_folder_path limits the profile to the documents in this folder and below it.
  • text_model names the provider and the model. Its config goes unchanged to UC AI: the web credential, and for some providers the base URL and the options of the request.
  • rules apply to every form of the profile.

The document type invoice names the data that ADM takes out of every invoice, from any issuer. It does this with a JSON schema. A JSON schema is a JSON document that describes another JSON object: the names of its fields, the type of each value, and the fields that must be present.

The figure shows two invoices from two issuers. The boxes mark the values that the schema asks for. Each issuer prints them at another place and in another format, for example 14.03.2026 and 14 Mar 2026, or 468,60 and 589.60. The schema gives each value one name and one format, for every form of the document type.

The fields of the document type invoice, marked on an invoice of Harbour Office Supplies and a freight invoice of Nordwind Logistics
Select the image to open it in full size.

ADM uses the schema at each step of the extraction:

  1. The request to the model. ADM sends the schema to the model together with the system prompt and the text of the document. The model must answer with one JSON object that has the fields of the schema, with values of the correct types. This is called structured output. Without a schema, a model answers with sentences, and a program cannot read the values reliably.
  2. The result. The answer of the model is the result of the extraction. ADM keeps its fields, and the line items separately.
  3. The checks. The check expressions of lesson 2 use the names of the fields, for example net_amount + vat_amount = total_amount.
  4. The publication. annotation_fields selects the fields that become annotations of the document. The hook of lesson 5 reads the fields by the same names and copies them into a table of your application.

These are the fields of the document type invoice:

Field Type Value that the model returns
document_type invoice or credit_note The kind of document. The schema allows only these two texts.
invoice_number text The number of the invoice, as printed.
invoice_date text The date of the document as YYYY-MM-DD. The model converts 14.03.2026 and 14 Mar 2026 to 2026-03-14.
due_date text or null The due date as YYYY-MM-DD, or null. A credit note has no due date.
customer_number text The number of the customer at the issuer.
customer_name text The company name of the customer who receives the invoice.
order_reference text or null The order number of the customer, or null when the invoice has none.
discount_amount number The discount in the totals block as a positive number, 0 when there is none.
net_amount, vat_rate, vat_amount, total_amount number The totals, and the VAT rate in percent.
line_items array of objects One object for each line of the table, with item_number (text), and quantity, unit_price and amount (numbers).

This is the configuration of the document type, written as a JSON schema with two more keys:

{
"json_schema": {
"type": "object",
"properties": {
"document_type": {"type": "string", "enum": ["invoice", "credit_note"]},
"invoice_number": {"type": "string"},
"invoice_date": {"type": "string", "description": "Date of the document as YYYY-MM-DD"},
"due_date": {"type": ["string", "null"], "description": "Due date as YYYY-MM-DD, null if there is none"},
"customer_number": {"type": "string", "description": "The number of the customer at the issuer"},
"customer_name": {"type": "string", "description": "Company name of the customer who receives the invoice"},
"order_reference": {"type": ["string", "null"], "description": "Order number of the customer, null if there is none"},
"discount_amount": {"type": "number", "description": "Discount in the totals block as a positive number, 0 if there is none"},
"net_amount": {"type": "number"},
"vat_rate": {"type": "number", "description": "VAT rate in percent, for example 19"},
"vat_amount": {"type": "number"},
"total_amount": {"type": "number"},
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"item_number": {"type": "string"},
"quantity": {"type": "number"},
"unit_price": {"type": "number"},
"amount": {"type": "number"}
},
"required": ["item_number", "quantity", "unit_price", "amount"]
}
}
},
"required": ["document_type", "invoice_number", "invoice_date", "due_date", "customer_number",
"customer_name", "order_reference", "discount_amount", "net_amount", "vat_rate",
"vat_amount", "total_amount", "line_items"]
},
"annotation_fields": ["invoice_number", "customer_number", "total_amount"],
"rules": [
"The issuer of the invoice is never the customer.",
"A credit note has no due date: return null. Its amounts are negative; keep the sign."
]
}

The schema uses these keywords:

  • type is the type of a value. "number" means a JSON number, not a text. The model must convert 1.234,56 to 1234.56, which is why each form has a rule for its number format. ["string", "null"] allows a text or null.
  • description is an instruction to the model for one field, for example the date format. Keep it short. A rule that is true for several fields belongs in rules.
  • enum lists the only values that the field can have.
  • required lists the fields that the answer must contain. All fields are required, so a value that is not on the page comes back as null, not as a missing field. ADM checks this before it accepts a result: a missing field, or null in a field whose type does not allow null, sends the result to review.
  • line_items is an array. Its items describe one line. The array of the line items always has the name line_items.

The two other keys of the configuration are annotation_fields, the fields that become annotations of a document with an accepted result, and rules, which apply to every form of the document type, for example the sign of the amounts on a credit note. Lesson 2 adds the checks of an invoice to this document type.

The table of the document type editor cannot mark a field as “may be null” and cannot show every keyword of a JSON schema. So you paste the whole configuration on the tab Advanced (JSON) and apply it to the form.

  1. On the page of the profile, open the tab Document types & forms and choose Create document type.
  2. Enter the Document type invoice.
  3. Open the tab Advanced (JSON). In the field Configuration, replace the text with the JSON above.
  4. Choose Apply to form. The tab Form now shows the fields, the rules and the annotation fields of the JSON.
  5. Choose Create document type. The message The configuration is valid. shows next to the button when the configuration has no errors.
The tab Advanced (JSON) of the document type editor with the pasted configuration and the warning that the JSON changed
Select the image to open it in full size.

The form HARBOUR_INVOICE describes the layout of one issuer. This is its configuration:

{
"markers": ["VAT ID DE 812 334 905", "^(INVOICE|CREDIT NOTE)$"],
"field_patterns": {
"invoice_number": "^(INV-[0-9]{4}-[0-9]{5}|CN-[0-9]{4}-[0-9]{4})$",
"customer_number": "^C-[0-9]{5}$",
"invoice_date": "^20[0-9]{2}-[01][0-9]-[0-3][0-9]$"
},
"line_pattern": "^([0-9]{1,3})\\n([A-Z]{3}-[A-Z0-9-]+)\\n(.*)\\n([0-9,]+)\\n([0-9.]+,[0-9]{2})\\n(-?[0-9.]+,[0-9]{2})$",
"line_columns": {"item_number": 2, "quantity": 4, "unit_price": 5, "amount": 6},
"decimal_comma": true,
"rules": [
"The customer is the company in the BILL TO block. The DELIVER TO block is only the shipping address.",
"Numbers use German format: \".\" groups thousands and \",\" is the decimal separator. \"1.234,56\" is 1234.56.",
"Dates are printed as DD.MM.YYYY.",
"\"Your order\" holds the order reference. \"-\" means there is none: return null.",
"\"Carried forward\" and \"Brought forward\" rows are page subtotals, not line items."
]
}
  • markers find the form in the text. The text must match every marker: the VAT ID of Harbour, and a line that holds only INVOICE or CREDIT NOTE.
  • field_patterns are checks that run after the model answered. Lesson 2 explains them.
  • line_pattern, line_columns and decimal_comma let ADM read the line table without the model. The section Read the line table with a pattern explains them.
  • rules apply to this form only. The system prompt of a form contains the rules of the profile, then of the document type, then of the form. A rule that is true for all your forms is written once, at the highest level.

The form has two more values that are not part of the configuration: a code and a description. The description is the first sentence of the system prompt. It tells the model what the document is: an invoice or credit note of Harbour Office Supplies GmbH, Hamburg.

  1. On the tab Document types & forms, choose Create form.
  2. In the section Identity, enter the Form code HARBOUR_INVOICE, choose the Document type invoice, and enter the Description an invoice or credit note of Harbour Office Supplies GmbH, Hamburg.
  3. Open the tab Advanced (JSON). Replace the text of the field Configuration with the JSON above, and choose Apply to form. The JSON holds \\n in the line pattern. After you apply it, the field Line pattern on the tab Form shows \n.
  4. On the tab Form, the sections Markers, Field patterns, Line items and Rules now hold the values of the JSON.
The tab Advanced (JSON) of the form editor with the pasted configuration of HARBOUR_INVOICE
Select the image to open it in full size.

Before you save, test the form on a document. The test calls no model and saves nothing.

  1. Go to the section Test the form on a document at the end of the form. Choose Choose a document, search for harbour-invoice-01-standard.pdf, and choose Choose in its row.
  2. The test shows which markers match the text of the document, and the line items that the line pattern read. The line Forms that match now lists saved forms only, so it says None until you save this form.
The test of the form on harbour-invoice-01-standard.pdf: both markers match, and the line pattern reads three line items
Select the image to open it in full size.
  1. Choose Create form. The tab Document types & forms lists the form.

The model reads the text of the PDF, not the PDF itself. adm_ai_form_extract_api.document_text returns that text: ADM extracts it with Oracle Text, the same way as for its full-text search. In the manager, the section Text of the form test shows it. 01_extract_one.sql prints the first lines:

The first 20 of 63 lines:
Invoice INV-2026-00412
Harbour Office Supplies GmbH
Kaistrasse 14 · 20457 Hamburg · Germany
VAT ID DE 812 334 905 · +49 40 555 019 0 · billing@harbour-office.example
INVOICE
Invoice no.
INV-2026-00412
Date
14.03.2026
Due date
13.04.2026
Customer no.
C-10233
Your order
PO-77810
BILL TO
DELIVER TO
Brightline Consulting AG
Attn. Accounts Payable

Each label and each value is on its own line. The headings BILL TO and DELIVER TO come before both addresses, so the text does not show which address belongs to which heading. On the page, the customer is under BILL TO. The rules of the form tell the model what the layout of the page shows and the text loses.

adm_ai_form_extract_api.matching_forms runs the markers of every active form of the profile against the text. It returns the forms whose markers all match. The markers use the match parameter m, so ^ and $ match at the start and the end of each line. A marker costs no model call and gives the same result every time. ADM extracts a text document only when exactly one form matches. The model never chooses the form of a text document.

adm_ai_form_extract_api.system_prompt returns the system prompt of a form: its description, then the rules of the profile, the document type and the form. The second section of 01_extract_one.sql prints both:

l_forms := adm_ai_form_extract_api.matching_forms(
p_profile_id => l_profile_id
, p_text => adm_ai_form_extract_api.document_text(d.document_id)
);
sys.dbms_output.put_line('Forms whose markers match: ' || apex_string.join(l_forms, ', '));
sys.dbms_output.put_line(adm_ai_form_extract_api.system_prompt(
adm_ai_form_config_api.get_form_id(l_profile_id, l_forms(1))
));
Forms whose markers match: HARBOUR_INVOICE
System prompt of the form:
You extract data from documents. This document is: an invoice or credit note of Harbour Office Supplies GmbH, Hamburg
Rules:
- Return dates as YYYY-MM-DD.
- If a value is not in the document, return null. Never guess.
- Take every value from this document. Numbers in notes or in the payment text that refer to other documents are not values of this document.
- The issuer of the invoice is never the customer.
- A credit note has no due date: return null. Its amounts are negative; keep the sign.
- The customer is the company in the BILL TO block. The DELIVER TO block is only the shipping address.
- Numbers use German format: "." groups thousands and "," is the decimal separator. "1.234,56" is 1234.56.
- Dates are printed as DD.MM.YYYY.
- "Your order" holds the order reference. "-" means there is none: return null.
- "Carried forward" and "Brought forward" rows are page subtotals, not line items.

The model reads the header fields. For the line table of the Harbour invoice, the model is not necessary: the line pattern of the form finds every line in the text. In the text, each cell of a line is on its own line, for example 1, PAP-A4-80, the description, 4, 24,90 and 99,60. The pattern describes one line of the table, with \n for each line break. line_columns names the group of the pattern that holds each column. decimal_comma true reads 24,90 as the number 24.90.

A pattern reads 120 lines in milliseconds, and it copies every value exactly. A model writes each value of the table as tokens of its answer, which takes time on a long table. When a form has a line pattern, ADM removes line_items from the schema that it sends to the model. A form without a line pattern gets the line table from the model. Lesson 3 adds such a form.

adm_ai_form_extract_api.extract_document runs every step for one document and writes the result: the form, the prompt, the model call, the line pattern, the checks and the annotations. It needs an ADM context with the edit right on the document, because it writes annotations and tags into ADM. It does not commit: the caller decides.

The manager has two ways to extract a document. Try a document shows the result as a preview and writes nothing. Extract with saved settings in the preview calls extract_document: it runs a new extraction with the saved configuration of the profile and writes that result and its annotations.

  1. On the page of the profile, choose Try a document. Search for harbour-invoice-01-standard.pdf and choose Choose in its row. The manager calls the model once and shows the result as a preview.
  2. Compare the values with the PDF. The facts above the values show the status, the form, the model, the tokens and the seconds.
The preview of harbour-invoice-01-standard.pdf: status OK, form HARBOUR_INVOICE, the fields and the three line items
Select the image to open it in full size.
  1. Choose Extract with saved settings. The manager calls the model again, writes the new result and opens the page of the document. The message names the status of the new result. It can differ from the preview, because it is a second model call.
  2. Choose the name of the profile in the breadcrumb, and open the tab Results. It lists the result of the document.
The tab Results of the profile pfx_supplier_forms with one result: harbour-invoice-01-standard.pdf, status OK, form HARBOUR_INVOICE
Select the image to open it in full size.
  1. Choose the document name in the list to open its result. The page shows the facts of the result and, below them, the review section that lesson 5 uses.
The result page of harbour-invoice-01-standard.pdf with status OK, method TEXT, form HARBOUR_INVOICE and the model gpt-5.6-luna
Select the image to open it in full size.

The result of gpt-5.6-luna for the first Harbour invoice, as 01_extract_one.sql prints it:

OK, form HARBOUR_INVOICE, gpt-5.6-luna, 853 prompt tokens, 100 completion tokens, 4.3 s
{
"document_type" : "invoice",
"invoice_number" : "INV-2026-00412",
"invoice_date" : "2026-03-14",
"due_date" : "2026-04-13",
"customer_number" : "C-10233",
"customer_name" : "Brightline Consulting AG",
"order_reference" : "PO-77810",
"discount_amount" : 0,
"net_amount" : 468.6,
"vat_rate" : 19,
"vat_amount" : 89.03,
"total_amount" : 557.63
}
line 1: {"item_number":"PAP-A4-80","quantity":4,"unit_price":24.9,"amount":99.6}
line 2: {"item_number":"TON-HP-26X","quantity":2,"unit_price":142.5,"amount":285}
line 3: {"item_number":"PEN-GEL-12","quantity":10,"unit_price":8.4,"amount":84}

The status is OK, so every field pattern passed. Every value matches the PDF. In the recording, the preview and the result page of the manager showed the same values. In the recordings of this lesson, a call took between 1.9 and 4.3 seconds, with 853 prompt tokens and 100 completion tokens. The three line items come from the line pattern, not from the model.

A result with the status OK becomes annotations of the document. The document type names the fields: invoice writes invoice_number, customer_number and total_amount, as the annotations invoice.<field>. They are on the document in the ADM file browser, in the information dialog of the document. The last section of 01_extract_one.sql reads them with SQL:

ANNOTATION_KEY ANNOTATION_VALUE
__________________________ ___________________
invoice.customer_number C-10233
invoice.invoice_number INV-2026-00412
invoice.total_amount 557.63

Other programs can find documents by these annotations, and a RAG collection of the AI Pack can select its files by an annotation key. A result with another status gets no annotations. It gets the tag extraction-review instead, which people see in the file browser. Lesson 5 uses it.

The view adm_my_form_extractions_v holds all fields and lines as JSON. Lesson 5 copies the accepted invoices into a table of your own application with a hook.