Menu
The Batch Process Data Model: Recipes, Lots and Yield

The Batch Process Data Model: Recipes, Lots and Yield

Field tables, scaling arithmetic and genealogy for process batches: recipes, batch orders, lot consumption, in-process results, yield and recall traces.
The Batch Process Data Model: Recipes, Lots and Yield

Key takeaways

  • A recipe is not a bill of materials. It is written against a standard batch size, it scales, its ingredients vary in strength, and it produces a variable quantity of more than one thing.
  • Three fields carry that difference: standard_batch_size on the header, scaling_role on the line, and potency_basis on any ingredient whose strength moves lot to lot.
  • In the worked example, a 1,000.000 kg recipe scaled to 1,750.000 kg needs 765.625 kg of a 96.0% active lot instead of 735.000 kg, and the compensating line drops by exactly 30.625 kg.
  • One batch honestly yields 95.00%, 97.94% or 93.71%. Nothing changes but the denominator, so every yield figure has to carry one.
  • A parent_batch_id column cannot survive a merge. Genealogy stored as edge rows with a quantity_contributed finds 2,000.000 kg of affected product where the column finds 1,000.000 kg.

Why a recipe is not a bill of materials

A discrete bill of materials says one finished unit contains two brackets and eight bolts. Multiply by the order quantity and you have the requirement.

A recipe, also called a formula or a master formula, does none of that. It describes a batch, and the batch size is a decision made when the order is raised.

The discrete side is modelled in our BOM and routing data model. This page is its process counterpart and deliberately does not repeat it.

QuestionDiscrete BOMProcess recipe
Quantity written asFixed amount per unitPer­cent of the batch
Sized byOrder quantityBatch size
OutputOne part numberPrimary plus co-products
Output quantityPlanned, met or shortVariable, weighed
Input strengthAssumed constantAssay per lot
StepsRouting oper­ationsPhases with para­meters
LossScrapped unitsMass that never appears
ScalingEvery line scalesScaled, fixed and com­pen­sating lines

Six of those eight rows need a field a BOM line has nowhere to put. That is the argument for a separate model.

Supplier lots, lot status and the trace itself are in our lot traceability data model. Here the concern is the batch that consumes those lots.

The objects in a batch model

Eight objects carry a process batch from definition to release. Each is a table with its own key.

ObjectOne row per
Recipe headerProduct, version, site
Recipe lineIn­gre­dient on that recipe
Recipe outputThing the batch produces
Batch orderOne batch actually made
Batch phaseStep inside that batch
Con­sump­tion recordLot issued to a phase
Quality resultTest on one sample
Gene­alogy edgeLink between two lots

The recipe header

One row per product, per version, per site. It holds no ingredients, only the frame they are measured against.

FieldTypeWhy
recipe_idText, keyStable code, never reused
versionText, yesRaised, never edited in place
product_idText, yesThe primary output
standard_batch_sizeDeci­mal, yesEvery line is a share of this
batch_size_uomText, yeskg, L or t. State it
min_batch_sizeDeci­mal, yesBelow it the process is invalid
max_batch_sizeDeci­mal, yesAbove it the vessel over­flows
scaling_ruleEnum, yesLinear, stepped or none
expected_yield_percentDeci­mal, yesPlanned output from the charge
equip­ment_classText, yesTrains this recipe is valid on
effective_fromDate, yesFirst date it may be used
effective_toDate, noLast date, or open
recipe_statusEnum, yesDraft, Approved, Super­seded
approved_byText, yesWho signed the version off
change_control_refText, noWhy the version was raised

standard_batch_size is the field everything hangs off

A recipe line written as 42.000% means nothing until you know what it is 42.000% of. The header answers that, with a quantity and a unit.

min_batch_size and max_batch_size are not documentation. They are the envelope in which the recipe was validated, and a batch order outside it should be refused rather than warned about.

Why the scaling rule needs three roles, not one

Most ingredients scale with the batch. Some do not, and scaling those is the most common process error in a system built on a BOM.

scaling_roleBehaviour
ScaledMulti­plied by the scale factor
FixedSame amount at any batch size
Com­pen­satingAbsorbs the differ­ence, makes the total

