Extract the data of one form
By the end of this lesson, you can configure a PDF form in ADM, extract its fields and its line table with a language model, and read the result in ADM.
What the course builds
Section titled “What the course builds”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) andadm_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:
- Extract the data of one form (this lesson).
- Check and measure a form: build a test set, measure the form, and add check expressions and field patterns.
- Add a form: the steps that you repeat for each of your own forms.
- Read images and choose the models for your setup.
- 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.
- Apply the workflow to other documents: a summary of a test with five other types, and a checklist for your own forms.
How the configuration fits together
Section titled “How the configuration fits together”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_NOTEThe figure shows one sample document of each form, with the form and its document type below it:

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:
- The profile selects the document, because the document is in the folder of the profile.
- The markers of the forms find the one form that matches the document.
- The form names its document type. The document type gives the JSON schema of the answer and the checks.
- 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.
Follow a document through the extraction
Section titled “Follow a document through the extraction”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 valuesTerms used in the course
Section titled “Terms used in the course”- 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
OKwhen every check passed,REVIEWwhen a person must look at the document, andFAILEDwhen an error stopped the extraction. Lesson 5 addsAPPROVED. - 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.
Prepare the schema and the model access
Section titled “Prepare the schema and the model access”Use a test schema with ADM and the AI Pack. You need these items:
- The course scripts. Get the folder
examples/pdf-field-extractionfrom 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.sqlloads the sample files. - A model that the database can call. The course uses
gpt-5.6-lunaof OpenAI through an APEX web credential with the static IDOPENAI. 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 headerAuthorizationand the valueBearer <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,ADMINby 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.comfor OpenAI, and the certificate chain of the host must be in the wallet of the database. A missing network permission givesORA-24247. A missing certificate givesORA-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 = ADMINdefine pfx_provider = openaidefine 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.
Load the sample documents
Section titled “Load the sample documents”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.pngsamples loaded: 23 new file(s) in /users/admin/pfx_samplesCreate the profile
Section titled “Create the profile”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, pluspfx_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.
-
Log in to the ADM application as an administrator and open the page Form Extraction (under Administration). Choose Create.
-
In the section Name, enter the Profile name
pfx_supplier_forms. For Description, enterCourse "Extract data from PDF forms": forms of Harbour Office Supplies and Nordwind Logistics. -
In the section Which documents, enter the Folder
/users/admin/pfx_samples. Use the home folder of the user insettings.sql, or choose Browse… and selectpfx_samples. -
In the section How documents are read, under Text model, choose the Provider
OpenAI, enter the Modelgpt-5.6-luna, and enter the APEX web credentialOPENAI. Leave the vision model and the OCR switches off. -
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. -
Leave the other sections unchanged and choose Create profile.

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.

01_create_profile.sql creates all three levels with three calls of adm_ai_form_config_api. The calls go from the top level down.
Each call returns an ID that the next call needs: the document type belongs to the profile, and the form belongs to the document type.
Each call takes a JSON configuration. The API validates it before it saves it, and refuses a key that it does not know.
This is the first call, for the profile:
l_profile_id := adm_ai_form_config_api.create_profile( p_profile_name => 'pfx_supplier_forms', p_description => 'Course "Extract data from PDF forms": forms of Harbour Office Supplies and Nordwind Logistics', p_config_json => q'~{ "source_folder_path": "&pfx_home/pfx_samples", "text_model": { "provider": "&pfx_provider", "model": "&pfx_text_model", "config": &pfx_model_config }, "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." ] }~');&pfx_home is the home folder of the ADM user, for example /users/admin. settings.sql also builds &pfx_model_config.
For OpenAI it is {"g_apex_web_credential": "OPENAI"}.
The script runs all three calls. Read the next two steps first, then run @01_create_profile.sql.
If you run it again, it deletes the profile first, with every result, test case and measurement of the profile.
profile pfx_supplier_forms: document type invoice, form HARBOUR_INVOICEThe keys of the configuration have these meanings:
source_folder_pathlimits the profile to the documents in this folder and below it.text_modelnames the provider and the model. Itsconfiggoes unchanged to UC AI: the web credential, and for some providers the base URL and the options of the request.rulesapply to every form of the profile.
Create the document type
Section titled “Create the document type”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.

