<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Isaac Mwendwa Muthui</title>
    <description>The latest articles on DEV Community by Isaac Mwendwa Muthui (@yit2022).</description>
    <link>https://dev.to/yit2022</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4175357%2Fb991557f-5312-494d-9c6e-ada13e38c838.jpeg</url>
      <title>DEV Community: Isaac Mwendwa Muthui</title>
      <link>https://dev.to/yit2022</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yit2022"/>
    <language>en</language>
    <item>
      <title>Modelling Environmental Monitoring in a Food ERP: Sampling Sites, Swab Results and the Workflow a Positive Should Trigger</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Sat, 31 Oct 2026 04:34:08 +0000</pubDate>
      <link>https://dev.to/yit2022/modelling-environmental-monitoring-in-a-food-erp-sampling-sites-swab-results-and-the-workflow-a-2kfm</link>
      <guid>https://dev.to/yit2022/modelling-environmental-monitoring-in-a-food-erp-sampling-sites-swab-results-and-the-workflow-a-2kfm</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/environmental-monitoring-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A swab result looks like the simplest record in a food factory: a site, a date, a pass or a fail. In practice it is one of the hardest to model well. The same "fail" can mean "re-clean the bench" or "hold three days of product", depending on where the sample was taken, what it was tested for, when it was taken relative to cleaning, and what has been produced since the last clear result. If the data model can't express that, the result ends up in a PDF from the lab and nobody can trend it.&lt;/p&gt;

&lt;p&gt;This post walks through how we think about modelling environmental monitoring in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/environmental-monitoring-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXG 61-2007&lt;/strong&gt; (Annex I) lists what a Listeria environmental monitoring programme must define: type of samples (food contact and non-food contact), target organisms, sampling locations and number of samples, frequency, tools, analytical methods, "a system to record the data and their evaluation, e.g. performing trend analyses", and actions on positive results. &lt;a href="https://www.fao.org/input/download/standards/10740/CXG_061e.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001&lt;/strong&gt;: environmental sampling "at a level commensurate with identifying the risk" (4.3.4), timely action when results exceed action limits (4.3.7), a per-line tracking system "to track the location of possible sources of contamination" (4.6.2), and "records of environmental tests" (14.3.4). &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 CFR 117.165(b)(3)&lt;/strong&gt; (a US benchmark, not Kenyan law): the written procedure must identify the test microorganisms, "the locations from which samples will be collected and the number of sites", the timing and frequency, the tests and methods, the laboratory, and the corrective action procedures. &lt;a href="https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-117/subpart-C/section-117.165" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translated into engineering terms: a sampling &lt;em&gt;plan&lt;/em&gt; is configuration, a &lt;em&gt;sample&lt;/em&gt; is an event against a site, a &lt;em&gt;result&lt;/em&gt; is a measurement against a limit, and a &lt;em&gt;positive&lt;/em&gt; is the start of a workflow, not the end of a record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sites are first-class entities
&lt;/h2&gt;

&lt;p&gt;The most common mistake is storing the location as free text on the sample. "Filler", "filler area", "near filler" and "Filler 2" become four sites and the trend disappears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EmSite      (id, code, area_id, zone, surface_type, food_contact bool, active_from, active_to)
            zone IN (1,2,3,4)
EmPlanItem  (id, site_id, target, method, frequency, timing, action_limit, owner_id)
            timing IN ('in_production','pre_startup','post_clean')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sites get stable codes that match the plant map (&lt;code&gt;RTE-Z1-03&lt;/code&gt;), a zone, and validity dates so the history survives a layout change. The plan item says what to test at that site, how often and when relative to cleaning. Timing matters: a post-clean swab verifies the sanitiser, a mid-shift swab looks for harbourage. Mixing them in one trend hides both signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Samples and results
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EmSample (id, site_id, plan_item_id, collected_at, collected_by, timing, pooled_group_id, lab_id, received_at)
EmResult (id, sample_id, target, method, value, unit, result_text, status, tested_at)
         status IN ('pass','fail','pending')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details come straight from the guidance. First, pooling: Codex allows compositing samples where sensitivity allows, but "in the case of positive findings additional testing will be necessary to determine the location". A &lt;code&gt;pooled_group_id&lt;/code&gt; lets a positive pool fan out into follow-up samples for each member site. Second, &lt;code&gt;received_at&lt;/code&gt;: if the lab can't show when a sample arrived, a transport problem looks like a clean result.&lt;/p&gt;

&lt;p&gt;In FactoraERP today, the laboratory module stores samples with a type (including &lt;code&gt;environmental&lt;/code&gt; and &lt;code&gt;water&lt;/code&gt;), a collection point, date, time and collector, and test results with spec min/max and a pass/fail status. Non-conformances with root cause and CAPA live in the same quality module.&lt;/p&gt;

&lt;h2&gt;
  
  
  A positive opens a workflow
&lt;/h2&gt;

&lt;p&gt;The response is where most systems stop. Codex describes a range: "no action (no risk of recontamination), to intensified cleaning, to source tracing (increased environmental testing), to review of hygienic practices up to holding and testing of product". That's a rule table keyed on zone and target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EmResponseRule (zone, target, actions[])
  (1, 'Listeria spp.') -&amp;gt; [hold_product_since_last_negative, intensified_clean, vector_sampling, ncr]
  (3, 'Listeria spp.') -&amp;gt; [intensified_clean, vector_sampling, increase_frequency]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Hold product since last negative" is the expensive one, and it is only possible if the system can answer a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- batches produced on lines served by a site since its last clear result&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;produced_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;em_site&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;site_code&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;produced_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collected_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;em_sample&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;
    &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;em_result&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pass'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Vector sampling" means automatically creating follow-up samples at the positive site and its neighbours. Neighbours can be modelled as an adjacency list on the site map, so the plan grows around a problem and shrinks back when it's fixed, which is what EAS 39 4.6.2 asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trending is the point
&lt;/h2&gt;

&lt;p&gt;CXG 61 says long-term review "can also reveal low level, intermittent contamination that may otherwise go unnoticed". The useful view isn't a pass rate; it is positives per site over time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collected_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'fail'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;positives&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;em_sample&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;em_site&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;site_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;em_result&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;er&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'in_production'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;positives&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One positive is an event. The same drain showing up three months in a row is a harbourage site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Make sampling sites first-class entities with stable codes and zones.&lt;/li&gt;
&lt;li&gt;Record timing relative to cleaning, and don't trend post-clean and mid-shift swabs together.&lt;/li&gt;
&lt;li&gt;Support pooled samples with fan-out on positives.&lt;/li&gt;
&lt;li&gt;Treat a positive as the start of a workflow keyed on zone and target.&lt;/li&gt;
&lt;li&gt;Be able to query the product made since a site's last clear result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full 10-step operational checklist, with references to Codex CXC 1 and CXG 61, EAS 39 and Kenya's food hygiene regulations, is on the &lt;a href="https://factoraerp.com/en/blog/environmental-monitoring-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;FactoraERP blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>foodsafety</category>
    </item>
    <item>
      <title>Waste Is an Output, Not a Subtraction: Modelling By-Products, Waste Streams and Licensed Collections in a Food ERP</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Fri, 30 Oct 2026 04:32:32 +0000</pubDate>
      <link>https://dev.to/yit2022/waste-is-an-output-not-a-subtraction-modelling-by-products-waste-streams-and-licensed-44b</link>
      <guid>https://dev.to/yit2022/waste-is-an-output-not-a-subtraction-modelling-by-products-waste-streams-and-licensed-44b</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/waste-management-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most production software models a batch as inputs in, one product out. Whatever doesn't make it into the finished-goods line is "loss", a single number that nobody can explain. In a food factory that number is made of very different things: rejects, line purges, bran, whey, pomace, film offcuts, chemical containers and effluent. Some of it is sold as feed, some goes to a licensed collector, some is hazardous. If the system can't tell them apart, nobody can manage them.&lt;/p&gt;

&lt;p&gt;This post walks through how we think about modelling waste and by-product streams in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/waste-management-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXC 1-1969&lt;/strong&gt; (9.2.1): waste "should be collected, disposed of by trained personnel and, where appropriate, disposal records maintained"; containers "for waste, by-products and inedible or hazardous substances should be specifically identifiable". &lt;a href="https://doi.org/10.4060/cc6125en" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001&lt;/strong&gt; (10.8): skips emptied "at least once per week", factory waste containers "emptied daily" and not mistakable for food containers. &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kenya's Sustainable Waste Management Act, 2022&lt;/strong&gt;: segregate "non-hazardous waste into organic and non-organic fractions" (s.12); a private sector entity's annual monitoring report specifies "the actual quantities of waste generated by the entity" (s.19); waste goes to "a person who is licensed to transport and dispose of the waste". &lt;a href="https://new.kenyalaw.org/akn/ke/act/2022/31/eng@2022-12-31" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translated into engineering terms: every production output has a type, every waste movement has a stream, a quantity and a destination, and every collection points at a counterparty whose licence has an expiry date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outputs are typed, not leftover
&lt;/h2&gt;

&lt;p&gt;The first design choice is to stop calculating waste as &lt;code&gt;input - output&lt;/code&gt;. Declare it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RecipeOutput     (id, recipe_id, item_id, output_type, expected_pct)
                 output_type IN ('primary','co_product','byproduct','waste')
ProductionOutput (id, run_id, recipe_output_id, qty_kg, recorded_at, recorded_by)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A recipe that turns 1,000 kg of maize into flour, bran and screenings says so up front. When the run closes, each output is weighed and recorded against the run. "Unexplained loss" becomes the residual after declared outputs, which is the number you actually want to drive towards zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By-products are items.&lt;/strong&gt; Bran or whey that leaves the gate as feed gets an item code, a lot, a stock location and a dispatch record, exactly like finished goods. If a raw-material lot is later found to have a problem, the by-product lot is in the same trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Waste never enters saleable stock.&lt;/strong&gt; A waste output posts to a waste location (or no stock at all), so it can't be picked, sold or counted as inventory value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streams, movements and collections
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WasteStream      (id, name, category, hazardous, container_spec, removal_frequency, owner_id)
                 category IN ('organic','plastic','general_dry','hazardous','effluent')
WasteMovement    (id, stream_id, source_run_id NULL, source_area, qty_kg, moved_at, moved_by)
WasteCollection  (id, stream_id, collector_id, qty_kg, collected_at, destination, document_ref)
Collector        (id, name, licence_no, licence_expires_on)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few rules make this useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category is fixed on the stream.&lt;/strong&gt; Segregation at source maps one-to-one to streams. If organic and plastic are mixed in one stream, the report can't separate them later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Block expired collectors.&lt;/strong&gt; A collection against a collector whose &lt;code&gt;licence_expires_on&lt;/code&gt; is in the past should fail validation, or at least raise an exception for the EHS lead. That turns a filing-cabinet check into a system rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schedules generate tasks.&lt;/strong&gt; &lt;code&gt;removal_frequency&lt;/code&gt; on the stream creates cleaning-schedule tasks (empty line bins every shift, skip weekly). A missed task is a hygiene finding, not a surprise at audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queries that matter
&lt;/h2&gt;