A Fixed line is a cleaning charge, a seed charge, a fixed volume of nitrogen blanket or a catalyst dosed to the vessel rather than to the mass. Scale it and you either waste it or poison the batch.

A Compensating line is usually the bulk carrier, the water or the diluent. It takes whatever quantity makes the charge add up after every other line is settled, which is what makes potency correction possible at all.

Exactly one line per recipe may be Compensating. Two of them and the arithmetic has no single answer, so the system will pick one silently.

The recipe line

One row per ingredient, keyed by recipe and line number so the same material can appear twice at different phases.

FieldTypeWhy
recipe_id + line_noText, key10, 20, 30 with gaps
material_idText, yesWhat is charged
quantity_typeEnum, yesPer­cent, absolute or ratio
quantity_valueDeci­mal, yesRead against quantity_type
uomText, yesThe in­gre­dient's unit, not the batch's
scaling_roleEnum, yesScaled, Fixed or Com­pen­sating
potency_basisDeci­mal, noAssay the quantity assumes
potency_adjustBool, yesCorrect the weight by lot assay
phase_seqText, yesWhere it is charged
addi­tion_orderInteger, yesSequence inside the phase
addi­tion_rateDeci­mal, noDose over time, not all at once
toler­ance_percentDeci­mal, yesHow far the weigh may deviate
sub­sti­tute_groupText, noApproved alter­na­tives
is_pro­cessing_aidBool, yesRemoved again, not in the product

potency_basis and potency_adjust are the pair with no equivalent on a BOM line. A bolt is a bolt, but an enzyme, a vitamin premix or a caustic solution arrives at whatever strength the supplier's lot reached.

is_processing_aid matters for the mass balance, because a solvent charged and then distilled off is input mass that was never meant to leave as product. Leave it unflagged and every batch looks like it lost material.

Worked example: scaling a 1,000 kg recipe to 1,750 kg

Recipe RCP‑8820 version B is defined at a standard batch size of 1,000.000 kg, with a min of 500.000 kg and a max of 2,000.000 kg. Order BATCH‑2026‑0431 asks for 1,750.000 kg.

Recipe linePer­centRole
RM-4471 active42.000Scaled, potency
RM-2210 carrier50.000Com­pen­sating
RM-0915 binder6.500Scaled
RM-7003 lubri­cant1.500Scaled
Total100.0001,000.000 kg

1. Turn percentages into a standard charge

At the standard batch size, each percentage is multiplied by 1,000.000 kg. The four lines come to 420.000, 500.000, 65.000 and 15.000 kg.

Those add to 1,000.000 kg, which is the first check the system should run and refuse to save without.

2. Apply the scale factor

The scale factor is the actual batch size divided by the standard, so 1,750.000 divided by 1,000.000 gives 1.75. Store it on the batch order rather than recomputing it later.

LineStandard kgScaled kg
RM-4471420.000735.000
RM-2210500.000875.000
RM-091565.000113.750
RM-700315.00026.250
Total1,000.0001,750.000

The scaled column adds back to 1,750.000 kg, which is the second check. A recipe with a Fixed line would not balance here, and the compensating line closes it.

3. Correct the active for lot potency

RM‑4471 carries potency_basis 100.0%, meaning the 42.000% is stated on a pure active basis. The lot dispensed, LOT‑A‑5512, assays at 96.0%.

The weight to issue is the target active divided by the lot assay: 735.000 divided by 0.960 gives 765.625 kg. Check it back: 765.625 multiplied by 0.960 returns 735.000 kg of active.

The difference, 30.625 kg, is not active. It is whatever else is in that lot, and it has just been added to the vessel.

4. Let the compensating line absorb the overage

If nothing else moves, the charge is now 1,780.625 kg and the batch is over the target by 30.625 kg. The compensating line takes the correction instead.

LineScaled kgIssued kg
RM-4471735.000765.625
RM-2210875.000844.375
RM-0915113.750113.750
RM-700326.25026.250
Total1,750.0001,750.000

RM‑2210 drops from 875.000 to 844.375 kg, exactly the 30.625 kg that came in with the active. The charge still totals 1,750.000 kg and still contains 735.000 kg of active.

