The labelling rules Labelwise applies
This is the record of every labelling rule Labelwise applies, with the Food Standards Code section for each and the test that pins it. It is the same file our engineers work from.
Scope: packaged food for retail sale in Australia
The app makes labels for packaged food sold at retail in Australia. This section overrides anything else in the project that says otherwise.
| Part of the label | Status in Australia | Law | In the app |
|---|---|---|---|
| Ingredient list and allergen declarations | Mandatory | Food Standards Code 1.2.3, 1.2.4, 1.2.10 | Step 1 |
| Nutrition information panel | Mandatory | Food Standards Code 1.2.8 | Step 2 |
| Country of origin label | Mandatory for most food | Country of Origin Food Labelling Information Standard 2016 | Step 3, on by default |
| Supplier, date mark, lot, storage, net quantity | Mandatory | Food Standards Code 1.2.2, 1.2.5, 1.2.6; trade measurement law | Step 4 |
| Health Star Rating | Voluntary | HSR system | Step 5 |
A label is "ready to print" only when every mandatory part is present. Country of origin can be turned off for food the Information Standard does not apply to, e.g. food made and packaged on the premises where it is sold (s 14(2)(b)), and the app then warns.
New Zealand is out of scope. The Food Standards Code is shared, so the nutrition panel, ingredient list and allergen rules below are the same there, but the app isn't offered for food sold in New Zealand: it doesn't implement New Zealand's country of origin rules, its trade measurement rules or its consumer law, and nothing here was checked for that market. New Zealand appears only as a country that food or ingredients can come from.
Also out of scope, and refused. The Food Standards Code governs these products under standards
the app doesn't implement, so the app refuses them rather than print a label that looks finished.
These are Labelwise policy rules, not Code wording; each refusal cites the standard that governs the
product so the business knows why (labelwise/scope.py, tests/test_scope.py), e.g. "Labelwise doesn't make labels
for kava. Standard 2.6.3 sets its own rules."
| Product | Governing standard | What the app does |
|---|---|---|
| Infant formula products | Standard 2.9.1 (its own composition and labelling rules) | Refuses |
| Food for infants (under 12 months) | Standard 2.9.2 | Refuses |
| Formulated meal replacements and formulated supplementary foods | Standard 2.9.3 | Refuses |
| Formulated supplementary sports foods | Standard 2.9.4 | Refuses |
| Food for special medical purposes | Standard 2.9.5 | Refuses |
| Alcoholic drinks (more than 1.15% alcohol by volume) | Part 2.7 and 1.2.8—5 (no standard nutrition panel; alcohol content and standard drinks statements instead) | Refuses |
| Kava | Standard 2.6.3 | Refuses |
| Formulated caffeinated beverages (energy drinks) | Standard 2.6.4 (extra statements and limits) | Refuses |
| Any product making a nutrition content claim or health claim on the label | Standard 1.2.7 (claims have conditions the app doesn't check yet; PLANS.md Phase 4) | Refuses |
| Unpackaged food, food for catering, food sold for immediate consumption | 1.2.1—6 to 1.2.1—9 (different labelling duties, or none) | Refuses: the app only does packaged food for retail sale |
- The business's tick is the record. On the product step the business ticks any of these that
apply (
Recipe.product_scope), or confirms that none do (Recipe.scope_confirmed). A ticked product is an error on the product step, which stops the label printing, and comes first in the warnings. Nothing ticked and nothing confirmed is a warning asking the business to confirm; it doesn't stop printing. - The detectors are a safety net, not the decision. The product name, the ingredient names and the details lines are searched (whole words, any case) for words that suggest a refused product: infant, baby, "stage 1" (2.9.2); formula (2.9.1); meal replacement, toddler (2.9.3, which covers formulated supplementary foods for young children); sports, protein powder, pre-workout (2.9.4); beer, wine, cider, spirit, vodka, gin, whisky, rum, liqueur, mead, "% alc", ABV (Part 2.7; the product name and ingredient names only, since the details lines name places such as Gin Gin QLD); kava (2.6.3); "energy drink", or guarana together with caffeine (2.6.4; guarana alone only needs its advisory statement). These everyday foods are skipped before matching: cider vinegar, wine vinegar, spirit vinegar, rice wine vinegar, baby spinach, baby corn, baby carrots, baby beetroot, baby leaf. A match, until the business confirms, is a warning naming the words seen; it never refuses on its own.
- Claims guardrail (1.2.7). The product name and the details lines (not the ingredient names) are searched for these phrases, whole phrases, any case, a space or a hyphen between words: low fat, fat free, reduced fat, low sugar, sugar free, no added sugar, low salt, low sodium, reduced salt, high fibre, source of fibre, high protein, source of protein, good source, high in, rich in, low GI, gluten free, lactose free, contains omega, healthy, nutritious, boosts, immunity, lowers cholesterol, supports. A match, until the business confirms, is a warning that the label may be making a nutrition content or health claim, which Labelwise doesn't support yet: remove the words, or tick the claims box, which then refuses the label.
- Not claims. "Free range", "organic", "natural", "handmade", "vegan", "vegetarian", "halal" and "kosher" describe how the food is made or who it suits. They aren't nutrition content or health claims under 1.2.7 and never trigger the guardrail.
How the rules are recorded
This file lists every labelling rule the app implements, with the section of the Australia New
Zealand Food Standards Code (or other law) it comes from. Each rule was checked against the latest compilations on the Federal
Register of Legislation as at 27 Sep 2026 (see the table at the end). Every rule has a test in tests/.
Before launch: each reference label is cross-checked against the official government tools (the FSANZ Nutrition Panel Calculator, the official HSR Calculator and the government's country of origin labelling tool), and any difference becomes a fix here and a test (see PLANS.md §7). This is planned, not yet done. Keep the table at the end up to date when the Code is amended.
Each warning names the components it concerns (nutrition panel, ingredient list, Health Star
Rating, country of origin). A component prints on its own only when its own errors are clear; the
full label, and the artwork download, only when there is no error at all (a Labelwise policy rule;
label.py, _printable() in main.py).
Nutrition Information Panel (Standard 1.2.8, Schedule 12)
| Rule | Source | Where |
|---|---|---|
| Format: "NUTRITION INFORMATION", "Servings per package:", "Serving size:", then per-serving and per-100 g (or 100 mL) columns | 1.2.8—6(2), S12—2 | nutrition.py, pdf.py |
| Headings state that values are averages: "Average quantity per serving" / "Average quantity per 100 g" | 1.2.8—7(1)(a); wording from the example panel in S12—7 | serving_texts() |
| Rows in order: Energy, Protein, Fat total, – saturated, Carbohydrate, – sugars, Sodium | S12—2 | nip_rows() |
| Dietary fibre (optional here) goes after the carbohydrate rows and before sodium | S12—3 | nip_rows() |
| Energy in kJ, optionally with Cal (kJ ÷ 4.18) | 1.2.8—6(1)(d)(i), S11—2(4) | energy() |
| Values to not more than 3 significant figures (we also cap grams at 1 decimal place, and show none from 100 g up); halves round up | 1.2.8—7(3) | declared(), fmt() |
| Optional "LESS THAN 40 kJ", "LESS THAN 1 g" (protein, fat, fatty acids, carbohydrate, sugars, fibre), "LESS THAN 5 mg" (sodium) | 1.2.8—7(4)–(6) | use_less_than |
| Values are per 100 g of the final food: the recipe's total nutrients ÷ weight after cooking | FSANZ NPC calculation method | calculate() |
| Per 100 mL uses the food's density | 1.2.8—6(2) | calculate() |
Lab values. The panel may be printed from values for the finished food reported by a lab or
taken from a supplier's specification of the finished product (Recipe.lab_values, per 100 g or
100 mL), instead of from the ingredients. The average quantity may be worked out from an analysis
of the food (method (a)) as well as from the ingredients (method (b), the recipe totals above);
the business chooses the method that best represents it, allowing for lot-to-lot and seasonal
variation (1.1.1—6(2), (3)(a); "average quantity" defined in 1.1.2—2(3)). Lab values are rounded
and declared exactly as recipe totals are, and rated for the Health Star Rating the same way; the
ingredients' values are then not used. The source is recorded in the label data (Label.lab_source) and in
the artwork README. The app doesn't check the lab's method or whether the values represent every lot: that
judgement is the business's (a Labelwise policy rule). calculate(), check().
Not implemented yet (the app must not be used for these products):
- Nutrition, health or related claims. These trigger extra rows: fibre when fibre or carbohydrate claims are made (1.2.8—6(5)), fatty-acid rows for fat claims (6(3)), potassium for salt claims (6(12)), galactose for lactose claims (6(11)).
- Variable-weight packs, where servings are stated per kilogram (1.2.8—6(1)(a)(ii)).
- Alternative wording such as "slice" or "pack" instead of "serving" (7(2)(a)).
- %DI (percentage daily intake) columns.
- Infant food, formula and other special-purpose foods.
Energy check (Schedule 11)
A data-entry check only, not used for the panel values: protein 17, fat 37, carbohydrate 17, dietary fibre 8 kJ/g (S11—2(2)). The estimate is 17 × protein + 37 × fat + 17 × carbohydrate + 8 × fibre per 100 g; ingredients estimated at 50 kJ or less aren't checked.
- Fibre known: an ingredient whose stated energy differs from the estimate by more than 25% gets a warning.
- Fibre unknown (the FSANZ data has none): fibre can only add energy, so the ingredient gets a
warning when its stated energy is more than 25% below the estimate, or more than 25% of the
estimate above the ceiling
estimate + 8 × (100 − protein − fat − carbohydrate), i.e. as if everything else in 100 g were fibre. Unchanged FSANZ foods (chillies, garlic, tomatoes, paprika, cinnamon, coriander seed) pass. For watery foods the ceiling is loose (tomatoes pass at double their energy); a typo that drops a digit is still caught.
Ingredients with negligible nutrition (a Labelwise data-entry rule, not a Code rule): an ingredient marked negligible, used in a tiny amount with no nutrition data (cultures, a pinch of citric acid), gets no "has no nutrition values" warning and counts as fibre 0 for the Health Star Rating, so it doesn't make the rating provisional. It is still listed and still counted in the weights. A negligible ingredient that is more than 2% of the ingredients' total ingoing weight gets a warning: too much to ignore; enter its nutrition values.
Ingredients removed after cooking (a Labelwise data-entry rule): bones, a bouquet garni or tea leaves strained out after cooking take nutrients with them that the recipe can't account for, since the calculation can only treat lost weight as evaporated water. While any ingredient is marked removed after cooking the label has an error: enter the strained liquid as an ingredient with its own nutrition values (from a lab report or supplier), or wait for lab-values input. The removed ingredients are still declared in the ingredient list (see "Ingredients removed after cooking are declared" under Ingredient list).
Ingredient list (Standard 1.2.4)
| Rule | Source |
|---|---|
| Descending order of ingoing weight | 1.2.4—5(1) |
| Added water counted as the amount remaining after cooking (lost moisture is taken from added water first) | 1.2.4—5(4) |
| Added water under 5% of the food is not listed; all the added water is counted together | 1.2.4—3(2)(c)(iii) |
| Compound ingredients: name followed by its components in brackets | 1.2.4—5(5) |
| The "Ingredients:" heading is conventional, not required by law | none |
Not implemented:
- The rule that compound ingredients under 5% of the food need only list their allergens and additives (1.2.4—5(6)). A compound ingredient's components are free text, printed as typed: an additive among them is typed in its declared form ("emulsifier (322)") and the app doesn't check it. An ingredient that is itself an additive is declared and checked as in "Food additives" below.
- Reconstitution water; volatile ingredients other than water; processing aids.
Ingredients removed after cooking are declared. Bones in a broth, tea leaves in a brewed drink and herbs in a bouquet garni are ingredients in the ordinary sense (1.1.2 does not define the word), and every ingredient is listed (1.2.4—3(1)). What they contribute (extract, flavour, nutrients, allergens) is in the food for sale, so they are not processing aids (1.2.4—3(2)(d), (e) with 1.1.2—13(1), (2)) and not volatile ingredients completely removed during processing (1.2.4—3(2)(b)). They are listed by their usual name in descending order of ingoing weight (1.2.4—5(1)), at their full ingoing weight: 1.2.4—5(4) adjusts only water and volatile ingredients, not the part strained out. An allergen they carry (fish frames: fish) is declared under 1.2.3 as for any ingredient. The app lists them like any other ingredient; the nutrition panel for such a food is an error (see "Ingredients removed after cooking" above).
Not offered yet: added water in a food declared or named as a broth, brine or syrup need not be listed (1.2.4—3(2)(c)(ii)). Labelwise always applies the general added-water rules above.
Food additives (1.2.4—7, —8; Schedules 7 and 8)
An ingredient the business confirms as a food additive (Ingredient.additive) is printed in its
declared form instead of its name, in its place by ingoing weight (1.2.4—5(1)). Code numbers are
printed as Schedule 8 prints them, with no "E" prefix.
| Rule | Printed | Source |
|---|---|---|
| An additive with a Schedule 7 class (prescribed or optional) is listed by the class name followed in brackets by its code number or its name as in Schedule 8 | "Acidity regulator (330)", "Acidity regulator (citric acid)" | 1.2.4—7(1)(a) |
| Otherwise, by its Schedule 8 name | "Monk fruit extract" | 1.2.4—7(1)(b) |
| Where Schedule 8 joins alternative names with "or", the label uses the one the business entered; the numerical listing's name is printed when the two listings differ (954, 307c), and the alphabetical variant is accepted as input | "Humectant (glycerol)", "Sweetener (saccharin)" | 1.2.4—7(1), S8—2 |
| An additive with no code number in Schedule 8 is listed by name | "Sweetener (monk fruit extract)" | 1.2.4—7(1)(a) |
| An enzyme may be listed as "enzyme" without its specific name | "Enzyme" | 1.2.4—7(3) |
| A flavouring is listed as "flavouring" or "flavour", or by a more specific name | "Flavouring", "Natural vanilla flavour" | 1.2.4—7(4) |
| Glutamates (620–625), disodium guanylate (627), disodium inosinate (631) and disodium-5′-ribonucleotides (635) added as or in a flavouring are named as in 7(1); a generic "flavouring" is an error | "Flavour enhancer (621)" | 1.2.4—7(5) |
| Added caffeine is listed as caffeine; under "flavouring" or another class name it is an error | "Caffeine" | 1.2.4—7(6) |
| An added vitamin or mineral may use the class name "vitamin" or "mineral"; under these class words the name is printed unless the business chooses the code | "Vitamin (ascorbic acid)" | 1.2.4—8 |
| An allergen the additive carries is declared as for any ingredient: bold where its required name is in the printed text, otherwise added in brackets | "Emulsifier (322) (soy)" | 1.2.3—7(2)(c) |
Checked when the label is built (each an error that stops printing): a class name not in Schedule 7 and not "flavouring", "flavour", "enzyme", "vitamin" or "mineral" (the closest is offered); a code not in Schedule 8, with "E330" or "INS 330" answered with the number alone; a name not in Schedule 8 (exact, case-insensitive; the closest is offered); a code and a name of different additives; a code shared by several Schedule 8 additives with no name, where the name is to be printed; a class name with nothing in brackets; an additive the app knows has a class, entered without one (7(1)(a), (2)); the 7(5) substances or caffeine under a generic flavouring. A vitamin or mineral under 1.2.4—8 may use a name not in Schedule 8, since most added vitamins and minerals are not listed there.
The most appropriate class is the producer's choice (1.2.4—7(2)). Citric acid is an acid in a
jam and an acidity regulator where it only adjusts pH; the business decides. The app only suggests:
from a Schedule 8 name or a code in the ingredient's name ("330", "E330", "INS 330"), with a class
from a class name typed in the name, or else from a curated map of about 30 additives small
producers commonly use (additives.CURATED_CLASS, each with the reason for its class). An
ingredient whose name suggests an additive but isn't confirmed as one gets a warning naming
1.2.4—7, the same guardrail as allergens and statements. Turmeric, saffron, amaranth and acacia are
not suggested: as an ingredient's name they are usually the food.
Data: labelwise/additives_s8.json, transcribed from Schedule 8 (344 rows, numerical listing,
cross-checked against the alphabetical listing) and Schedule 7 (19 prescribed and 6 optional class
names); see additives_s8.README.md and "Compilations checked".
Additives inside a compound ingredient's components are typed by the user and not checked.
Not modelled: processing aids (1.2.4—3(2)(d), Standard 1.3.3) are not listed and the app doesn't tell them apart; whether an additive is permitted in that food at that level (Schedules 15 and 16, Standard 1.3.1) is not checked.
Characterising ingredients (Standard 1.2.10)
| Rule | Source |
|---|---|
| % = ingoing weight of the ingredient ÷ total ingoing weight, excluding added water removed in manufacture | 1.2.10—4(1), 4(2) |
| Rounded to the nearest whole number, or the nearest 0.5 below 5% | 1.2.10—8(3) |
| The % immediately follows the ingredient's name | 1.2.10—8(2) |
Not implemented: the optional final-weight basis for products that lose moisture (1.2.10—5), minimum percentages (8(4)), and a percentage in the product name instead of the ingredient list. The user decides which ingredients are characterising.
Allergens: Plain English Allergen Labelling (Standard 1.2.3 Div 3, Schedule 9 S9—3)
| Rule | Source |
|---|---|
| Required names in bold in the ingredient list, in the same size as the other ingredient text | 1.2.3—7(1) |
| If the ingredient's name doesn't contain the required name, add it next to the name in brackets, e.g. "Butter (milk)" | 1.2.3—7(2)(c) |
| Tree nuts named individually (almond, Brazil nut, cashew, hazelnut, macadamia, pecan, pine nut, pistachio, walnut) | S9—3 |
| Wheat: "wheat" in the ingredient list; "wheat" and "gluten" in the summary | S9—3 |
| Barley, oats, rye (when they contain gluten): the cereal name in the ingredient list; "gluten" in the summary | S9—3 |
| Soy: "soy", "soya" or "soybean" in the ingredient list; "soy" in the summary | S9—3 |
| Summary statement starts with "Contains", lists only required names, is bold, and sits directly next to the ingredient list | 1.2.3—7(3)–(5) |
| Sulphites only when added at 10 mg/kg or more (the user confirms) | S9—3 |
| Singular or plural forms both accepted | 1.2.3—8 |
Required names (S9—3), as declaration.py (REQUIRED_NAME, ALTERNATIVE_NAMES) prints them. The test
named for each asserts the printed ingredient list with the name in bold and the "Contains:" statement
(tests/test_declaration.py).
| Allergen | In the ingredient list (bold) | Also accepted there | In the "Contains:" statement | Test |
|---|---|---|---|---|
| Wheat | wheat | wheat, gluten | test_order_bold_and_contains |
|
| Barley (containing gluten) | barley | gluten | test_gluten_cereals |
|
| Oats (containing gluten) | oats | gluten | test_gluten_cereals |
|
| Rye (containing gluten) | rye | gluten | test_every_required_name_printed |
|
| Soy | soy | soya, soybean | soy | test_soy_alternative_names |
| Almond | almond | almond | test_tree_nuts_named_individually_and_plurals |
|
| Brazil nut | Brazil nut | Brazil nut | test_tree_nuts_named_individually_and_plurals |
|
| Cashew | cashew | cashew | test_tree_nuts_named_individually_and_plurals |
|
| Hazelnut | hazelnut | hazelnut | test_every_required_name_printed |
|
| Macadamia | macadamia | macadamia | test_every_required_name_printed |
|
| Pecan | pecan | pecan | test_every_required_name_printed |
|
| Pine nut | pine nut | pine nut | test_every_required_name_printed |
|
| Pistachio | pistachio | pistachio | test_every_required_name_printed |
|
| Walnut | walnut | walnut | test_every_required_name_printed |
|
| Peanut | peanut | peanut | test_every_required_name_printed |
|
| Milk | milk | milk | test_order_bold_and_contains |
|
| Egg | egg | egg | test_every_required_name_printed |
|
| Fish | fish | fish | test_every_required_name_printed |
|
| Crustacean | crustacean | crustacean | test_every_required_name_printed |
|
| Mollusc | mollusc | mollusc | test_every_required_name_printed |
|
| Sesame | sesame | sesame | test_every_required_name_printed |
|
| Lupin | lupin | lupin | test_every_required_name_printed |
|
| Sulphites (added at 10 mg/kg or more) | sulphites | sulphites | test_every_required_name_printed |
Guardrails in the app:
- Every ingredient must be marked "allergens checked" before anything can be printed. The server enforces this: the PDF and artwork downloads are refused while any ingredient is unchecked or the label has any other error. Changing an ingredient's name or components clears its check.
- The app never says "no allergens" for an ingredient that hasn't been checked.
- Made products from the database (bread, sauces, sausages, anything "commercial") are marked "allergens unknown: check the spec", because a name can't show what is in them.
The user's responsibility:
- Confirming each ingredient's allergens from supplier specifications. The app only suggests them from names.
- Exemptions in S9—3 column 2: glucose syrups from wheat, alcohol distilled from wheat or whey, isinglass, refined soy oil, soy tocopherols and phytosterols.
- Precautionary "may be present" statements. These are voluntary and not regulated by the Code.
Other mandatory label information (Standard 1.2.1—8)
The app prompts for the supplier's name and business address (1.2.2—4; the Code accepts an address in Australia or New Zealand, and the app asks for the Australian one). The app refuses a postal address (PO box, GPO box, locked or private bag): the address must say where the business is, a date mark (1.2.5—3, not needed if the best-before date is 2 years or more away), lot identification (1.2.2—3), and storage conditions and directions for use (1.2.6—2). It doesn't check the food name (1.2.2—2) or product-specific requirements. Type must be legible and contrast with the background (1.2.1—24). Warning and advisory statements (1.2.1—8(1)(d)) have their own section below.
Warning and advisory statements (Standard 1.2.3, Schedule 9)
The label of packaged food for retail sale must carry the advisory statements, warning statements
and declarations the Code requires (1.2.1—8(1)(d)). The statements are made in statements.py
from what the business confirms: substances tagged on each ingredient (statement_tags) and what
the product is (product_flags). They print after the ingredient list, the "Contains" statement and
any "may contain" statement, in the sticker PDFs (whole label, or ingredients only) and in the pack
artwork's ingredients block. Tests: tests/test_statements.py.
The warning statement is prescribed word for word. Advisory statements are required "to the effect that" (S9—2), so the app prints each one as a plain sentence that keeps the schedule's meaning.
| Trigger | Statement printed | Source |
|---|---|---|
| Royal jelly, or a food that has it as an ingredient (warning statement) | This product contains royal jelly which has been reported to cause severe allergic reactions and in rare cases, fatalities, especially in asthma and allergy sufferers. | 1.2.3—3 |
| Bee pollen, or a food that has it as an ingredient | This product contains bee pollen, which can cause severe allergic reactions. | 1.2.3—2(1), S9—2 item 1 |
| A drink made from cereals, nuts or seeds with less than 3% m/m protein | This product is not suitable as a complete milk replacement for children under 5 years. | 1.2.3—2(1), S9—2 item 2 |
| Such a drink with at least 3% protein and no more than 2.5% fat; milk or a soy drink with no more than 2.5% fat | This product is not suitable as a complete milk replacement for children under 2 years. | 1.2.3—2(1), S9—2 item 3 |
| Aspartame or aspartame-acesulphame salt (951, 962) | This food contains phenylalanine. | 1.2.3—2(1), S9—2 item 4 |
| Quinine | This food contains quinine. | 1.2.3—2(1), S9—2 item 5 |
| Guarana or guarana extracts | This food contains caffeine. | 1.2.3—2(1), S9—2 item 6 |
| Added phytosterols, phytostanols or their esters | When consuming this product, it should be consumed as part of a healthy diet. / This product may not be suitable for children under 5 years and pregnant or lactating women. / Plant sterols do not provide additional benefits when consumed in excess of 3 grams per day. | 1.2.3—2(1), S9—2 item 7 |
| A cola drink with added caffeine, or a food that has one as an ingredient | This product contains caffeine. | 1.2.3—2(1), S9—2 item 8 |
| Propolis, or a food that has it as an ingredient | This product contains propolis, which can cause severe allergic reactions. | 1.2.3—2(1), S9—2 item 9 |
| Unpasteurised egg products | This product is unpasteurised. | 1.2.3—2(1), S9—2 item 10 |
| Unpasteurised milk or unpasteurised liquid milk products | This product has not been pasteurised. | 1.2.3—2(1), S9—2 item 11 |
| Lactitol, maltitol, maltitol syrup, mannitol or xylitol at 10 g/100 g or more, alone or together | Excess consumption may have a laxative effect. | 1.2.3—2(2)(a) |
| Erythritol, isomalt, polydextrose or sorbitol at 25 g/100 g or more, alone or together | (the same) | 1.2.3—2(2)(b) |
| A mix of the two groups above at 10 g/100 g or more | (the same) | 1.2.3—2(2)(c) |
| Added D-allulose in a bakery product, bubble or chewing gum, breakfast cereal, confectionery other than chocolate, a dessert, ice cream or edible ice, a non-alcoholic water-based flavoured drink, or yoghurt | (the same) | 1.2.3—2(2)(d) |
How the app decides:
- Royal jelly, bee pollen, propolis, aspartame, quinine, guarana, phytosterols, the polyols and
D-allulose are tags on an ingredient. Product flags cover the rest:
plant_milk(a cereal, nut or seed drink),milk_or_soy_beverage,cola_added_caffeine,unpasteurised_egg,unpasteurised_milkandallulose_food(the food has added D-allulose and is one of the foods in 1.2.3—2(2)(d)). - Items 2 and 3 are decided from the food's protein and fat per 100 g (% m/m), unrounded: the
limits are on the food's composition, not the printed panel, so 2.96 g protein (printed "3.0 g")
is under 3%. A drink's per 100 mL values are taken back to per 100 g with its density. Below 3%
protein a
plant_milkdrink gets item 2; otherwise item 3 when fat is 2.5% or less; above that, no statement (an info message says why). The evaporated and dried forms (as reconstituted) are left to the business. - Polyols are counted from ingoing weights over the final weight, the same per 100 g basis as the panel. A tagged ingredient counts in full (maltitol syrup is counted as if it were all maltitol, which can only add a statement, never drop one), and counts once: in the 10 g group if it is tagged with polyols from both groups. A tagged polyol under every threshold gives an info message with the amount and the thresholds.
- D-allulose tagged without the
allulose_foodflag gives an info message listing the foods in 1.2.3—2(2)(d). The flag on its own prints the statement. - Items 6 and 8 both say the food contains caffeine; with both, the sentence prints once.
Guardrail: the app suggests tags from the ingredient name (royal jelly; bee pollen; propolis; aspartame, aspartame-acesulphame, 951, 962; quinine; guarana; phytosterol, phytostanol, plant sterol, sterol ester; lactitol 966, maltitol 965, mannitol 421, xylitol 967, erythritol 968, isomalt 953, polydextrose 1200, sorbitol 420; allulose). A suggestion the business hasn't confirmed gives a warning on the allergens step that names the statement and its clause, e.g. "Royal jelly: confirm this ingredient is royal jelly so the warning statement is added (1.2.3—3).", and no statement is printed for it.
Size of type and legibility:
- A warning statement must be at least 3 mm high, or 1.5 mm on a small package (1.2.1—25). A small
package has a surface area under 100 cm² (1.1.2—2); the business enters
package_surface_cm2, and without it the app uses 3 mm and says so in an info message. - Size of type is "the measurement from the base to the top of a letter or numeral" (1.1.2—2): the
cap height. The warning statement is set in bold at the minimum height in points divided by the
bold face's cap height (OS/2
sCapHeight, 1409/2048 em for Liberation Sans Bold), rounded up to 0.1 pt (artwork.warning_statement_size_pt()): 12.4 pt for 3 mm, 6.2 pt for 1.5 mm. It is never set smaller than the body type. In the sticker PDFs it keeps this size while the shrink-to-fit loop reduces the rest of the label; if the label then doesn't fit, the PDF is refused with a message that gives the warning statement's size. The pack artwork sets it the same way, and its README gives the size. - Advisory statements are printed at the body size, like the rest of the label.
- Every required statement must be in English, legible, and prominent so as to contrast distinctly with the background of the label (1.2.1—24). The app prints black on the label stock.
Not modelled: declarations other than allergens; infant foods and their statements (Standard 2.9.5); formulated caffeinated beverages (Standard 2.6.4); kava; the evaporated and dried forms of the milk replacement drinks; tags found in a compound ingredient's components (the suggestion reads the ingredient's name only).
Net quantity (trade measurement)
Packaged food sold in Australia must show its net quantity under national trade measurement law
(National Measurement Act 1960 and National Trade Measurement Regulations 2009, "NTMR"), which is
separate from the Food Standards Code. Checked against NTMR Compilation No. 8 (20 Mar 2024). The
checks are in net_quantity_check() and net_quantity_min_height_mm() (label.py); the tests are
in tests/test_net_quantity.py. Each message ends with its regulation in brackets.
| Rule | NTMR | App |
|---|---|---|
| The package must be marked with a statement of the measurement of the product | 4.9(1) | Warning when the net quantity is missing |
| A pack under 15 g or 15 mL needs no statement, unless it holds adhesives, dried or freeze-dried vegetables, herbs, instant tea, spices, therapeutic goods or tobacco; it may still be shown | 4.4(1), Sch 4 item 5.3 | Info when the stated amount is under 15 g or 15 mL |
| Metric units only, as Schedule 6 permits: kg always, g only up to 1 000 g; L (also dL, cL) always, mL only up to 1 000 mL | 4.11(1)(e), 4.13(6), Sch 6 | Error for oz, lb, fl oz, g over 1 000 g and mL over 1 000 mL |
| A decimal below 1 has a zero before the point (fractions are written as decimals, 4.14(1)) | 4.11(2) | Error for ".5 kg" |
| Liquids by volume; solid, semi-solid or partly solid goods by mass, unless Schedule 5 says otherwise | 4.13(2), (3) | Warning when a liquid (panel per 100 mL) is stated in g or kg, or another food (panel per 100 g) in mL or L |
| No marking by reference to a minimum measurement (except dressed poultry and preserved sausage) | 4.16(1) | Error for "min", "minimum", "at least" or "approx" in the statement |
| The AQS mark "e" (℮) is optional; using it brings the average quantity system's sampling obligations | 4.1, 4.16A, 4.34–4.44 | Warning when the statement contains "e" or "℮" on its own |
| "Net" or "nett" is not required | (none) | Allowed either way; the app's example is "Net 250 g" |
| On the principal display panel, clear, conspicuous and easily read, in a colour that contrasts distinctly with the background | 4.9(2), 4.10(1), 4.12(2)(a) | Info on every label: put it on the front in a contrasting colour, and repeat it there if this label goes on the back. The app prints black on the label stock |
| Minimum character height by the package's largest dimension: 120 mm or less 2.0 mm; over 120 to 230 mm 2.5 mm; over 230 to 360 mm 3.3 mm; over 360 mm 4.8 mm | 4.12(3) | Printed at least this high in the sticker PDFs and the pack artwork (below). Info when the package size is missing: the 2.0 mm band is used |
| The largest dimension is, for a rectangular package, the greatest of breadth, height and length; otherwise the greatest of height, length and maximum diameter. The height applies to every character, unit symbols included | 4.12(4) | The user enters package_max_dimension_mm |
| Serving size × servings per package should equal the net quantity | (consistency check) | Warning when they differ by more than 10%; not a legal tolerance |
Character height. 4.12(4)(c) applies the minimum to each character, unit symbols included, and
neither the Regulations nor any NMI guide found says lowercase letters are measured differently. The
app takes the conservative reading: the shortest printed characters, the lowercase unit letters
("g", the "m" in "mL"), must reach the minimum at their x-height. The net quantity's type size is
the minimum height in points divided by the font's x-height (OS/2 sxHeight, 1082/2048 em for
Liberation Sans), rounded up to 0.1 pt (artwork.net_quantity_size_pt()): 10.8 pt for 2.0 mm,
13.5 pt for 2.5 mm, 17.8 pt for 3.3 mm and 25.8 pt for 4.8 mm. Digits and capitals then stand
taller, at the cap height (1409/2048 em). If NMI guidance ever says lowercase is measured by cap
height, switch to cap height; sizes would drop by about a quarter. The line is never set smaller
than the body type. In the sticker PDFs it keeps this size while the shrink-to-fit loop reduces the
rest of the label; if the label then doesn't fit, the PDF is refused with a message that gives the
net quantity size. The pack artwork sets the net quantity line in the details block the same way,
and its README says so and that the line must be on the front.
Not modelled: Schedule 5, which overrides the volume/mass rule for some goods; wine and other goods with their own rules; the dressed poultry and preserved sausage exceptions to the minimum measurement ban; the average quantity system (the "e" mark's sampling, 4.34–4.44) and the tolerances for short measure; multiple-unit packs and the other exemptions in Schedule 4.
Health Star Rating (voluntary)
HSR is not part of the Food Standards Code. It's a voluntary government scheme (Guide Section 1); the HSR website has a page on a proposal to mandate it, so check its status before launch. Sources:
- Health Star Rating System Implementation Guide v9.1 ("Guide"), Appendix 1.
- The official HSR Calculator 4.2 workbook (
HSR_Calculator_4.2.xlsm), sheets "HSR Calculator", "Points Table A", "Points Table C", "Non-Dairy Beverages ONLY Calc." and "Lookups".
The engine is labelwise/hsr.py. Tests check it against 58 worked cases taken from the Guide and
the workbook. They also run 20,000 random inputs through tests/hsr_workbook_port.py, a line-by-line
port of the workbook formulas, and require identical results. A 200,000-case run found no
differences.
How it works
- Categories: 1 (non-dairy drinks, assessed per 100 mL), 1D (dairy drinks), 2 (other foods), 2D (dairy foods), 3 (oils and spreads) and 3D (cheese with calcium above 320 mg/100 g). The user picks the category.
- Inputs: the values as the nutrition panel declares them (3 significant figures), so the rating matches what the official calculator gives for the panel on the pack.
- Baseline points: energy, saturated fat, total sugars and sodium, per 100 g or 100 mL as sold (as prepared for drinks diluted with water, below; Guide Tables 1–3). Categories 3 and 3D use Table 2 for saturated fat and sugars. Category 1 uses Table 3, where even 0 kJ scores 1 energy point.
- V points (fruit, vegetables, nuts and legumes, "FVNL"; Table 4): these come from the HSR group
tag on each ingredient. The FVNL percentage uses the same base as characterising-ingredient
percentages, i.e. ingoing weight less added water removed in processing. The user can override it.
- With only concentrated (dried) fruit or veg, the concentrated column is used (≥ thresholds).
- With a mixture, the Guide's formula is used: 100 × (FVNL + 2 × concentrated) / (FVNL + 2 × concentrated + other), rounded to 2 decimal places, then the FVNL column (> thresholds).
- The app suggests tags from the ingredient name. Every suggestion should be checked: for example, peanut butter counts but cocoa, coconut milk and oils don't.
- P and F points (protein and fibre; Table 6): the 2-point protein row is "≥ 3.2 g"; every other
row is "greater than". Protein counts only when baseline points are below 13 or V points are 5 or
more. Category 1 is scored on energy and sugars less V points only, with no saturated fat, sodium,
protein or fibre points (Table 3; the workbook); category 1D earns no fibre points (Table 6). So
neither drink category earns fibre points, and a drink is never provisional for missing fibre
(
test_category_1_scores_energy_sugars_and_v_only,test_drinks_never_provisional_for_fibre). The FSANZ ingredient data has no fibre values. While any ingredient has no fibre value, the rating is marked provisional ("3.5 stars or more") with a warning that names the ingredients; the user enters fibre from supplier specifications. - Stars: from the score (baseline − V − P − F) using the category's column in Table 7. Calculated category 1 drinks can reach at most 4 stars.
- Automatic ratings (Guide 4.1): plain water 5 stars; unsweetened flavoured water 4.5 stars; fresh or minimally processed fruit and vegetables 5 stars.
- As prepared, diluted with water (Guide v9.1, the "as prepared" provisions): a food the directions say to prepare with water only is rated as prepared; preparing with anything else (milk etc.) hasn't been allowed since the 2020 changes. The user enters the parts of water per 1 part of product by volume (4 for "1 part cordial to 4 parts water"). Each per-100 mL value the panel declares is divided by 1 + N, since water has no nutrients; volumes are taken to add up, and the product's specific gravity is taken as 1 for the mixing. The FVNL % is unchanged. Only drink categories (1 and 1D) can be rated this way; any other category is an error. The nutrition panel stays as sold (1.2.8); an as-prepared column is optional and not offered. An info note reminds the user that the directions on the pack must give the same dilution.
Where the Guide and the workbook differ, the app follows the workbook, because it's the official calculator businesses use:
- Inputs are rounded before scoring, as the workbook instructs: energy and sodium to whole numbers, grams to 0.1 g. Halves round up, as in Excel.
- In the FVNL column, the workbook gives 8 V points from 99.51% (the concentrated-only column, from 99.5%). The Guide gives them only at 100%.
- Category 1 takes a single FVNL percentage. Concentrated FVNL isn't treated separately, per the workbook's amendment history.
Not covered: infant foods, sports foods, alcoholic drinks and kava, which are out of scope for HSR. Also not covered: the "as prepared" rules for foods that are drained before eating (the Guide rates them drained), which need the drained food entered as the recipe.
Artwork (labelwise/hsr_mark.py, tools/extract_hsr_artwork.py; Guide Sections 5 and 6)
| Rule | Source |
|---|---|
| The graphic is the government's own artwork: the shapes are read from the official layered artwork file (DT0003621, from the HSR Calculator page) and drawn unchanged, only scaled in proportion. Stylised versions aren't permitted | Guide 5.2 |
| Only the stars element is drawn ("Health Star Rating" alone, option 4 of the hierarchy). It must be part of every use of the system; the energy and nutrient icons are optional | Guide 5.1, 6, Figure 4.4 |
| All five stars are visible, with shading behind the rating's stars, the rating as a numeral, and the words "Health Star Rating" | Guide 5.2, 5.3 |
| No graphic is made while the rating is provisional (fibre values missing) | consumer law: a rating on pack must be right |
| The business chooses the size and colour; it must be legible and contrast with the background. The app offers 12–60 mm, in black | Guide 5.2 |
It goes on the front of the pack: it's in the artwork download, and printed on the label only when asked (with_hsr), for labels that wrap the front |
Guide 5.2 |
Not implemented: the energy and nutrient icons (options 1–3 of the hierarchy) and the vertical layouts. Their lettering is live text in Helvetica Neue, a commercial typeface the app can't ship, and another typeface would be a stylised version. The Guide encourages businesses to use as many elements as the pack allows, so a business with room for them should use the official artwork file.
To confirm before launch: with the HSR Unit (hsr@health.gov.au) that software may generate the graphic for a business's own food labels. The Guide says the artwork is trademarked, that using it requires compliance with the Guide, and that copyright approval may be needed for uses "other than for food labelling purposes".
Country of origin (mandatory for most food)
The app turns this on for every new recipe and counts it in the label checklist.
Source: Country of Origin Food Labelling Information Standard 2016 (F2016L00528), made under the
Australian Consumer Law and enforced by the ACCC, not part of the Food Standards Code. Current
compilation F2017C00920 (12 Aug 2017), which is exempt from sunsetting. The Country of Origin Food
Labelling Style Guide v2.1 (Aug 2017) and the government's label templates (business.gov.au,
May 2025) were used for how the marks look. Section numbers below are sections of the Information
Standard. Engine: labelwise/coo.py; artwork: labelwise/coo_mark.py; tests:
tests/test_coo.py and tests/test_coo_mark.py. Where the Standard or Style Guide gives an
example, a test uses it.
Where the Standard does not apply. Part 2 applies to retail sales (s 14(1)). Under s 14(2) it does not apply to:
- (a) food sold for immediate consumption by a restaurant, canteen, school, caterer, prison or hospital;
- (b) food made and packaged on the premises where it is sold;
- (c) food delivered packaged and ready for consumption at the express order of the purchaser;
- (d) food sold at a fund-raising event;
- (e) food for special medical purposes.
The scope check refuses (a) and (e) (see "Scope"). The off switch for the country of origin label exists for (b) to (d).
What label the food needs
| Food | Label | Sections |
|---|---|---|
| Priority food grown, produced or made in Australia from only Australian ingredients | Kangaroo logo, full bar chart, e.g. "Grown in Australia", "Made in Australia from 100% Australian ingredients" | s18 |
| Priority food made in Australia with some imported ingredients | Logo, bar chart, "Made in Australia from at least P% Australian ingredients"; under 10% may say "from less than 10%"; 0% says "from 0%" or "from imported ingredients" | s19 |
| Priority food packed in Australia, made in one other country | A boxed statement "Made in C", or a mark without the logo: "Made in C from (at least P% / 100% / less than 10%) Australian ingredients", optionally with "Packed in Australia" | s22(2)–(4) |
| Priority food packed in Australia, grown, produced or made in one other country with no Australian ingredients | Boxed "X C", or a mark with an empty bar chart: "X C" + "Packed in Australia" | s22(5) |
| Priority food packed in Australia from several countries | "Packed in Australia from at least P% / less than 10% / 0% / imported ingredients" with bar chart, no logo | s22(6)–(8) |
| Imported priority food | Boxed statement "Made in C", "Product of C" or, from several countries, "Packed in C from imported ingredients"; optionally a mark showing Australian ingredients | s15(3)–(4), s26 |
| Non-priority food (seasonings, confectionery, biscuits and snack food, bottled water, soft and sports drinks, tea and coffee, alcoholic drinks, as defined in the Dictionary) | A plain statement, e.g. "Made in Australia"; the marks are optional | s9, s24, s25 |
| Small package (under 100 cm², Food Standards Code 1.1.2—2) | The mark's words in a box, without the logo or bar chart | s14(3) |
"Grown", "produced" and "made" follow s8: grown or produced means every significant ingredient is from that country and virtually all processing happened there; made means last substantially transformed there. If a "grown" or "produced" food has any ingredient that isn't Australian, s18 doesn't apply and the mark says "Made in Australia" (s19). Packed in Australia uses "Packed" or "Packaged" (the user chooses). The optional phrase naming an ingredient's origin ("with prawns from Thailand") must start with "with" and is only offered where the Standard allows it (s19(5), s22(9), s26(5)).
The Australian percentage
- Share of Australian ingredients by ingoing weight of all ingredients (s11(1)), calculated with exact fractions. It isn't the same base as characterising-ingredient percentages: water lost in cooking isn't removed.
- A compound ingredient made from both Australian and imported ingredients counts pro rata (s11(4)(b)); the user enters its Australian percentage (the s11 lasagne example is a test).
- Added water takes the origin of where it was collected (s12(3)), so water in food grown, produced or made in Australia counts as Australian unless the user sets it otherwise. Food that is only packed here had its water added overseas, so the user must set its origin.
- 'P%' is "a whole number … not more than the proportion" (e.g. s19(2)(b)): always rounded down, so 99.99% is "at least 99%".
- The user can type the percentage instead, e.g. from supplier records. The Standard requires records to be kept for a year (s31).
The mark
- Bar chart (s10 table and Style Guide p3, p8): four markings at 20% steps; filled to the 10% step below P for 10–94%, to 95% for 95–99%, to 5% for "less than 10%" and for P under 10, empty for 0%, full for 100%. Tests cover the Style Guide's 77% and 97% examples.
- Kangaroo logo: vector paths from the government's label template, used unchanged. The logo can only be used as part of a standard mark (s29; Style Guide p9), so the app never shows it alone.
- Proportions (box, padding, logo and bar sizes, markings) are taken from the government templates and scaled to the chosen type size. Portrait and landscape layouts, as in the Standard; landscape breaks lines between phrases as in the Standard's figures.
- Colours: Pantone 341 green (CMYK 100/0/67/29) and Pantone 137 gold (CMYK 0/35/90/0) with 100% K text, or one colour (100% K and a 40% K tint), which the Style Guide allows.
- Typeface: the Style Guide recommends Interstate Bold, a commercial font. Liberation Sans Bold is used instead; the recommendation isn't a requirement.
- The Standard only requires the text to be legible and to contrast with the background (s28). The Style Guide recommends 3 mm clear space and not using the base; the artwork README says so.
Not covered
- Marks for Australian content that varies over time, using an average and a way to get details (s21, s23, s13).
- Food exported and re-imported with processing overseas (s20).
- Unpackaged food and fresh fruit and vegetables in transparent packaging (s16, s17), and "Australian [kind of food]" / "[C] [kind of food]" wordings.
- Deciding whether a process is a "substantial transformation": the app explains the test, the business decides.
To confirm before launch: that generating the standard mark, including the kangaroo logo from the government template, inside a paid product is fine with the Department of Industry and the Australian Made Campaign (which owns the logo trade mark). The Standard lets food businesses use it in their labelling (s29), and the government provides the templates for businesses to create their own labels.
Compilations checked
| Instrument | Title ID | Compilation used |
|---|---|---|
| Standard 1.1.1 | F2015L00383 | F2026C00543 (Compilation No. 17, 9 Jun 2026) |
| Standard 1.1.2 | F2015L00385 | Compilation No. 21 (9 Jun 2026) |
| Standard 1.2.1 | F2015L00386 | F2026C00538 (9 Jun 2026) |
| Standard 1.2.2 | F2015L00389 | F2024C00726 |
| Standard 1.2.3 | F2015L00397 | F2024C01143 (29 Oct 2024) |
| Standard 1.2.4 | F2015L00392 | F2025C00914 |
| Standard 1.2.5 | F2015L00401 | F2021C00660 |
| Standard 1.2.8 | F2015L00395 | F2026C00411 (30 Apr 2026) |
| Standard 1.2.10 | F2015L00398 | F2018C00945 |
| Schedule 7 | F2015L00477 | As made (1 Apr 2015), the only version on the register |
| Schedule 8 | F2015L00478 | Compilation No. 8 (16 Sep 2025) |
| Schedule 9 | F2015L00479 | F2021C00195 (25 Feb 2021) |
| Schedule 11 | F2015L00481 | F2024C01138 |
| Schedule 12 | F2015L00482 | F2017C00342 |
| Country of Origin Food Labelling Information Standard 2016 | F2016L00528 | F2017C00920 (12 Aug 2017) |
| National Trade Measurement Regulations 2009 | F2009L03479 | F2024C00311 (Compilation No. 8, 20 Mar 2024) |