&lt;p&gt;Monthly totals by category, which is close to what an annual monitoring report asks for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'month'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collected_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty_kg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kg&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;waste_collection&lt;/span&gt; &lt;span class="n"&gt;wc&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;waste_stream&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stream_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the one production managers care about, waste as a share of output per product and shift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty_kg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'waste'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
       &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty_kg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;waste_share&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;production_output&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;recipe_output&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recipe_output_id&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;production_run&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;po&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shift&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;waste_share&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When waste share on one shift starts climbing, it's usually a yield, equipment or training problem showing up early, long before it shows up in the month-end cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Declare outputs on the recipe: primary, co-product, by-product, waste.&lt;/li&gt;
&lt;li&gt;Treat by-products as stock items with lots, not as negative waste.&lt;/li&gt;
&lt;li&gt;Model waste streams with a fixed category so segregation is reportable.&lt;/li&gt;
&lt;li&gt;Put licence expiry on collectors and validate against it.&lt;/li&gt;
&lt;li&gt;Measure waste where it is made, per run, not by subtraction at month end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full 10-step operational checklist, with references to Codex, EAS 39, Kenya's food hygiene regulations and the Sustainable Waste Management Act, is on the &lt;a href="https://factoraerp.com/en/blog/waste-management-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;FactoraERP blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>sustainability</category>
    </item>
    <item>
      <title>Packaging Is an Ingredient: Modelling Food-Contact Specs, Supplier Evidence and Packaging Lots in a Food ERP</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:27:15 +0000</pubDate>
      <link>https://dev.to/yit2022/packaging-is-an-ingredient-modelling-food-contact-specs-supplier-evidence-and-packaging-lots-in-a-19ai</link>
      <guid>https://dev.to/yit2022/packaging-is-an-ingredient-modelling-food-contact-specs-supplier-evidence-and-packaging-lots-in-a-19ai</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/packaging-material-control-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In most inventory systems, packaging is a boring item: a SKU, a unit of measure, a reorder level. In a food factory that is a modelling mistake. The film, bottle or tub that touches the food is closer to an ingredient than to stationery. It needs a specification, evidence that it is safe for contact with this food, an intake check, a lot number, and a link to every batch it was used on.&lt;/p&gt;

&lt;p&gt;This post walks through how we think about modelling food-contact packaging in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/packaging-material-control-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXC 1-1969&lt;/strong&gt;: packaging "should be safe and suitable for food use" and should not pose a threat to the safety and suitability of food "under the specified conditions of storage and use"; reusable packaging should be "suitably durable, easy to clean and, where necessary, to disinfect" (13.2.9); packaging materials need separate storage from non-food chemicals (9.2.7). &lt;a href="https://doi.org/10.4060/cc6125en" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001&lt;/strong&gt;: food-contact packaging "shall be free of contamination, shall not taint the food" (4.3.10), and packaging "shall not be used as a waste receptacle" (10.8.4). &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kenya Food Hygiene Regulations (Cap. 254)&lt;/strong&gt;: packaging processes and materials must not transmit contaminants to the products (reg. 14(f)), and each container must be marked to identify the producing factory and the lot (reg. 14(d)). &lt;a href="https://faolex.fao.org/docs/pdf/ken106527.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EU Regulation 1935/2004, Article 17&lt;/strong&gt; (a benchmark, not Kenyan law): businesses must be able to identify the businesses from which and to which food contact materials are supplied. &lt;a href="https://www.legislation.gov.uk/eur/2004/1935/article/17/adopted" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read as a developer, that is four requirements: conditions of use are part of the item's definition, suitability evidence is data with a scope, packaging is lot-tracked, and packaging lots belong in batch genealogy.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Conditions of use" belong on a versioned specification
&lt;/h2&gt;

&lt;p&gt;A packaging item is not suitable in general. It is suitable for a food, at a fill temperature, for a shelf-life. So the specification is a first-class, versioned record, and the item points at the approved version.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;packaging_spec&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item_id&lt;/span&gt;       &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;version&lt;/span&gt;       &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;material&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- e.g. 'PET', 'LDPE film 60 micron'&lt;/span&gt;
  &lt;span class="n"&gt;food_contact&lt;/span&gt;  &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- primary packaging?&lt;/span&gt;
  &lt;span class="n"&gt;intended_use&lt;/span&gt;  &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;-- {"foods":["juice"],"max_fill_c":85,"shelf_life_days":180}&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'approved'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'superseded'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="n"&gt;approved_by&lt;/span&gt;   &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;app_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;approved_at&lt;/span&gt;   &lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;unique&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new supplier or material is a new version, not an edit. The old version stays, because batches made last month were packed against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Suitability evidence is data with a scope and a date
&lt;/h2&gt;

&lt;p&gt;The supplier's food-contact statement is not a PDF in someone's inbox. It is a record linked to the supplier, the item and the spec version it covers, with the conditions it covers and an expiry or review date.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;packaging_evidence&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;              &lt;span class="n"&gt;bigserial&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;supplier_id&lt;/span&gt;     &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;supplier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;spec_id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;packaging_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt;            &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- 'food_contact_statement','test_report'&lt;/span&gt;
  &lt;span class="n"&gt;covers&lt;/span&gt;          &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;-- foods / temperatures the document actually covers&lt;/span&gt;
  &lt;span class="n"&gt;document_url&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;valid_until&lt;/span&gt;     &lt;span class="nb"&gt;date&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful check is a comparison, not a checkbox: does &lt;code&gt;evidence.covers&lt;/code&gt; include everything in &lt;code&gt;spec.intended_use&lt;/code&gt;? If the spec says hot fill at 85 °C and the statement only covers ambient use, that is a gap the system can show before the first delivery, not after the first complaint.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Receiving creates a lot, and the lot starts on hold
&lt;/h2&gt;

&lt;p&gt;Packaging is received exactly like an ingredient: the supplier's lot number is mandatory, the intake inspection is a record, and the lot cannot be issued until it passes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PackagingLot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;specId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// the spec version in force at receipt&lt;/span&gt;
  &lt;span class="nl"&gt;supplierId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;supplierLot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// required: no supplier lot, no receipt&lt;/span&gt;
  &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;on_hold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;released&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canIssue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PackagingLot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;evidenceOk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;released&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;evidenceOk&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Making &lt;code&gt;supplierLot&lt;/code&gt; required is the cheapest traceability control you will ever ship. Making &lt;code&gt;on_hold&lt;/code&gt; the default status means a forgotten inspection fails safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Packaging lots are inputs in batch genealogy
&lt;/h2&gt;

&lt;p&gt;When a roll of film or a pallet of bottles is issued to the line, the issue is a consumption against the production batch, the same table you use for ingredient lots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Which batches did a faulty film lot go on, and where were they shipped?&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipped_at&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;batch_input&lt;/span&gt; &lt;span class="n"&gt;bi&lt;/span&gt;
&lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;        &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;
&lt;span class="k"&gt;left&lt;/span&gt; &lt;span class="k"&gt;join&lt;/span&gt; &lt;span class="k"&gt;dispatch&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;bi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_lot_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;packaging_lot_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If packaging is not in &lt;code&gt;batch_input&lt;/code&gt;, this query has no answer and the factory holds every batch from the period instead of the affected ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Reconciliation is a cheap anomaly detector
&lt;/h2&gt;

&lt;p&gt;Issued minus used minus wasted minus returned should be close to zero for each run. Store the four numbers per batch and flag the gap. A large positive gap means packaging went somewhere it should not: into another product, into the bin, or into someone's car. A negative gap means a count or an issue was missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not let packaging be a "consumable" type.&lt;/strong&gt; Consumables are often excluded from lot tracking by default. Food-contact packaging must not be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model reusable containers separately.&lt;/strong&gt; Crates and returnable bottles are assets that cycle: they need a cleaning event and an inspection before each reuse, not a receipt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the spec version on the lot.&lt;/strong&gt; When the spec changes, lots received under the old version still need to be judged against what was approved when they arrived.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build software for factories, how do you treat packaging in your data model: as stock, as an ingredient, or as something else?&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>Records Are Append-Only: Modelling Signed, Contemporaneous Food Safety Records and Corrections in a Food ERP</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:33:29 +0000</pubDate>
      <link>https://dev.to/yit2022/records-are-append-only-modelling-signed-contemporaneous-food-safety-records-and-corrections-in-a-3h5f</link>
      <guid>https://dev.to/yit2022/records-are-append-only-modelling-signed-contemporaneous-food-safety-records-and-corrections-in-a-3h5f</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/food-safety-records-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most food factories still run on paper records, and most of the software that replaces them makes one mistake: it treats a record as a row you can edit. In a food factory a record is evidence. The pasteuriser reading at 14:10 on Tuesday, who took it, what the limit was and what happened when it was out. If someone can quietly change that row, it is no longer evidence.&lt;/p&gt;

&lt;p&gt;This post walks through how we think about modelling food safety records in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/food-safety-records-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXC 1-1969&lt;/strong&gt;: CCP records "should be signed or initialled by the person performing the monitoring and should also report the results and timing of the performed activity"; records should be kept "for a period that exceeds the shelf-life of the product or as determined by the competent authority" (13.4); "records can also be maintained electronically" (19.12). &lt;a href="https://doi.org/10.4060/cc6125en" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001&lt;/strong&gt;: procedures for "identification, collection, indexing, filing, storage, maintenance and disposition of hygiene records" (14.2); records of food safety processes inspected at least daily by a suitably qualified person and signed (4.3.6). &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 CFR 117.305&lt;/strong&gt; (US benchmark): "actual values and observations", "accurate, indelible, and legible", "created concurrently with performance of the activity", with date, time where appropriate, signature or initials, and lot code where appropriate. &lt;a href="https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-117/subpart-F/section-117.305" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translated into engineering terms: entries are append-only, carry an actor and a timestamp, store a typed value next to the limit that applied at the time, link to a lot, and get a separate review event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core entities
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FormTemplate     (id, code, version, effective_from, superseded_at)
CheckDefinition  (id, form_template_id, name, value_type, unit,
                  limit_min, limit_max, is_ccp)
RecordEntry      (id, check_definition_id, lot_id, value, observed_at,
                  recorded_at, recorded_by, supersedes_entry_id NULL,
                  correction_reason NULL)