Without a compensating role the plant has two bad options: charge 1,780.625 kg into a vessel validated for 1,750.000, or dose 735.000 kg of a 96.0% lot and run 4.0% under strength. Both are data model failures, not operator failures.

The batch order header

One row per batch actually made. It copies from the recipe rather than pointing at it, for the same reason a work order copies a BOM.

FieldTypeWhy
batch_idText, keyThe number on the label
recipe_id + versionText, yesCopied, not referenced
planned_batch_sizeDeci­mal, yesWhat was ordered
scale_factorDeci­mal, yesStored, so it survives a recipe change
actual_charge_qtyDeci­mal, yesSum of the con­sump­tion rows
equip­ment_train_idText, yesWhich line or vessel set
actual_start / endTime­stamp, yesBounds the machine data
theo­reti­cal_yieldDeci­mal, yesCeiling the batch is judged against
actual_yieldDeci­mal, yesGood output, weighed
yield_percentDeci­mal, yesStore the de­nomi­nator with it
batch_statusEnum, yesPlanned, Running, Complete
dis­po­si­tionEnum, yesQuaran­tine, Released, Rejected
expiry_dateDate, yesSet at completion, not at receipt
retest_dateDate, noEarlier than expiry for actives
cam­paign_idText, noGroups batches on one changeover
de­vi­ation_countInteger, yesZero is a claim, so make it explicit
released_byText, noWho let it go to stock

batch_status and disposition are two fields, not one. A batch can be physically complete and still be in quarantine, and a batch can be rejected halfway through while still running to a safe stop.

The order itself follows the pattern in our production work order data model. The fields above are what process adds on top.

The batch phase

A phase is a step inside the batch, and it is not a routing operation. An operation names a work centre and a time, while a phase names a vessel and the parameters that decide whether the material is correct.

FieldTypeWhy
batch_id + phase_seqText, key010, 020, 030 with gaps
phase_codeEnum, yesDis­pense, Charge, Mix, Dry, Blend
equip­ment_idText, yesThe vessel or machine used
qty_inDeci­mal, yesMass entering the phase
qty_outDeci­mal, yesMass leaving it
planned_durationDeci­mal, yesFor the schedule, not the record
actual_start / endTime­stamp, yesWhat really happened
is_hold_pointBool, yesNothing proceeds without a result
cycle_noInteger, yesWhich pass, when a phase repeats
oper­a­tor_idText, yesWho ran it
verified_byText, noSecond signature where required
clean_statusEnum, yesState the vessel was in before
phase_statusEnum, yesPending, Running, Complete, Aborted

cycle_no is the field that separates a phase from an operation. A routing runs 010, 020, 030 once each, while a mill and test loop may run three times before the particle size is in range.

qty_in and qty_out on every phase are what make a loss locatable. A batch that finishes 66.500 kg light tells you nothing, but a drying phase taking 1,750.000 kg in and giving 1,715.000 kg out points at the dust filter.

Process parameters belong in a child table of the phase, one row per parameter with target, minimum, maximum and recorded value. Columns on the phase itself work for one product and break on the second.

The material consumption record

One row per lot issued to a phase. This table is the genealogy, the mass balance and the cost at once, so missing fields here are expensive.

FieldTypeWhy
con­sump­tion_idText, keyOne event, not one material
batch_id + phase_seqText, yesWhere it went in
recipe_line_noText, yesWhich line it satisfies
material_idText, yesWhat was issued
raw_material_lot_idText, yesThe gene­alogy link
qty_issuedDeci­mal, yesWhat left the store
qty_returnedDeci­mal, yesWhat came back unused
qty_netDeri­ved, yesIssued minus returned
uomText, yesAs weighed, not as bought
assay_percent_usedDeci­mal, noFrozen at dis­pens­ing
scale_idText, yesWhich balance weighed it
weigh_ticket_idText, noPrinted evi­dence
issued_by / atText, yesWho and when
reversal_ofText, noCorrect by reversing, never by editing

qty_returned is the field most systems leave out, and it breaks the mass balance immediately. Dispensing opens a 25.000 kg sack, weighs out 17.000 kg and returns 8.000 kg to the store.