ADM uses the schema at each step of the extraction:
- 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.
- The result. The answer of the model is the result of the extraction. ADM keeps its fields, and the line items separately.
- The checks. The check expressions of lesson 2 use the names of the fields, for example
net_amount + vat_amount = total_amount. - The publication.
annotation_fieldsselects 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:
typeis the type of a value."number"means a JSON number, not a text. The model must convert1.234,56to1234.56, which is why each form has a rule for its number format.["string", "null"]allows a text or null.descriptionis 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 inrules.enumlists the only values that the field can have.requiredlists 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_itemsis an array. Itsitemsdescribe one line. The array of the line items always has the nameline_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.
- On the page of the profile, open the tab Document types & forms and choose Create document type.
- Enter the Document type
invoice. - Open the tab Advanced (JSON). In the field Configuration, replace the text with the JSON above.
- Choose Apply to form. The tab Form now shows the fields, the rules and the annotation fields of the JSON.
- Choose Create document type. The message The configuration is valid. shows next to the button when the configuration has no errors.

The second call of 01_create_profile.sql passes this JSON to create_doc_type, with the ID of the profile:
l_doc_type_id := adm_ai_form_config_api.create_doc_type( p_profile_id => l_profile_id, p_doc_type => 'invoice', p_config_json => q'~{ ... the JSON above ... }~');Create the form
Section titled “Create the form”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." ]}markersfind the form in the text. The text must match every marker: the VAT ID of Harbour, and a line that holds onlyINVOICEorCREDIT NOTE.field_patternsare checks that run after the model answered. Lesson 2 explains them.line_pattern,line_columnsanddecimal_commalet ADM read the line table without the model. The section Read the line table with a pattern explains them.rulesapply 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.
- On the tab Document types & forms, choose Create form.
- In the section Identity, enter the Form code
HARBOUR_INVOICE, choose the Document typeinvoice, and enter the Descriptionan invoice or credit note of Harbour Office Supplies GmbH, Hamburg. - Open the tab Advanced (JSON). Replace the text of the field Configuration with the JSON above, and choose Apply to form.
The JSON holds
\\nin the line pattern. After you apply it, the field Line pattern on the tab Form shows\n. - On the tab Form, the sections Markers, Field patterns, Line items and Rules now hold the values of the JSON.

Before you save, test the form on a document. The test calls no model and saves nothing.
- 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. - 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
Noneuntil you save this form.

- Choose Create form. The tab Document types & forms lists the form.
The third call of 01_create_profile.sql passes the code, the document type ID, the description and this JSON to create_form:
l_form_id := adm_ai_form_config_api.create_form( p_profile_id => l_profile_id, p_form_code => 'HARBOUR_INVOICE', p_doc_type_id => l_doc_type_id, p_description => 'an invoice or credit note of Harbour Office Supplies GmbH, Hamburg', p_config_json => q'~{ ... the JSON above ... }~');The line pattern in the SQL text has \\n, as in the JSON. If you run @01_create_profile.sql now, it creates the profile, the document type and the form.
Read the text of an invoice
Section titled “Read the text of an invoice”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 PayableEach 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.
Find the form and build the prompt
Section titled “Find the form and build the prompt”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_INVOICESystem prompt of the form:You extract data from documents. This document is: an invoice or credit note of Harbour Office Supplies GmbH, HamburgRules:- 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.Read the line table with a pattern
Section titled “Read the line table with a pattern”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.
Extract one invoice
Section titled “Extract one invoice”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.
- On the page of the profile, choose Try a document. Search for
harbour-invoice-01-standard.pdfand choose Choose in its row. The manager calls the model once and shows the result as a preview. - Compare the values with the PDF. The facts above the values show the status, the form, the model, the tokens and the seconds.

- 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.
- Choose the name of the profile in the breadcrumb, and open the tab Results. It lists the result of the document.

- 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.

Run @01_extract_one.sql. The script makes one model call. The section “Extract the invoice” calls extract_document for the file harbour-invoice-01-standard.pdf:
adm_ai_form_extract_api.extract_document( p_profile_id => adm_ai_form_config_api.get_profile_id('pfx_supplier_forms'), p_document_id => d.document_id);commit;The script sets the ADM context once at its start with adm_context_api.system_user_login.
The result goes into the view adm_my_form_extractions_v, one row for each profile and document. The script prints the row.
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.
Read the result in ADM
Section titled “Read the result in ADM”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-10233invoice.invoice_number INV-2026-00412invoice.total_amount 557.63Other 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.