Review           (id, form_instance_id, reviewed_by, reviewed_at, outcome)
Deviation        (id, record_entry_id, product_action, corrective_action,
                  decided_by, closed_at NULL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices matter more than the table names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot the limit.&lt;/strong&gt; Copy &lt;code&gt;limit_min&lt;/code&gt; and &lt;code&gt;limit_max&lt;/code&gt; onto the entry (or reference an immutable &lt;code&gt;CheckDefinition&lt;/code&gt; version). When the HACCP team changes a critical limit next year, last year's records must still show the limit that applied when the reading was taken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two timestamps.&lt;/strong&gt; &lt;code&gt;observed_at&lt;/code&gt; is when the check happened; &lt;code&gt;recorded_at&lt;/code&gt; is when it was typed in, set by the server. The gap between them is the most honest data-quality metric you will ever have. A shift where every reading was recorded at 17:58 tells you something no audit will.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corrections are new rows.&lt;/strong&gt; Never &lt;code&gt;UPDATE&lt;/code&gt; a value. A correction inserts a new entry with &lt;code&gt;supersedes_entry_id&lt;/code&gt; and a reason. The "current" value is the latest entry in the chain; the history is the chain. This is the database version of crossing out with a single line and initialling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out-of-limit creates work.&lt;/strong&gt; If a value falls outside its snapshotted limits, create a &lt;code&gt;Deviation&lt;/code&gt; in the same transaction. Product on that lot should not be releasable while a deviation is open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review is an event, not a flag.&lt;/strong&gt; A boolean &lt;code&gt;reviewed&lt;/code&gt; loses who reviewed and when. A &lt;code&gt;Review&lt;/code&gt; row keeps both, and lets you report on records not reviewed within a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The query that matters
&lt;/h2&gt;

&lt;p&gt;The test of the model is a recall question: give me everything about lot 2417.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit_min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit_max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observed_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recorded_by&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;corrective_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reviewed_by&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;record_entry&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;check_definition&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;check_definition_id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;deviation&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_entry_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;review&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;form_instance_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lot_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;lot&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;record_entry&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supersedes_entry_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;observed_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that takes minutes, the system works. If it takes two days of pulling files, it does not, however many records you keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things we learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Factories still need paper fallback. Design for a later transcription with &lt;code&gt;observed_at&lt;/code&gt; entered by hand and &lt;code&gt;recorded_at&lt;/code&gt; set by the server, so the gap stays visible.&lt;/li&gt;
&lt;li&gt;Ticks are a smell. A &lt;code&gt;value_type&lt;/code&gt; of boolean should be rare for anything with a measurable result.&lt;/li&gt;
&lt;li&gt;Retention belongs to the record type, not the database. Store a retention rule per form and make disposal a deliberate, logged action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full operational checklist, with references, is in the &lt;a href="https://factoraerp.com/en/blog/food-safety-records-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>Building a manufacturing operating system: five lessons as founder</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Wed, 28 Oct 2026 05:14:46 +0000</pubDate>
      <link>https://dev.to/yit2022/building-a-manufacturing-operating-system-five-lessons-as-founder-2h6o</link>
      <guid>https://dev.to/yit2022/building-a-manufacturing-operating-system-five-lessons-as-founder-2h6o</guid>
      <description>&lt;p&gt;When we started building FactoraERP, I thought we were building software for factories. What we were really building was a way for a plant to remember what it did, accurately, without anyone having to rebuild the story at the end of the week.&lt;/p&gt;

&lt;p&gt;Here are five lessons that shaped the product, and that I think apply to anyone running or building for a production floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start from the lot, not the ledger.&lt;/strong&gt;&lt;br&gt;
Most business software starts from accounts. A factory starts from material. A delivery arrives, it's checked, it's stored, it's issued, it's transformed, it's tested, it's shipped. If the lot number isn't the spine of the system, everything else is reconstruction. So in FactoraERP, batch-level traceability from intake to dispatch came first, and finance, costing and sales hang off it, not the other way round.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The record has to be written where the work happens.&lt;/strong&gt;&lt;br&gt;
A record that gets typed in later is a memory, not a record. People on the floor won't walk to an office to log a reading, and the signal isn't always there when they need it. That's why mobile capture works offline and syncs when the connection returns. If capture is hard, the data will be late, and late data is guesswork with a timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A named person beats an anonymous alert.&lt;/strong&gt;&lt;br&gt;
Alerts that go to "everyone" go to no one. We made acknowledgements named: when a QC checkpoint fails or a condition goes out of range, a specific person acknowledges it, and that acknowledgement stays in the audit trail. Accountability isn't about blame. It's about knowing who saw it, and when.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Costing is only as true as the stock movements underneath it.&lt;/strong&gt;&lt;br&gt;
Every factory wants to know its real cost per batch. But you can't cost a batch from an estimate. If materials, production runs and stock movements are recorded in the same system as they happen, the cost of a batch comes from what actually happened. If they live in three spreadsheets, the cost is an opinion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. One system beats many integrations.&lt;/strong&gt;&lt;br&gt;
It's tempting to stitch together a production tool, an inventory tool, an accounting package and a payroll app. Every seam between them is a place where the truth can drift. FactoraERP puts production, inventory and costing, finance, procurement, sales, payroll and multi-site operations in one system, so a question like "show me this lot" has one answer, with a full audit trail behind it. And when sensors join the picture through FactoraIoT, the conditions a batch went through stay with that batch's record instead of sitting in a separate dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell another founder building for industry&lt;/strong&gt;&lt;br&gt;
Don't build features. Build the record. The features are just the different ways people need to write into it and read from it. If the record is honest, complete and available at the moment someone needs it, whether a production lead, a finance team, a buyer or an auditor, the software is doing its job.&lt;/p&gt;

&lt;p&gt;That's the idea behind FactoraERP: the manufacturing operating system. If your plant still keeps part of its story outside the system, I'd like to hear which part, and why.&lt;/p&gt;

&lt;p&gt;Learn more at factoraerp.com (pricing on request).&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders
&lt;/h2&gt;

&lt;p&gt;The same ideas show up outside factories. A system whose "source of truth" is reconstructed every Friday is a ledger without a lot number. An alert that pages a shared inbox is an anonymous alert. A cost model fed from three spreadsheets is an opinion. Prefer one record written where the work happens, with a named person on every exception.&lt;/p&gt;

&lt;p&gt;Learn more at &lt;a href="https://factoraerp.com" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt; (pricing on request).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://isaacmuthui.com/insights/manufacturing-os-five-lessons" rel="noopener noreferrer"&gt;isaacmuthui.com/insights&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>manufacturing</category>
      <category>startup</category>
      <category>erp</category>
      <category>leadership</category>
    </item>
    <item>
      <title>Mansa Musa had more gold than anyone, and still ran out of cash. Is your system rich but illiquid?</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:13:35 +0000</pubDate>
      <link>https://dev.to/yit2022/mansa-musa-had-more-gold-than-anyone-and-still-ran-out-of-cash-is-your-system-rich-but-illiquid-b87</link>
      <guid>https://dev.to/yit2022/mansa-musa-had-more-gold-than-anyone-and-still-ran-out-of-cash-is-your-system-rich-but-illiquid-b87</guid>
      <description>&lt;p&gt;In July 1324, a caravan from West Africa camped for three days beside the Pyramids of Giza. Then, on 19 July, it crossed the Nile into Cairo. At its head was Musa, the mansa, or king, of Mali, on his way to Mecca for the hajj.&lt;/p&gt;

&lt;p&gt;Cairo had seen pilgrims before. It had not seen anything like this. The Arabic writers who described the visit afterwards struggled for numbers big enough: thousands of attendants dressed in silk and brocade, camels loaded with gold dust, a king who gave gold away to officials, scholars and the poor. Modern historians treat those numbers as a way of saying "more than we could count", not as an audit. But nobody doubts the main point. Mansa Musa arrived with a great deal of gold, and he spent it as if it would never run out.&lt;/p&gt;

&lt;p&gt;Before he was home, he was borrowing money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The richest king in the world
&lt;/h2&gt;

&lt;p&gt;Mali, at its height under Musa, reached from the Atlantic coast deep into the Sahel, across parts of what are now Senegal, the Gambia, Guinea, Mauritania and Mali. Its wealth came from trade and tribute: salt from the north, and gold from the fields of Bambuk and Bure to the south. West African gold was a large share of the gold that circulated in the medieval Mediterranean world.&lt;/p&gt;

&lt;p&gt;The hajj was planned for years. Historians think the gold Musa carried was probably tribute gathered over much of his early reign. And in Cairo he did what a great king was expected to do. He met the Mamluk sultan, al-Nasir Muhammad, and the two exchanged gifts. He stayed about three months. He and his people bought freely in the markets.&lt;/p&gt;

&lt;p&gt;Shihab al-Din al-Umari, who wrote about the visit about twelve years later from people in Cairo who had met the king, recorded a striking detail. Before Musa arrived, he said, a mithqal of gold was worth about 25 silver dirhams. After the visit it fell below 22, and it had not recovered in the years since.&lt;/p&gt;

&lt;p&gt;That detail became famous. You will often read that Musa "wrecked" Egypt's economy. Historians are more careful. Warren Schultz, who studied Mamluk gold prices, has argued that a fall of that size sat within the normal ups and downs of gold in Cairo. So the honest version is smaller: Musa released so much gold, so fast, that people in Cairo remembered it as having made gold cheaper. Even his admirers saw that his abundance had lowered the price of the very thing he was rich in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The way home
&lt;/h2&gt;

&lt;p&gt;The return journey went badly. Travelling apart from the main pilgrim caravan, the Malians suffered cold, hunger and raids. By the time they reached Suez many had died and much of their supply was gone.&lt;/p&gt;

&lt;p&gt;Back in Cairo, the king who had handed out gold now needed money. Several Arabic writers, among them al-Umari, Ibn al-Dawadari and Ibn Battuta, say Musa and his people had to sell goods and borrow from Cairo's merchants, at high rates of interest. Ibn al-Dawadari, as translated by the historian Michael Gomez, says the visitors had at first behaved as if "their money was inexhaustible", and later had to resell what they had bought at half its value.&lt;/p&gt;

&lt;p&gt;One merchant family, the Banu al-Kuwayk, is recorded as lending him 50,000 dinars. Musa sold them a palace the sultan had given him as part payment. The lender sent agents all the way to Mali to collect the rest. According to al-Umari and Ibn Khaldun, many lenders were repaid only in part, or not at all. Other sources disagree about how it ended.&lt;/p&gt;

&lt;p&gt;It is worth stating the paradox plainly. Mansa Musa was among the richest rulers of his age. And for a few months, far from home, he was short of cash.&lt;/p&gt;

&lt;h2&gt;
  
  
  What he did get right
&lt;/h2&gt;

&lt;p&gt;It would be unfair to stop there. The journey made Mali famous. In 1375, about half a century later, the Catalan Atlas showed an African king on a throne holding a gold nugget, and that image of Mali shaped how the rest of the world saw West Africa for centuries. Musa came home with scholars, including the Andalusian poet Abu Ishaq al-Sahili, and his reign is linked with Timbuktu's rise as a centre of learning.&lt;/p&gt;

&lt;p&gt;But in Mali itself, memory is mixed. Lucy Duran of SOAS in London told the BBC that some griots, the hereditary historians of the Mande world, are reluctant to praise him: "they think he wasted local resources outside the empire."&lt;/p&gt;

&lt;p&gt;Both things are true. The spending bought reputation. It also left a powerful king borrowing from strangers at high interest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson for enterprise today
&lt;/h2&gt;

&lt;p&gt;I see the Mansa Musa pattern in companies all the time, and not only in big ones. It has three parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wealth is not cash.&lt;/strong&gt; Musa's gold was real, but it was back in Mali, or already given away. What mattered in Cairo was what he could spend that week. A business can have a full warehouse, a strong order book and valuable equipment, and still not be able to pay its suppliers on Friday. Stock, receivables and assets are wealth. Cash is what lets you act. Watch them separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flooding the market cheapens what you bring.&lt;/strong&gt; When you release too much of your best thing at once, you lower its price. That is true of gold in Cairo, and it is true of heavy discounting, of dumping a season's production into one market, or of giving your expertise away so freely that customers stop seeing its value. Abundance needs a release plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trip abroad is where the plan breaks.&lt;/strong&gt; Musa's trouble came far from home, on a journey that went worse than planned. Expansion works the same way: a new market, a new plant, a big contract in another country. Costs land before income does, and surprises cost more when you are away from your base. Budget the way back, not just the way out, and keep a reserve you do not touch for display.&lt;/p&gt;

&lt;p&gt;Notice, too, what happened to the lenders. Merchants lent to a famous king on the strength of his reputation and many of them never got all their money back. Reputation is not a payment plan. Whoever you extend credit to, know how and when you will be paid.&lt;/p&gt;

&lt;p&gt;Mansa Musa is remembered as the richest man who ever lived. He is also remembered, by some of his own people, as the king who spent Mali's gold abroad. The difference between those two memories is not how much he had. It is how he managed what he carried.&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders: wealth versus what you can spend now
&lt;/h2&gt;

&lt;p&gt;Systems can be rich and illiquid too. A cluster with plenty of total capacity can still fall over because one pool, one queue or one region is empty. A team can own a huge backlog of "assets" (libraries, services, data) and still have no engineer free this sprint. A startup can show a large annual contract on paper and still miss payroll because the invoice is paid quarterly in arrears.&lt;/p&gt;

&lt;p&gt;A few habits help. Measure available headroom where the load actually lands, not the total. Rate-limit your own generosity: free tiers, generous quotas and unlimited plans are your gold in Cairo, so release them with a plan. Budget the return trip on every migration or launch: the rollback, the dual-running period, the clean-up that always costs more than expected. And when you extend credit (a partner's SLA, a customer's payment terms), know how you will collect before you ship.&lt;/p&gt;

&lt;p&gt;So here is my question for you this week: if your business had to pay all its bills from what it can actually spend this month, not what it owns, how long could it travel?&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Wikipedia, "Musa I of Mali" (summary of al-Umari, Ibn Khaldun, Ibn al-Dawadari, Ibn Battuta; Levtzion &amp;amp; Hopkins, Corpus of Early Arabic Sources for West Africa, 2000, p. 271 for the 25 → under 22 dirham figure; Gomez, African Dominion, 2018, pp. 104–121; Schultz 2006 on normal fluctuations; Collet 2019 on rhetorical caravan figures): &lt;a href="https://en.wikipedia.org/wiki/Musa_I_of_Mali" rel="noopener noreferrer"&gt;https://en.wikipedia.org/wiki/Musa_I_of_Mali&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;BBC News, Naima Mohamud, "Is Mansa Musa the richest man who ever lived?" (10 March 2019); Lucy Duran quote is verbatim from this article: &lt;a href="https://www.bbc.com/news/world-africa-47379458" rel="noopener noreferrer"&gt;https://www.bbc.com/news/world-africa-47379458&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Warren Schultz, "Mansa Mūsā's gold in Mamluk Cairo: a reappraisal of a world civilizations anecdote" (2006), in Pfeiffer &amp;amp; Quinn (eds.), Harrassowitz.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Michael A. Gomez, African Dominion (Princeton University Press, 2018), pp. 118–120 (borrowing, resale at half value, Ibn al-Dawadari translation).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Catalan Atlas (1375), BnF Espagnol 30, sheet 6.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Originally published at isaacmuthui.com/stories.&lt;/p&gt;

</description>
      <category>history</category>
      <category>architecture</category>
      <category>startup</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Competence Is a Join, Not a Spreadsheet: Modelling Training Matrices, Certificate Expiry and Task Authorisation in a Food ERP</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:32:36 +0000</pubDate>
      <link>https://dev.to/yit2022/competence-is-a-join-not-a-spreadsheet-modelling-training-matrices-certificate-expiry-and-task-5nd</link>
      <guid>https://dev.to/yit2022/competence-is-a-join-not-a-spreadsheet-modelling-training-matrices-certificate-expiry-and-task-5nd</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/food-safety-training-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most HR systems store training as a list of events: a course name, a date and a list of attendees. That is fine for payroll and leave. It breaks the moment a production supervisor asks the question that actually matters in a food factory at 6 am: is the person I have just put on the pasteuriser authorised to run that CCP, and is their medical certificate still valid?&lt;/p&gt;

&lt;p&gt;Answering that is not a lookup. It is a join across roles, tasks, required training, assessments, certificates with expiry dates and the shift roster. This post walks through how we think about modelling food safety training and task authorisation in an ERP for food and agro manufacturers in East Africa. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/food-safety-training-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXC 1-1969&lt;/strong&gt; (section 10): everyone in contact with food needs "competence appropriate to the operations they are to perform"; "periodic assessments of the effectiveness of training and instruction programmes should be made"; "records should be kept of training activities". For HACCP (19.13), working instructions should define "the tasks of the operating personnel in charge of each CCP", and "re-training may be needed as part of corrective actions for some deviations". &lt;a href="https://doi.org/10.4060/cc6125en" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001&lt;/strong&gt; (5.3): all staff, including management, get "induction and on going training in hygiene", delivered by competent personnel, and "training records shall be kept". &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kenya Food Hygiene Regulations&lt;/strong&gt; (reg 15): staff who identify sanitation failures must be "properly trained"; medical examination before employment and at intervals of not more than twelve months, with certificates kept at the plant. &lt;a href="https://faolex.fao.org/docs/pdf/ken106527.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kenya OSHA 2007&lt;/strong&gt; (s.99): training on recruitment, transfer or change of job, new equipment or materials and new technology, "repeated periodically". &lt;a href="https://www.labour.go.ke/sites/default/files/2022-10/Occupational-Safety-and-Health-Act-2007.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 CFR 117.4&lt;/strong&gt; (US benchmark): training applies to everyone "including temporary and seasonal personnel", and training records "must be established and maintained". &lt;a href="https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-117/subpart-A/section-117.4" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translated: training requirements hang off roles and tasks, not people; completion is not the same as competence; some certificates expire; certain events (job change, new equipment, a deviation) create new training obligations; and casual workers are in scope from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core entities
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role              (id, name)
Task              (id, name, is_ccp, ccp_id, required_certificate)
RoleTask          (role_id, task_id)
TrainingItem      (id, name, procedure_id, procedure_version,
                   refresh_interval_days, required_before_unsupervised)
TaskTraining      (task_id, training_item_id)
Person            (id, kind, role_id)          -- kind: employee | casual | contractor
TrainingSession   (id, training_item_id, trainer_id, held_on, procedure_version)
Assessment        (id, person_id, training_item_id, session_id,
                   method, assessor_id, assessed_on, result)   -- observed | oral | practical
Certificate       (id, person_id, type, issued_on, expires_on, document_ref)
TrainingTrigger   (id, person_id, training_item_id, reason, source_ref, due_on)
RosterSlot        (id, shift_id, task_id, person_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices matter more than the table list.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Requirements attach to tasks, not to people
&lt;/h3&gt;

&lt;p&gt;A person needs training because of what they do. Model &lt;code&gt;Task -&amp;gt; TaskTraining -&amp;gt; TrainingItem&lt;/code&gt; and derive each person's requirements from their role and rostered tasks. When someone moves from packing to the filler, their requirements change automatically, which is exactly the OSHA "change of job" trigger.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Attendance and competence are different rows
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;TrainingSession&lt;/code&gt; records that something was taught. An &lt;code&gt;Assessment&lt;/code&gt; records that a specific person was observed doing the task and passed. Authorisation comes only from a passing assessment, never from attendance alone. This is the whole point of the article in one constraint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pin the procedure version
&lt;/h3&gt;

&lt;p&gt;Store the procedure version on both the session and the assessment. When a CCP work instruction is revised, every assessment against the old version becomes "needs refresh". Without this, a factory can show that everyone was trained, just not on the procedure currently in force.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Authorisation is a derived view
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- People currently authorised for a task (simplified)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;task_training&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;
  &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;training_item&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;training_item_id&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;assessment&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;training_item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pass'&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;procedure_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;procedure_version&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refresh_interval_days&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
             &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assessed_on&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ti&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refresh_interval_days&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_certificate&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
      &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required_certificate&lt;/span&gt;
        &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_on&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;current_date&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never store an "authorised" flag that someone has to remember to untick. Derive it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Warn at rostering time, block where it matters
&lt;/h3&gt;

&lt;p&gt;Check authorisation when a person is assigned to a roster slot or production task. For most tasks a visible warning is enough. For CCP tasks, a plant may choose to block the assignment outright. In FactoraERP, production task types can name a certificate the person should hold, and the system shows a warning when someone without a valid certificate is assigned; employee and casual worker documents (including medical and food handler certificates) carry expiry dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Events create training obligations
&lt;/h3&gt;

&lt;p&gt;New equipment, a revised procedure, a job change or a non-conformance whose root cause was a skill gap should insert a &lt;code&gt;TrainingTrigger&lt;/code&gt; row with a due date. That turns "operator retrained" from a phrase in a corrective action into a tracked obligation that is closed by an assessment.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Casuals are first-class people
&lt;/h3&gt;

&lt;p&gt;Seasonal and casual workers are often kept in a separate spreadsheet, which is exactly where gaps hide. Model them as &lt;code&gt;Person&lt;/code&gt; with &lt;code&gt;kind = casual&lt;/code&gt; so the same authorisation view applies on their first day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Before each shift: rostered people not authorised for their task.&lt;/li&gt;
&lt;li&gt;Certificates expiring in the next 30 days.&lt;/li&gt;
&lt;li&gt;Assessments made against superseded procedure versions.&lt;/li&gt;
&lt;li&gt;Open training triggers past their due date, by source (deviation, change, refresher).&lt;/li&gt;
&lt;li&gt;Deviations by person and task, joined to their training status at the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exotic. It is a dozen tables and one derived view. The hard part is deciding that training is about tasks and competence, not courses and attendance.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FactoraERP is an ERP for food and agro manufacturers in East Africa, built by Tamalaki Business Network Ltd. Full checklist: &lt;a href="https://factoraerp.com/en/blog/food-safety-training-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;Food Safety Training Checklist&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>The Garamantes ran a desert kingdom on water that never refilled. What is your system running down?</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:23:23 +0000</pubDate>
      <link>https://dev.to/yit2022/the-garamantes-ran-a-desert-kingdom-on-water-that-never-refilled-what-is-your-system-running-down-2m6j</link>
      <guid>https://dev.to/yit2022/the-garamantes-ran-a-desert-kingdom-on-water-that-never-refilled-what-is-your-system-running-down-2m6j</guid>
      <description>&lt;p&gt;In the fifth century BC, the Greek historian Herodotus wrote about a people living deep in the Libyan desert, ten days' journey beyond the oasis of Augila. He called them the Garamantes, "a very great nation, who carry earth to lay over the salt and then sow crops."&lt;/p&gt;

&lt;p&gt;Herodotus mixed fact with travellers' tales. In the same passage he describes cattle that had to graze walking backwards because their horns curved into the ground. For a long time, many readers treated the Garamantes as one more desert legend.&lt;/p&gt;

&lt;p&gt;They were not a legend. Archaeologists have since found their capital, Garama, at today's Jarma in the Fazzan region of southern Libya. Researchers now describe them as arguably the earliest state in the central Sahara. And the secret of their power was hidden under the sand.&lt;/p&gt;

&lt;h2&gt;
  
  
  A kingdom with no river
&lt;/h2&gt;

&lt;p&gt;The Sahara was not always a desert. Monsoon rains made it much greener between about 11,000 and 5,000 years ago. Then the rains stopped. By around 3,100 years ago, according to dating of old spring deposits, the springs at the foot of the escarpment near Jarma had dried up.&lt;/p&gt;

&lt;p&gt;The Garamantes built their civilisation after all that, in a climate close to today's. They lived along the Wadi al-Ajal, a long, narrow valley with a sand sea on one side and a barren rock plateau on the other. There was no river. Geologists describe them as the first urban society to take root in a desert with no continuously flowing river.&lt;/p&gt;

&lt;p&gt;What they did have was water left over from the green Sahara, sealed underground in a vast sandstone aquifer. The water-bearing rock sat at the base of the plateau, a little higher than the valley floor. If you could reach it, it would flow downhill on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tunnels
&lt;/h2&gt;

&lt;p&gt;So the Garamantes dug. They built foggaras: gently sloping tunnels that ran from the aquifer out into the valley, dug and maintained through a line of vertical shafts. The method is close to the qanats of Persia, and archaeologists think the idea reached the Fazzan from the east, probably by way of Egypt.&lt;/p&gt;

&lt;p&gt;The scale is hard to picture. Archaeologists have recorded hundreds of foggaras, with an estimated 100,000 shafts, some reaching around 40 metres deep. Estimates of their total length range from hundreds to thousands of kilometres, depending on the study. Much of the digging was probably done by forced labour, in heat above ground and in the dark, narrow passages below.&lt;/p&gt;

&lt;p&gt;The result was an oasis economy in one of the driest places on earth. Plant remains from Garamantian sites show irrigated wheat, barley, grapes and dates. Radiocarbon dating shows the foggaras were in use from about 400 BC to AD 700, with the busiest construction around the turn of the millennium. Garama traded across the desert and dealt with Rome as a neighbour, sometimes as a rival.&lt;/p&gt;

&lt;p&gt;Frank Schwartz, a hydrogeologist at Ohio State University who has studied the system, put the puzzle simply: "Their qanats shouldn't have actually worked, because the ones in Persia have annual water recharge from snowmelt, and there was zero recharge here."&lt;/p&gt;

&lt;p&gt;That is the heart of the story. Persian qanats tap water that refills. The Garamantes were drawing down a store that did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the level fell
&lt;/h2&gt;

&lt;p&gt;Fossil water is a savings account with no income. You can live well on it for a long time, but every year the balance is lower.&lt;/p&gt;

&lt;p&gt;As the water table dropped, the tunnels had to reach further and deeper to keep flowing. Eventually, as the Geological Society of America summarised Schwartz's research, the groundwater fell below the level of the foggaras. A tunnel above the water line carries nothing.&lt;/p&gt;

&lt;p&gt;Researchers are careful not to blame water alone. Changes in trade and politics mattered too. But the pattern is clear in the record. When Arab armies reached the Fazzan in the 660s, there was still a king at Garama, but his power had faded and many foggaras had stopped working. People fell back on wells, which meant lifting water by hand or with animals. That supports far fewer people than water that flows by itself.&lt;/p&gt;

&lt;p&gt;A civilisation that had lasted roughly a thousand years shrank to what its wells could carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson for enterprise and industry
&lt;/h2&gt;

&lt;p&gt;The Garamantes were not careless. Their engineering was brilliant. Their problem was invisible: the thing they depended on most was out of sight, slowly falling, with no gauge on the wall.&lt;/p&gt;

&lt;p&gt;Every business draws on stores like that. Some are physical: groundwater for a factory, the remaining life of a machine, the cold chain's margin before a product spoils. Some are not: the knowledge in one experienced person's head, customer goodwill, cash runway, the patience of a lender. They feel like a flow because they keep delivering. Until they don't.&lt;/p&gt;

&lt;p&gt;The Garamantes' story suggests three questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are you drawing on that is not being refilled?&lt;/strong&gt; List the inputs your operation cannot work without. Then ask honestly which ones are being replenished and which are being drawn down. Training refills knowledge. Maintenance refills machine life. Good service refills goodwill. Anything without a refill plan is fossil water.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you see the level?&lt;/strong&gt; The Garamantes could see the water at the tunnel mouth. They could not easily see the aquifer dropping behind it. Most companies are the same: they watch output, not the stores behind it. Measure the level directly and often: temperatures and run hours on equipment, stock and shelf life, who knows how to do what, days of cash. A number on a screen today beats a dry tunnel next year.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What will you do when the level drops below your tunnel?&lt;/strong&gt; The fall-back to wells was slow, costly and forced. Decide your switch before you need it: the second supplier, the backup process, the cross-trained team, the reserve. Change is cheaper when it is a plan rather than an emergency.&lt;/p&gt;

&lt;p&gt;Herodotus was right about one thing. The Garamantes were a great nation. Their tunnels still cross the Fazzan, dry now, like a line of question marks in the sand.&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders: find your fossil water
&lt;/h2&gt;

&lt;p&gt;Software systems draw on stores that quietly run down too: disk space, connection pools, API quotas, certificate expiry dates, error budgets, a free tier's credits, the one engineer who understands the billing code. They behave like flows right up until the moment they are empty.&lt;/p&gt;

&lt;p&gt;A few habits help. Monitor the level, not just the output: alert on remaining capacity and the rate it is falling, not only on failures. Put an expiry date and an owner on everything that expires. Spread knowledge with docs, pairing and runbooks, so it refills when people move on. And rehearse the fall-back (the restore, the failover, the second provider) before you need it. The Garamantes saw water at the tunnel mouth until the day it stopped. Your dashboards should show the aquifer.&lt;/p&gt;

&lt;p&gt;So here is my question for every founder, manager and plant team:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is your business drawing on that is not being refilled, and who is watching the level?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://isaacmuthui.com/stories/garamantes-hidden-water/" rel="noopener noreferrer"&gt;isaacmuthui.com/stories&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>history</category>
      <category>architecture</category>
      <category>devops</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Kilwa grew rich on gold it never mined. Is your product built on someone else's route?</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:14:51 +0000</pubDate>
      <link>https://dev.to/yit2022/kilwa-grew-rich-on-gold-it-never-mined-is-your-product-built-on-someone-elses-route-11a6</link>
      <guid>https://dev.to/yit2022/kilwa-grew-rich-on-gold-it-never-mined-is-your-product-built-on-someone-elses-route-11a6</guid>
      <description>&lt;p&gt;In 1944, an Australian soldier named Maurie Isenberg was posted to a radar station on Marchinbar Island, in the Wessel Islands off the far north coast of Australia. In his spare time he went fishing. One day, in the sand just below the high-water mark, he noticed a few small, green, round objects. He picked them up, found a few more nearby, and put them in a tin.&lt;/p&gt;

&lt;p&gt;The tin stayed in his kit for 35 years. In 1979 he finally had the coins identified. Four were Dutch East India Company coins. The other five were copper coins from the Sultanate of Kilwa, an island city off the coast of what is now southern Tanzania, on the other side of the Indian Ocean.&lt;/p&gt;

&lt;p&gt;Nobody knows for certain how they got there. The Powerhouse Museum in Sydney, which holds them today, thinks they are most likely linked to later Portuguese voyages, but it is still a mystery.&lt;/p&gt;

&lt;p&gt;What is not a mystery is why Kilwa had coins to lose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The city at the gate
&lt;/h2&gt;

&lt;p&gt;Kilwa Kisiwani was lived in from the ninth to the nineteenth century, and it reached its peak in the thirteenth and fourteenth. UNESCO, which made its ruins a World Heritage Site in 1981, describes it as a Swahili trading city whose prosperity rested on control of the Indian Ocean trade with Arabia, India and China. Gold and ivory from the interior went out. Silver, carnelians, perfumes, Persian faience and Chinese porcelain came in. Kilwa minted its own coins between the eleventh and fourteenth centuries.&lt;/p&gt;

&lt;p&gt;When the Moroccan traveller Ibn Battuta stopped there in 1331, he was impressed. "Kilwa is one of the most beautiful and well-constructed towns in the world," he wrote. "The whole of it is elegantly built."&lt;/p&gt;

&lt;p&gt;The ruins still show why. The Great Mosque, built of coral stone and lime mortar, is the oldest standing mosque on the East African coast. Its great dome, from the thirteenth century, was the largest in East Africa until the nineteenth. On a bluff above the sea stand the remains of Husuni Kubwa, a palace built between about 1310 and 1333, with more than a hundred rooms and a large octagonal bathing pool. Archaeologists believe its great courtyard was a place where merchants rested and goods were stored before being loaded onto dhows.&lt;/p&gt;

&lt;p&gt;This was a confident, cosmopolitan, wealthy city. And most of that wealth rested on one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gold it never mined
&lt;/h2&gt;

&lt;p&gt;The gold did not come from Kilwa. It came from the interior of southern Africa, the world of Great Zimbabwe, and was carried to the port of Sofala, in what is now Mozambique.&lt;/p&gt;

&lt;p&gt;Kilwa's genius was to put itself in the middle of that flow. According to National Geographic's account of the city's history, Kilwa's rulers extended their influence south until Sofala became their outpost. From the late 1200s, under the Mahdali dynasty, they kept consolidating until the East African gold trade came under Kilwa's control.&lt;/p&gt;

&lt;p&gt;Geography helped. As one study of mining and African cities explains, the monsoon winds further south were too weak for the large ocean-going ships. So gold moved north from Sofala in smaller boats to Kilwa, where it was loaded onto bigger vessels bound for the markets of the Middle East and India. Kilwa sat at the gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the route moved
&lt;/h2&gt;

&lt;p&gt;A business built on someone else's flow lasts as long as the flow.&lt;/p&gt;

&lt;p&gt;Historians point to several pressures. The same study links the end of Kilwa's great age, after about 1350, to falling gold production in Zimbabwe. Some scholars also point to falling gold prices. None of these happened on the island. All of them hit it.&lt;/p&gt;

&lt;p&gt;Then came the Portuguese. They reached East Africa in 1498 and wanted the gold trade for themselves. To them, Sofala was the focal point: control it, and you control the gold. In 1505 Francisco de Almeida occupied Kilwa after it refused to pay tribute, and the Portuguese built a fort there. They held it only until 1512. But the damage was done. Kilwa slowly declined and was abandoned by the middle of the nineteenth century.&lt;/p&gt;

&lt;p&gt;There is a twist. The Portuguese plan to take over the gold trade did not work well either. Historians of the Sofala coast note that, even with forts, trading posts and licences, the system proved ineffective, partly because the newcomers did not understand the networks they were trying to control. Owning the gate turned out to be harder than taking it.&lt;/p&gt;

&lt;p&gt;Today Kilwa's ruins are fragile. From 2004 to 2014 UNESCO kept them on its List of World Heritage in Danger, threatened by the sea, vegetation and new building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson for enterprise and industry
&lt;/h2&gt;

&lt;p&gt;Kilwa was not foolish. It was excellent at what it did: the harbour, the ships, the merchants, the coins, the buildings. But its greatest asset was its position, standing where someone else's product passed. Positions can be taken.&lt;/p&gt;

&lt;p&gt;Many companies today are Kilwas without knowing it. A distributor that lives on one brand's contract. A factory that sells most of its output to one buyer. A processor that depends on a single source for its main raw material. A software business that lives or dies on one platform's rules. The middle can be a good place to stand, as long as you know you are standing there.&lt;/p&gt;

&lt;p&gt;Kilwa's story suggests three questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does your value really come from?&lt;/strong&gt; Draw the flow: inputs, suppliers, channels, customers. Mark every point where one name carries most of the weight. That is your Sofala.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do you own that would move with you?&lt;/strong&gt; Kilwa's palace could not follow the gold. Capabilities can: skills, process know-how, good records, relationships, a product customers choose for its own sake. A location, a licence or a single contract cannot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are you watching the source, or only the counter?&lt;/strong&gt; From the island, Kilwa could see the gold arriving. It could not easily see the mines slowing hundreds of kilometres inland. Real visibility means looking earlier in the chain, at supplier performance, intake quality and changing demand, before problems show up in revenue.&lt;/p&gt;

&lt;p&gt;Five copper coins in a Sydney museum show how far Kilwa's reach once went. The ruins on the island show that reach is not the same as roots.&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders: find your Sofala
&lt;/h2&gt;

&lt;p&gt;Every software product sits on someone else's flow: a cloud provider, an app store, a payment processor, an API with generous free limits, a single big customer, one search engine sending most of your traffic. That is normal. The danger is not knowing which of those dependencies is your Sofala, the one that, if it moved, would take most of your value with it.&lt;/p&gt;

&lt;p&gt;A few habits help. Keep a dependency map that names the single points of failure, not just the libraries. Put critical third-party APIs behind an interface you own, so swapping a provider is a project, not a rewrite. Own your data and your relationship with users: exports, email lists, your own domain. And watch the signals upstream, such as pricing changes, rate limits, deprecation notices and policy updates, before they show up as an outage or a revenue dip. Kilwa could see the gold arriving. It could not see the mines slowing.&lt;/p&gt;

&lt;p&gt;So here is my question for every founder, manager and plant team:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the route through your business moved tomorrow, what would still be yours?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UNESCO World Heritage Centre, "Ruins of Kilwa Kisiwani and Ruins of Songo Mnara" (whc.unesco.org/en/list/144/): occupied 9thâ€“19th century, peak 13thâ€“14th; prosperity based on control of Indian Ocean trade with Arabia, India and China; gold and ivory traded for silver, carnelians, perfumes, Persian faience and Chinese porcelain; own currency 11thâ€“14th centuries; Ibn Battuta's 1331â€“32 visit; Great Mosque (oldest standing mosque on the East African coast; dome largest in East Africa until the 19th century); Husuni Kubwa c.1310â€“1333 with octagonal pool; Portuguese fort; inscribed 1981; in Danger 2004â€“2014; threats from sea, vegetation and encroachment.&lt;/li&gt;
&lt;li&gt;Ibn Battuta, &lt;em&gt;Travels in Asia and Africa 1325â€“1354&lt;/em&gt;, trans. H. A. R. Gibb (1929), as reproduced by Teaching Medieval Slavery and Captivity (medievalslavery.org): "Kilwa is one of the most beautiful and well-constructed towns in the world. The whole of it is elegantly built."&lt;/li&gt;
&lt;li&gt;Powerhouse Collection, Sydney, "Collection of coins, photograph and documentation" (collection.powerhouse.com.au/object/305381): 5 Kilwa Sultanate + 4 Dutch coins from Marchinbar Island, found by M. Isenberg during WWII; likely link to Portuguese voyages suggested.&lt;/li&gt;
&lt;li&gt;Indiana University news, "IUPUI anthropologist to lead search for ancient coins" (2013) and Ian S. McIntosh, "Life and Death on the Wessel Islands": 1944, 312 Radar Unit / Jensen Bay radar station, coins found while fishing, kept in a tin until 1979, one Dutch coin dated 1690.&lt;/li&gt;
&lt;li&gt;National Geographic, "This abandoned East African city once controlled the medieval gold trade": Kilwa extended influence to control Sofala; Mahdali dynasty from the late 1200s consolidated the gold trade; Husuni Kubwa &amp;gt;100 rooms, courtyard thought to be a caravanserai; 1505 Almeida occupation after Kilwa refused tribute; Gereza fort; abandoned by the mid-19th century.&lt;/li&gt;
&lt;li&gt;Encyclopaedia Britannica, "Kilwa": held by the Portuguese 1505â€“12, then gradually declined.&lt;/li&gt;
&lt;li&gt;World Monuments Fund, "Historic Sites of Kilwa" (archived): Portuguese arrival in East Africa in 1498.&lt;/li&gt;
&lt;li&gt;"Eureka and beyond: mining's impact on African urbanisation", Journal of Contemporary African Studies (2012, doi 10.1080/02589001.2012.719376): Kilwa peak c.1250â€“1350, ebbing with dwindling gold production in Zimbabwe; gold shipped north from Sofala in small boats and transferred at Kilwa because monsoon winds further south were too weak for large vessels.&lt;/li&gt;
&lt;li&gt;Centro de Estudos Internacionais (CEI-IUL), chapter "The Sofala Coast (Mozambique) in the 16th Century" (books.openedition.org/cei/428): Sofala as the reason for Kilwa's prosperity and the Portuguese focal point for the gold trade; conquest of Kilwa 1505; Portuguese system proved ineffective.&lt;/li&gt;
&lt;li&gt;Nakamura (Minpaku), "Hidden Cultural Heritage... Kilwa Island" (minpaku.repo.nii.ac.jp/record/8856): falling gold prices as a factor in decline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at isaacmuthui.com/stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>history</category>
      <category>architecture</category>
      <category>startup</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Lalibela's builders carved downward. Some of your deploys are the same</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:14:29 +0000</pubDate>
      <link>https://dev.to/yit2022/lalibelas-builders-carved-downward-some-of-your-deploys-are-the-same-4nh5</link>
      <guid>https://dev.to/yit2022/lalibelas-builders-carved-downward-some-of-your-deploys-are-the-same-4nh5</guid>
      <description>&lt;p&gt;High in the Lasta mountains of northern Ethiopia, about 645 kilometres from Addis Ababa, there is a town where the churches are not built. They are carved.&lt;/p&gt;

&lt;p&gt;Lalibela holds eleven medieval churches cut out of solid volcanic rock. UNESCO lists them as a World Heritage Site, and they were among the very first twelve places inscribed on the World Heritage List in 1978. Their creation is attributed to King Lalibela of the Zagwe dynasty, who ruled in the late twelfth and early thirteenth century. According to UNESCO, he set out to build a "New Jerusalem" after Muslim conquests had halted Christian pilgrimages to the Holy Land. Even the landscape carries the idea: a channel cut through the site is named the Yordannos, the Jordan.&lt;/p&gt;

&lt;p&gt;Most visitors come to see one church above all. And what makes it remarkable is not only how beautiful it is. It is how it was made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building from the top down
&lt;/h2&gt;

&lt;p&gt;Almost every building in history goes up. You lay a foundation, raise walls, and put a roof on last.&lt;/p&gt;

&lt;p&gt;Lalibela's builders went the other way. As the Metropolitan Museum of Art describes it, workers first traced the outline of a church on the surface of the rock. Then they cut deep trenches around it, isolating a single giant block. Only then did they begin to shape it, working with chisels, axes and other blades, refining the outside and hollowing out the inside: doors, windows, columns, floors and ceilings, all from the same piece of stone.&lt;/p&gt;

&lt;p&gt;That means the roof was finished first and the floor last. Every window, every arch, every cross was made by taking stone away.&lt;/p&gt;

&lt;p&gt;The most famous of them, Bete Giyorgis, the Church of Saint George, shows how far the method could go. Seen from above it is a perfect cross, with three Greek crosses carved one inside another on its roof. It is about 12 metres wide and roughly 12 metres high, standing on a stepped platform at the bottom of a pit about 25 metres square. It has three doors, twelve windows and, below them, nine false windows that were carved for the look of the facade but never opened. Visitors reach it by walking down a trench and through a tunnel.&lt;/p&gt;

&lt;p&gt;There is a legend about it, too. Local tradition says King Lalibela built it only after Saint George, Ethiopia's patron saint, reproached him for not building him a house.&lt;/p&gt;

&lt;h2&gt;
  
  
  No undo button
&lt;/h2&gt;

&lt;p&gt;Think about what carving downward demands.&lt;/p&gt;

&lt;p&gt;When you build with bricks, a mistake can be fixed. You knock down a wall and lay it again. When you carve a building out of a mountain, there is no putting stone back. A wrong cut on the roof stays wrong forever. A wall carved too thin cannot be thickened. The decision about where the floor would be had to be right long before anyone reached it.&lt;/p&gt;

&lt;p&gt;So the work had to be planned before the first blow. The outline traced on the rock was not a sketch to be improved later. It was a commitment.&lt;/p&gt;

&lt;p&gt;Historians still debate exactly how long the work took and who did it. Some scholars think the churches were carved over a longer period, possibly from the tenth to the thirteenth century or later, with some adapted from earlier structures; Bete Giyorgis itself is usually dated to the late twelfth or early thirteenth century. But whatever the timeline, the method left no room for careless work. Every stage depended on the one before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody photographs
&lt;/h2&gt;

&lt;p&gt;There is another detail that gets less attention than the soaring walls.&lt;/p&gt;

&lt;p&gt;Carving deep into the ground creates an obvious danger: water. Rain and groundwater could flood the pits and eat away at the stone. So, the Met notes, the builders cut drainage canals and trenches to carry water away, and the roofs of the four free-standing churches slope at the same angle as the rock they came from, which helps water run off. Other channels fed cisterns and baptismal pools. UNESCO describes an "extensive system of drainage ditches, trenches and ceremonial passages" linking the site.&lt;/p&gt;

&lt;p&gt;None of that is what tourists come to photograph. All of it is part of why the churches are still standing more than 800 years later.&lt;/p&gt;

&lt;p&gt;And they are not museums. Lalibela is a living place of worship. A 2018 UNESCO advisory mission counted almost 800 priests serving there, with daily services and thousands of pilgrims arriving for Ethiopian Christmas and Easter. The churches have survived because a community has kept using them, caring for them and passing on their meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson for enterprise and industry
&lt;/h2&gt;

&lt;p&gt;A lot of modern work is more like carving than building, even if we pretend otherwise.&lt;/p&gt;

&lt;p&gt;In a factory, a batch that has been mixed cannot be unmixed. Product that has been packed and shipped cannot be quietly corrected. A cut in steel, a contract signed, a production run started on the wrong specification: these are downward cuts. You can only take away from what you have.&lt;/p&gt;

&lt;p&gt;Lalibela's builders teach three things about that kind of work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan before the first cut.&lt;/strong&gt; The outline on the rock was the specification, agreed before the work began. In industry, the equivalent is a clear recipe, drawing or standard that everyone works from, instead of decisions made up on the floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check at every stage.&lt;/strong&gt; You cannot carve the floor and then discover the roof is wrong. Checks have to happen while there is still something to save: at intake, during the run, before packing. Quality found at the end is just a record of what went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the drainage.&lt;/strong&gt; The unglamorous systems, maintenance schedules, cleaning routines, records, backups, the people who care for the work after it is finished, are what let anything last. They never make the brochure. They are the reason there is a brochure in the second century of a company's life.&lt;/p&gt;

&lt;p&gt;Lalibela's churches were cut from a mountain by people who knew that some mistakes cannot be reversed, so they designed their work to avoid them. Eight centuries later, people still walk down those trenches to pray.&lt;/p&gt;

&lt;h2&gt;
  
  
  For builders: know which of your operations are downward cuts
&lt;/h2&gt;

&lt;p&gt;Most of what we do in software is brick-laying: a bad commit can be reverted, a bad deploy rolled back. But some operations are carving. A destructive database migration, a data deletion, an email sent to every customer, a payment captured, a message published to a public topic. There is no putting the stone back.&lt;/p&gt;

&lt;p&gt;Treat those differently. Write the outline before the first cut: a reviewed plan or migration script. Check at every stage: dry runs, staging data, a small first batch. And build the drainage: backups you have actually restored, audit logs, and alerts that fire before water reaches the floor. Lalibela's builders could not undo a wrong cut, so they designed their process so they would rarely need to.&lt;/p&gt;

&lt;p&gt;So here is my question for every founder, manager and plant team:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which of your processes cannot be undone, and are you checking them before the cut or after it?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;UNESCO World Heritage Centre, "Rock-Hewn Churches, Lalibela" (whc.unesco.org/en/list/18/): 11 monolithic churches; ~645 km from Addis Ababa; attributed to King Lalibela, "New Jerusalem" after Muslim conquests halted pilgrimages; drainage ditches, trenches and ceremonial passages; inscribed 1978.&lt;/li&gt;
&lt;li&gt;UNESCO / ICOMOS, Report of the Advisory Mission to Lalibela, May 2018 (whc.unesco.org/document/169702): among the first twelve properties inscribed in 1978; almost 800 priests; daily services; thousands of pilgrims at Ethiopian Christmas and Easter; Lasta mountain range.&lt;/li&gt;
&lt;li&gt;The Metropolitan Museum of Art, "The Rock-hewn Churches of Lalibela" (metmuseum.org/en/essays/the-rock-hewn-churches-of-lalibela): carved top-down with chisels, axes and blades; perimeter traced, structure isolated, interior sculpted; drainage canals and trenches; sloping roofs of the four free-standing churches; cisterns and baptismal pools; river Yordannos.&lt;/li&gt;
&lt;li&gt;ETH Zurich photogrammetry team, "Photogrammetric reconstruction and 3D visualisation of Bet Giorgis" (ISPRS, 2001): 12 x 12 x 13 m church in a 25 m square trench; entrance trench and tunnel; three doorways, nine blind lower windows, 12 upper windows; Greek crosses on roof; legend of Saint George; King Lalibela "one of the last kings of the Zagwe dynasty".&lt;/li&gt;
&lt;li&gt;Biblical Archaeology Society, "The Rock-Hewn Churches of Lalibela": Beta Giyorgis dated late 12th or early 13th century; other churches estimated across the 10th to 13th centuries or later; three doors, twelve windows, nine false windows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Originally published at isaacmuthui.com/stories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>history</category>
      <category>devops</category>
      <category>database</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Chemicals Are Inventory Too: Modelling a Chemical Register, Dilution Lots and Food-Grade Lubrication Rules in a Food ERP</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Mon, 26 Oct 2026 11:16:53 +0000</pubDate>
      <link>https://dev.to/yit2022/chemicals-are-inventory-too-modelling-a-chemical-register-dilution-lots-and-food-grade-15g8</link>
      <guid>https://dev.to/yit2022/chemicals-are-inventory-too-modelling-a-chemical-register-dilution-lots-and-food-grade-15g8</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/chemical-control-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In most factory software, cleaning chemicals and lubricants are an afterthought. They sit in the inventory as "consumables", next to gloves and printer paper, with a stock level and a reorder point. That is enough to buy them. It is not enough to answer the questions a food safety auditor, a safety officer or an incident investigation will ask. Is this sanitiser approved for food-contact surfaces? Where is its safety data sheet? Which grease went on the filler last week, and was it food grade? Who mixed the bucket that was found unlabelled next to the line?&lt;/p&gt;

&lt;p&gt;This post walks through how we think about modelling chemical control in an ERP for food and agro manufacturers. The operational checklist behind it is in the &lt;a href="https://factoraerp.com/en/blog/chemical-control-checklist-food-factories-kenya" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the domain rules say
&lt;/h2&gt;

&lt;p&gt;The requirements read like a spec:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codex CXC 1-1969, 13.2.6&lt;/strong&gt;: "Systems should be in place to prevent or minimize contamination of foods by harmful chemicals, e.g. cleaning materials, non-food grade lubricants, chemical residues from pesticides..." Toxic cleaning compounds, disinfectants and pesticides "should be identified, safely stored and used in a manner that protects against contamination". Chemicals should be used with "the correct dilutions and contact times" and stored "in clearly identified containers". &lt;a href="https://doi.org/10.4060/cc6125en" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 CFR 117.35(b)&lt;/strong&gt;: only toxic materials needed for cleaning and sanitation, laboratory testing, plant and equipment maintenance and operation, or the plant's operations may be used or stored where food is processed or exposed. &lt;a href="https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-117/subpart-B/section-117.35" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;21 CFR 178.3570&lt;/strong&gt;: lubricants with incidental food contact must be made from listed substances, and the amount used must be "the minimum required to accomplish the desired technical effect". &lt;a href="https://www.ecfr.gov/current/title-21/chapter-I/subchapter-B/part-178/subpart-D/section-178.3570" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kenya, Occupational Safety and Health Act 2007, s.84-85&lt;/strong&gt;: safety data sheets available at the workplace for all chemicals in use; all chemicals labelled or marked; and a "record of hazardous chemicals used at the workplace, cross-referenced to the appropriate chemical safety data sheets". &lt;a href="https://www.labour.go.ke/sites/default/files/2022-10/Occupational-Safety-and-Health-Act-2007.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EAS 39:2001, 7.3.13&lt;/strong&gt;: pesticides "shall not be used while processing is in progress". &lt;a href="https://law.resource.org/pub/eac/ibr/eas.39.2001.pdf" rel="noopener noreferrer"&gt;Source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Translated: every chemical needs an identity, an approval and a safety data sheet; it has an allowed purpose; it lives in a known, segregated location; it is often transformed (diluted, decanted) into new containers that still need identity; some equipment points demand a specific grade; some activities are forbidden while production runs; and there must be a queryable record of all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core entities
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chemical          (id, name, supplier_id, category, purpose_class,
                   food_contact_ok, food_grade_lubricant, sds_current_id, status)
SafetyDataSheet   (id, chemical_id, revision, issued_on, file_ref)
ChemicalApproval  (chemical_id, approved_by_qa, approved_by_safety, approved_at)
StorageLocation   (id, name, segregation_group, is_food_area)
ChemicalLot       (id, chemical_id, location_id, qty, unit, received_on, expires_on)
WorkingSolution   (id, parent_lot_id, container_label, target_conc,
                   measured_conc, made_by, made_at, discard_after)
LubricationPoint  (id, equipment_id, description, food_contact_risk)
ChemicalUse       (id, kind, chemical_lot_id, solution_id, point_id,
                   area_id, qty, used_by, used_at, production_running)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices matter more than the table names.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. "Purpose class" is an allow-list, not a tag
&lt;/h3&gt;

&lt;p&gt;The US rule's four categories make a neat enum: &lt;code&gt;cleaning&lt;/code&gt;, &lt;code&gt;laboratory&lt;/code&gt;, &lt;code&gt;maintenance&lt;/code&gt;, &lt;code&gt;operations&lt;/code&gt;. A chemical that cannot be assigned one should not be approvable. Approval itself needs two signatures, QA for food safety and the safety officer for worker safety, and a current SDS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chemical&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;approved_needs_sds&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;sds_current_id&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OSH Act's "record cross-referenced to the data sheets" then falls out as a plain join.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dilution creates a new identity
&lt;/h3&gt;

&lt;p&gt;The unlabelled bucket is the classic failure, and it happens because software tracks the drum but not what is made from it. Treat a working solution like a child lot: it has a parent lot, a label, a target and measured concentration, who made it, and when it must be discarded. Printing the label is part of creating the record, so there is no record without a label and no labelled bucket without a record. When a QA walk finds a container, its label should resolve to a row.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lubrication points carry the rule, not the grease
&lt;/h3&gt;

&lt;p&gt;Whether a lubricant must be food grade depends on where it goes. Model the point, not just the product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- block non-food-grade lubricant on a food-contact-risk point&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;check_lube_grade&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'lubrication'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lubrication_point&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chemical_lot&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chemical&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;point_id&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chemical_lot_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chemical_id&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_contact_risk&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_grade_lubricant&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'Point requires a food-grade lubricant'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pairs naturally with the preventive maintenance schedule: the work order names the point, so the technician picks from an already-filtered list.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Some uses are time-boxed by production state
&lt;/h3&gt;

&lt;p&gt;EAS 39 forbids pesticide use while processing is in progress. If the system knows which lines are running (it does, if production orders are in the same database), a pest treatment record can be checked against the production calendar for that area. Flag, rather than block, because treatments are often recorded after the fact by a contractor; a flagged record is an audit finding, not a silent gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Usage reconciliation is a quality signal
&lt;/h3&gt;

&lt;p&gt;Stock movements for chemicals are not just costing. Expected usage can be estimated from cleaning schedules and production hours; actual usage comes from store issues. A large variance either way is worth a look: overuse can mean over-strong dilution and residue risk, underuse can mean surfaces that were not properly sanitised. A weekly variance report per area is a cheap early warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we keep simple
&lt;/h2&gt;

&lt;p&gt;Small factories will not maintain a perfect chemical master. The model has to survive that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Default every lubrication point on food equipment to &lt;code&gt;food_contact_risk = true&lt;/code&gt; and make people justify switching it off.&lt;/li&gt;
&lt;li&gt;Let the SDS be a scanned file with a revision date; parsing it is not required to get value.&lt;/li&gt;
&lt;li&gt;Make the "unlabelled container found" event one tap on a phone, linked to an area and a photo, feeding the same non-conformance workflow as audit findings.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where FactoraERP is today
&lt;/h2&gt;

&lt;p&gt;In FactoraERP, chemicals already live in the inventory catalogue under their own category with supplier, location, stock, reorder level and receipt and issue movements; safety incidents (including near misses and hazards) and quality non-conformances have their own records. The dilution, lubrication-point and approval layers above are the direction we are designing towards, and we would welcome views from anyone who has modelled this in another domain.&lt;/p&gt;

&lt;p&gt;How do you model "derived" containers in your systems, the bucket made from the drum, so that identity survives the transformation?&lt;/p&gt;

</description>
      <category>erp</category>
      <category>database</category>
      <category>architecture</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why organisations need an enterprise agent orchestrator</title>
      <dc:creator>Isaac Mwendwa Muthui</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:50:54 +0000</pubDate>
      <link>https://dev.to/yit2022/why-organisations-need-an-enterprise-agent-orchestrator-3gd4</link>
      <guid>https://dev.to/yit2022/why-organisations-need-an-enterprise-agent-orchestrator-3gd4</guid>
      <description>&lt;p&gt;&lt;em&gt;By Isaac Muthui, CEO, Tamalaki Business Network Ltd. Originally published at &lt;a href="https://factoraerp.com/en/blog/why-organisations-need-enterprise-agent-orchestrator" rel="noopener noreferrer"&gt;factoraerp.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;AI agents can now draft replies to customers, open pull requests, update CRM records and prepare deployments. That is useful. It also raises a question most organisations have not answered yet: &lt;strong&gt;who is in charge of the agents?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If every team picks its own tools, connects its own accounts and gives its agents whatever access seems convenient, you end up with dozens of automated workers that nobody can list, budget for or audit. An enterprise agent orchestrator exists to fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: agent sprawl and shadow AI
&lt;/h2&gt;

&lt;p&gt;Staff are not waiting for a formal AI strategy. In Microsoft and LinkedIn's &lt;a href="https://www.microsoft.com/en-us/worklab/work-trend-index/ai-at-work-is-here-now-comes-the-hard-part" rel="noopener noreferrer"&gt;2024 Work Trend Index&lt;/a&gt;, 78% of people who use AI at work said they bring their own AI tools, and the figure was 80% at small and medium-sized companies. The same report notes that this puts company data at risk.&lt;/p&gt;

&lt;p&gt;Agents make this sharper, because an agent does not just answer a question. It acts. Typical symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent sprawl.&lt;/strong&gt; Agents live inside separate apps, each with its own login, keys and settings. Nobody has one list of what is running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow AI.&lt;/strong&gt; Personal accounts and API keys are used for company work, outside any policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No oversight.&lt;/strong&gt; There is no single record of what an agent did, which model it used, what it cost or who allowed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Siloed tools.&lt;/strong&gt; Email, CRM, code and cloud each get their own automation, with no shared rules for what needs a human.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The risk is not theoretical. The &lt;a href="https://genai.owasp.org/llmrisk/llm062025-excessive-agency/" rel="noopener noreferrer"&gt;OWASP Top 10 for LLM Applications (2025)&lt;/a&gt; lists &lt;strong&gt;Excessive Agency&lt;/strong&gt; as a top risk: agents with more functions, permissions or autonomy than they need can take damaging actions after a hallucination or a prompt injection. And Gartner &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;predicts&lt;/a&gt; that over 40% of agentic AI projects will be cancelled by the end of 2027, citing escalating costs, unclear business value and inadequate risk controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an orchestrator does
&lt;/h2&gt;

&lt;p&gt;An enterprise agent orchestrator is the control layer between your people, your agents, the AI models they call and the systems they touch. In practice it should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tie every agent to a named person and team&lt;/strong&gt;, so ownership is never in doubt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route model calls through one gateway&lt;/strong&gt;, with budgets and keys managed centrally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide which tools and connectors each agent may use&lt;/strong&gt;, and keep everything else switched off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop high-impact actions for a human&lt;/strong&gt;: sending email, posting externally, deploying, paying, writing to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record everything&lt;/strong&gt; in a log that cannot be quietly edited.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control.&lt;/strong&gt; You decide which models and tools are allowed, per team and per person, instead of discovering them afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit.&lt;/strong&gt; When a customer, auditor or board member asks "who approved this?", there is an answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; Spend is capped and visible before it becomes a surprise invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security.&lt;/strong&gt; Credentials stay on the server, not in prompts or personal accounts. This is the "least privilege" advice OWASP gives for agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human approval.&lt;/strong&gt; OWASP recommends human-in-the-loop approval for high-impact actions. An orchestrator makes that the default rather than an afterthought.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How TEAO approaches it
&lt;/h2&gt;

&lt;p&gt;At Tamalaki Business Network we built &lt;strong&gt;TEAO, the Tamalaki Enterprise Agent Orchestrator&lt;/strong&gt;, as the agent desk for our own team at &lt;a href="https://agents.tamalakibn.com" rel="noopener noreferrer"&gt;agents.tamalakibn.com&lt;/a&gt;. Its sign-in page sums up the design: &lt;em&gt;named people, gated actions, append-only ledger.&lt;/em&gt; TEAO is an MVP, and this is what it does today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named people own agents.&lt;/strong&gt; Staff are organised into departments and roles. Agents come from templates matched to a department and role, and a person is added by invitation. No passwords are seeded. Sign-in supports an authenticator app with backup codes, and single sign-on can be added in front.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One gateway for models, with budgets.&lt;/strong&gt; Agents call models through a gateway that routes between tiers. Paid models stay off until a super admin enables them. Spend is counted per month, with caps per department and per person. When a cap is hit, a call can either drop to a cheaper tier or pause and raise an alert. Department heads can lower a member's cap but cannot raise it above the department cap. Jobs prefer an enabled free model and escalate to a paid one only when the budget allows, and the reason is written on the job timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gated actions.&lt;/strong&gt; Every job writes a plan first. Sending mail, posting externally, deploys, payments and production writes stop for a person. So does a job that goes over its cost threshold or needs a tool or connector that has not been granted. Approvers can reject a job back for revision, or edit the text and then approve it. Approval rights follow the organisation. For example, an administration head can approve outreach sends but not payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connectors with least privilege.&lt;/strong&gt; TEAO connects to email, GitHub, a CRM, web search and files, and has sign-in patterns for services such as Google, Microsoft, Zoho, Slack, Notion, HubSpot and Canva. Credentials come from connector settings or the server environment and are never placed in the model prompt. Some limits are built in: the CRM connector never deletes, pushes to the main branch are refused, and a pull request opens only after approval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redaction, tracing and sandboxing.&lt;/strong&gt; Every model call is traced after a pass that redacts keys, card numbers and passwords. Card numbers are never stored. Code runs in a locked-down sandbox with no network, a read-only file system and tight memory, CPU and time limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An append-only, hash-chained ledger.&lt;/strong&gt; Sign-ins, jobs, approvals (including what was approved), connector and model changes, budgets and exports are written to a hash-chained activity log. Each agent run records the person, agent, model, tools used (with secrets redacted), tokens, cost, duration and outcome. Operations shows whether the chain is still intact, the log can be exported as CSV or PDF, and records are kept for at least two years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A live console with working modes.&lt;/strong&gt; A technical console shows running jobs. Each job can run in Observe, Assist, Autopilot within guardrails, Troubleshoot, Build or Human takeover mode. Whatever the mode, the gated actions still wait for approval, and every swap of agent, model or tool needs a stated reason.&lt;/p&gt;

&lt;p&gt;TEAO is self-hosted with Docker Compose and is designed to fit on one modest cloud server. Staff can also open the desk in a TEAO desktop app for Windows and macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist for choosing an orchestrator
&lt;/h2&gt;

&lt;p&gt;Before you adopt any agent platform, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is every agent owned by a named person, and can you list them all?&lt;/li&gt;
&lt;li&gt;Do model calls go through one gateway, with per-team and per-person budgets?&lt;/li&gt;
&lt;li&gt;Are paid models and new connectors off by default?&lt;/li&gt;
&lt;li&gt;Which actions always require human approval, and can you change that list?&lt;/li&gt;
&lt;li&gt;Are credentials kept out of prompts and redacted from logs?&lt;/li&gt;
&lt;li&gt;Is the audit log tamper-evident and exportable?&lt;/li&gt;
&lt;li&gt;Can a person take over a running job at any point?&lt;/li&gt;
&lt;li&gt;Can you self-host it, or at least control where your data and logs live?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to several of these is "no" or "we're not sure", you have agents but not yet an operating model for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step
&lt;/h2&gt;

&lt;p&gt;TEAO is how we run agents at Tamalaki: people stay accountable, risky actions wait for a human, and every step is on the record. To see how this approach could work in your organisation, visit &lt;a href="https://agents.tamalakibn.com" rel="noopener noreferrer"&gt;agents.tamalakibn.com&lt;/a&gt; or write to &lt;a href="mailto:info@tamalakibn.com"&gt;info@tamalakibn.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