Record only the issue and the batch is charged 25.000 kg it never received, so the yield looks 8.000 kg worse and the store shows stock it does not have. Both errors are silent.

assay_percent_used must be stored on this row, not read from the lot when a report runs. Lots get re-assayed, and a figure that moves after the fact rewrites a correction that was already made.

The in-process quality result

One row per test on one sample. The batch record is mostly made of these, and two choices decide whether they are usable.

FieldTypeWhy
result_idText, keyOne test on one sample
batch_id + phase_seqText, yesWhere it was taken
sample_idText, yesSeveral tests share one sample
sample_pointText, yesTop, middle, bottom, outlet
sample_timeTime­stamp, yesNot the time it was tested
test_codeText, yesAssay, mois­ture, pH, viscosity
result_valueDeci­mal, yesA number, never free text
result_uomText, yesPer­cent and ppm are not the same
spec_min / maxDeci­mal, yesCopied onto the row
spec_versionText, yesWhich spec it was judged by
pass_failEnum, yesDerived once, then frozen
method_idText, yesMethods change, results do not
in­stru­ment_idText, noLinks a drift to a cali­bra­tion
retest_ofText, noPoints at the result it repeats
result_statusEnum, yesDraft, Approved, Can­celled

Store result_value as a number. The moment a result can read "approx 41.4" or "within spec", the column stops supporting a trend, a control chart or a query for batches near a limit.

Copy spec_min, spec_max and spec_version onto the result row. Specs get tightened, and a stored pass that recomputes to a fail two years later is worse than either answer on its own.

A failing result is never deleted. It gets result_status Cancelled with a reason, or a new row pointing back at it through retest_of, so the pattern of retests stays visible.

These numbers also feed the control charts that statistical process control is built on. A control limit is not a specification limit, and the two need separate fields.

Yield, and the three denominators it can have

Yield is the number every process plant reports and the one most often quoted without a denominator. Three definitions must be kept apart.

Theoretical yield is the maximum output the charge could give, which for a blend is the mass charged and for a reaction is the stoichiometric ceiling. For BATCH‑2026‑0431 that is 1,750.000 kg.

Actual yield is the good output, weighed at the point it becomes stock. The batch gave 1,662.500 kg.

Yield percentage is actual divided by theoretical, so 1,662.500 divided by 1,750.000 gives 95.00%. The recipe expected 97.0%, meaning a planned output of 1,697.500 kg, so the batch is 35.000 kg light.

De­nomi­natorCalcu­lationResult
The charge1,662.5 / 1,750.095.00%
The plan1,662.5 / 1,697.597.94%
The active688.75 / 735.093.71%

None of those three is wrong, and a plant reporting the highest is not lying. The failure is reporting one without naming its denominator, so yield_percent needs a companion field saying what it was divided by.

Yield loss is not scrap

The 87.500 kg that did not become good product splits into two kinds, and they need different handling.

Where the charge wentkgShare
Good output1,662.50095.00%
Rejected, so scrap21.0001.20%
Yield loss66.5003.80%
Charged1,750.000100.00%

Scrap is material that became product and was then rejected. It has a weighable quantity, a reason code from our scrap reason code taxonomy, a disposition and sometimes a rework route.

Yield loss is material that never became product at all. It is a difference between two weighings: dust in a filter, film on a vessel wall, heel in a pipe, samples the laboratory consumed.

Scrap can be attributed and yield loss usually cannot, unless qty_in and qty_out are recorded per phase. Of the 66.500 kg here, 35.000 kg was caught in the dust filter and 31.500 kg was retention and sampling.

Mass yield against active yield

The blend was charged with 735.000 kg of active in 1,750.000 kg of material, a target concentration of 42.000%. The loss streams were not uniform, and the dust filter caught material assaying 70.0% active.

StreamkgActive kg
Charged1,750.000735.000
Dust filter35.00024.500
Reten­tion, samples31.50013.050
Rejected21.0008.700
Good output1,662.500688.750

The dust filter removed 24.500 kg of active with only 35.000 kg of mass, so the blend that carried on assays 710.500 divided by 1,715.000, that is 41.43%. Retention, samples and rejects then leave at that same concentration.

Good output therefore holds 688.750 kg of active, and both columns reconcile: the four loss and output rows add to 1,750.000 kg and to 735.000 kg.

