Classify Your Fields
Every ISV shipping to AppSource does this chore by hand: after declaring DataClassification on each field, someone still has to record what each field means for privacy — Normal, Personal, Sensitive or Company Confidential — so that a customer's admin can answer a data-subject request without guessing. Business Central keeps those answers in the Data Sensitivity table, one row per company, table and field, and the Data Classification Worksheet is where an admin reads them.
Your extension ships two tables. Automate the chore for them.
What you start with
The starter holds three files, and your submission must contain all three: the two tables and the codeunit you fill in.
Table "Loyalty Member":
| Field | DataClassification |
Data sensitivity your code must record |
|---|---|---|
"Member No." |
CustomerContent |
Normal |
"Full Name" |
EndUserIdentifiableInformation |
Personal |
"E-Mail" |
EndUserIdentifiableInformation |
Personal |
"Health Notes" |
EndUserIdentifiableInformation |
Sensitive |
"Loyalty Discount" |
CustomerContent |
Company Confidential |
"Visit Count" (FlowField) |
— | no row at all |
Table "Loyalty Visit":
| Field | DataClassification |
Data sensitivity your code must record |
|---|---|---|
"Entry No." |
CustomerContent |
Normal |
"Member No." |
CustomerContent |
Normal |
"Visit Date" |
CustomerContent |
Normal |
Amount |
CustomerContent |
Company Confidential |
"Device Id" |
EndUserPseudonymousIdentifiers |
Personal |
"Partner Store Code" |
OrganizationIdentifiableInformation |
Company Confidential |
Leave both tables exactly as they are: the grading tests look every field up by name, and the classifications above are what the exercise turns on — one test reads the shipped DataClassification values and the "Visit Count" FlowField back out of the metadata and fails if you changed them.
Requirements
Create a codeunit named "Loyalty Data Classification" with two public procedures:
procedure ClassifyTable(TableNo: Integer): Boolean
procedure ClassifyField(TableNo: Integer; FieldNo: Integer; Sensitivity: Text)Rules:
ClassifyTablerecords the sensitivities listed above for every field of the given table and returnstrue. That is the whole chore for one table: put every stored field of your own on the plain Normal baseline first, then override the fields that deserve more than Normal.ClassifyTablereturnsfalseand records nothing when the table number is not a supported table — a number no table object uses — and also when it belongs to a table your extension does not own.Customeris not yours to classify.ClassifyFieldrecordsSensitivityfor a single field. The tests pass exactly one of'Normal','Sensitive','Personal'and'Company Confidential', and expect the field to end up with that sensitivity whether or not it already had a row.ClassifyFieldraises an error when the table has no field with that number. The message must containField <field number> does not exist in table <table number>, with the two numbers rendered the way%1and%2render an Integer.- Running
ClassifyTablea second time on a table it already classified must not fail, and must leave the same sensitivities behind. This chore gets re-run after every release. - The
"Visit Count"FlowField and the platform's own system fields (SystemIdand friends) must end up with no Data Sensitivity row. A FlowField stores nothing, and the system fields are the platform's, not yours to classify.
Two things that cost time when nobody says them out loud:
- The
Data Sensitivitytable isScope = OnPrem, so an extension cannot declareRecord "Data Sensitivity"at all — it does not compile. Everything you need is onCodeunit "Data Classification Mgt."in the System Application. - That codeunit's
SetTableFieldsToNormallooks like the baseline you want, but it is not: the system fields carry the table-levelDataClassification(CustomerContenton both tables), so the routine putsSystemIdand friends on Normal along with everything else — and nothing in the codeunit deletes a row again. Build the baseline yourself fromRecord "Field":RecordRef.SystemIdNo()is the first system field number, so every field below it is yours. The per-fieldSetFieldTo...procedures update an existing row and insert a missing one, which makes them both the baseline and the override —"Partner Store Code"isOrganizationIdentifiableInformation, and it still has to come out as Company Confidential.
Pick object IDs in the range 50100–50199, and reference every object by name, never by a literal ID.
What the tests check
The grading tests call ClassifyTable for each table — expecting true from both — and read the resulting rows back through the Data Classification Worksheet page, asserting the sensitivity of every field in the two tables above one by one, including that the FlowField and SystemId produce no row, and that no row of either table is left Unclassified. Two tests read the two tables' own metadata and expect the shipped DataClassification values and the "Visit Count" FlowField to be untouched. One test classifies the same table twice and expects the second run to succeed with the same result. One passes a table number that no object uses and expects false; one passes Customer and expects false plus an unchanged Customer classification. ClassifyField is exercised once per sensitivity name on the same field, once on a field that already carries a sensitivity, and once with a field number that does not exist, where the error message is matched as a substring. Captions and tooltips are not graded.
Learn More
- Classifying data sensitivity fields — what Normal, Personal, Sensitive and Confidential actually mean to the admin reading your rows.
- Business Central privacy for AL developers — why an extension is expected to classify its own data model.
- AppSourceCop Error AS0016 — the AppSource rule behind the chore, and the reason FlowFields are always SystemMetadata.
- System fields — the fields the platform adds to every table, the ones your classification must leave alone.
Codeunit "Data Classification Mgt." in the System Application. Browse its public procedures once before writing anything: there is one that answers whether a table can be classified at all, and one per sensitivity level for a single field. The one that classifies a whole table in one call is a trap here — it sweeps the system fields in, and no procedure deletes a row again.Record "Field" filtered on the table number, Class = Normal and a field number below RecordRef.SystemIdNo(), putting each field on the plain baseline; then override the handful that deserve more — the overrides win because each one rewrites the row the baseline just created. Field numbers come from FieldNo("Field Name"), so nothing is hardcoded and nothing breaks when the app is renumbered.InsertDataSensitivityForField is the trap: it always inserts, so a second run dies on the existing primary key. The SetFieldTo... procedures update the row when it is there and insert it when it is not — which is also what gets a field a row when no baseline created one. For the unknown-field error, Record "Field" is keyed on table number and field number, so a failed Get is your check.