Mass yield is 95.00% while active yield is 93.71%, a gap of 1.29 percentage points. That gap is the whole diagnosis: the plant did not lose blend, it lost the expensive part of the blend.

The batch still passes a specification of 42.00% plus or minus 1.00, because 41.43% sits inside it. A tighter specification would have rejected it, and a model storing only mass yield cannot see the problem at all.

Co-products, by-products and the single output field

A batch order with one output_quantity field works until the first plant that sells its fines, recovers its solvent or renders its trim. Then it stops working permanently.

output_roleWhat it isCosted as
PrimaryThe product orderedCarries the batch cost
Co-productSecond saleable outputShares cost by value
By-productMinor, saleable or reusableCredited, not costed
ReworkGoes back into a future batchValued at input cost
WasteLeaves as lossNo value, still weighed

Give the batch a batch_output table with one row per output, carrying output_role, material_id, lot_id, a quantity and a flag for whether it counts in the yield numerator. That last flag is what a single field can never express.

Take the 35.000 kg of fines from the example. Thrown away, they are yield loss and the batch yields 95.00%.

Recovered and sold as by-product FG‑8821 at 70.0% active, the same batch produces 1,697.500 kg of saleable output, which is 97.00% of the charge and exactly the planned quantity. Active recovery rises from 93.71% to 97.04%.

The physical batch is identical in both readings. Only the model changed, and with it every yield report, every cost per kilogram and the answer to whether this recipe is performing.

Cost allocation needs the same field. Co-products split the batch cost by relative sales value or by mass, while by-products are usually credited at net realisable value, and one output_quantity cannot carry both rules.

Splits, merges and the edge that survives them

Genealogy in a process plant is not a tree. Batches split into sublots, sublots merge into homogenisation lots, rework goes back into a later batch, and the graph has cycles.

The instinct is to put a parent_batch_id column on the child lot. It holds for exactly as long as no batch is ever merged.

FieldTypeWhy
edge_idText, keyOne link, one row
parent_lot_idText, yesWhat went in
child_lot_idText, yesWhat came out
edge_typeEnum, yesCon­sume, Split, Merge, Repack, Rework
quantity_con­tri­butedDeci­mal, yesHow much of the parent is in the child
uomText, yesSame unit as the parent lot
event_timeTime­stamp, yesOrders the graph
batch_idText, noThe batch that created the link
source_doc_idText, yesCon­sump­tion or transfer record
reversed_byText, noEdges are cancelled, never deleted

quantity_contributed is the field that saves a merge. Without it an edge says only that two lots are related, so a forward trace lists the affected lots but cannot size the exposure.

The recall walk, with and without the edge

LOT‑A‑5512 is 765.625 kg of the 1,750.000 kg charged, so it is 43.75% of that batch by mass. A supplier notifies a problem with it and the trace starts.

The 1,662.500 kg of good output was split into two sublots: SL‑0431‑A of 1,000.000 kg and SL‑0431‑B of 662.500 kg. SL‑0431‑B was then blended with 337.500 kg from BATCH‑2026‑0438 to make HOM‑0912, a 1,000.000 kg homogenisation lot.

LotkgShare of HOM-0912
SL-0431-A1,000.000Not in it
SL-0431-B662.50066.25%
From BATCH-2026-0438337.50033.75%
HOM-09121,000.000100.00%

With edge rows, the forward trace returns SL‑0431‑A and HOM‑0912, so 2,000.000 kg of finished product is in scope. It also shows that 337.500 kg of that, the contribution from the other batch, is innocent material caught by the blend.

With a parent_batch_id column on HOM‑0912, whichever batch the operator or the import routine named wins. Name BATCH‑2026‑0438 and the trace returns 1,000.000 kg, missing half the affected mass, with no error anywhere.

The split does the same damage in a quieter way. Record it as a relabel from BATCH‑2026‑0431 to SL‑0431‑A and sublot B has no parent at all, so it is invisible to a backward trace from the customer.

Two rules keep this working. Every quantity movement writes an edge, and an edge is cancelled with reversed_by rather than deleted, because a deleted edge leaves a lot that appears to have come from nowhere.

The trace mechanics themselves are in the lot traceability data model. What batch manufacturing adds is the merge, and the merge is what a column cannot hold.

Five questions to ask a vendor about batch data

  1. Show me a recipe scaled to a non-standard batch size, with a potency correction. Ask which line absorbed the difference, where the assay it used was stored, and what happens when two lines are marked compensating.
  2. Where is yield_percent defined, and what is its denominator? Ask whether the field records that denominator, and whether processing aids and by-products sit inside or outside it.
  3. Can one batch have several outputs with different roles? Ask to see a by-product credited on a real batch and whether it counts toward yield. If the answer needs a second batch order, it is a workaround.
  4. Merge two lots and run a forward trace from the raw material lot. Ask for both affected lots and the contributed quantity of each. It takes ten minutes and most systems fail it.
  5. What happens when dispensing returns unused material? Ask to see qty_issued, qty_returned and the resulting mass balance, then how a mis-keyed weight is corrected.

Run all five against your own recipe and lots, in a live system. Demonstration data was built to make the demonstration work.

How Fabrico helps

Fabrico does not hold recipes, formulas or batch orders, and it does not do MRP, production planning or scheduling. Those belong in your ERP, MES or process historian.

Its inventory covers maintenance spares, with minimum and maximum levels and consumption booked against a work order. It is not a raw material store, so it holds no lots, assays or dispensing records.

What Fabrico owns is the equipment those batches run on. It measures OEE, downtime, short stops and cycle times through PLC connections, IoT sensors and AI cameras, in real time.

That matters here in one specific way. If the batch_id and equipment_train_id travel with the machine data, a stalled dryer or an overlong phase is attributed to the batch that was running.

Whether that identifier crosses the integration depends on what your MES or historian sends, so ask for the mapping in writing. Our page on the manufacturing execution system covers that boundary.

On maintenance, Fabrico is a full CMMS: your team raises work orders, runs preventive plans and recurring templates, scans QR codes on machines and parts, and works from web or mobile apps. The structure behind it is our CMMS data model.

Cleaning and changeover between campaigns is maintenance work, planned and recorded alongside the breakdowns, with approvals and shifts. The AI assistant answers questions about one machine's losses and history in plain language.

How those losses are calculated is in our OEE calculation guide. Fabrico is ISO 27001 certified and its interface runs in several languages.

Want to see whether your equipment is the reason a batch missed its yield? Book a 30 minute demo with a Fabrico consultant, no commitment, or contact us with your questions.

Frequently asked questions

What is the difference between a recipe and a bill of materials?

A BOM gives a fixed quantity per finished unit, while a recipe gives a percentage of a standard batch size, scaled to the batch actually made. A recipe also carries potency adjustment, phases instead of operations, and more than one output.

What fields does a batch order need?

It needs batch_id, the copied recipe_id and version, planned_batch_size, scale_factor, actual_charge_qty, equipment_train_id, actual start and end, theoretical_yield, actual_yield, yield_percent, batch_status, disposition and expiry_date. Keep batch_status and disposition as separate fields.

How do you calculate batch yield?

Divide the good output by the theoretical yield, which for a blend is the mass charged. In the example, 1,662.500 kg from a 1,750.000 kg charge is 95.00%, and the same batch reads 97.94% against the planned output.

What is the difference between yield loss and scrap?

Scrap became product and was then rejected, so it carries a quantity, a reason code and a disposition. Yield loss never became product, shows up as a difference between two weighings, and is locatable only if each phase records quantity in and quantity out.

Why does a merged batch break lot traceability?

Because a parent_batch_id column on the child lot holds only one source, every other batch in the blend disappears from the trace. Edge rows with a quantity_contributed keep all sources and size each one's exposure.

Latest from our blog

Još uvek se pitate?
Proverite sami!
Još uvek se pitate?

Zakažite sastanak KSNUMKS-to-KSNUMKS sa našim stručnjacima ili se direktno upišite u naš besplatni plan.
Nije potrebna kreditna kartica!

By clicking the Accept button, you are giving your consent to the use of cookies when accessing this website and utilizing our services. To learn more about how cookies are used and managed, please refer to our Privacy Policy и Cookies Declaration