The four layers
1. Signal layer
Continuously ingested inputs: vegetation indices (NDVI, NDRE, NDMI, LST, CWSI, Salinity Index), weather observations and forecasts (temp, humidity, leaf-wetness hours, rainfall, ET), phenology (days after sowing or transplanting, derived stage), and optional in-field sensors and scout observations.
2. Rule cards
One YAML card per condition (e.g.
rice_blast, rice_water_stress, rice_bph). Each card lists the crop, valid stage window, driver thresholds, severity formula, yield-impact curve, mitigation window, and recommended actions. Every card links back to its literature page.3. Engines
The alert engine fires on threshold crossings against the signal layer. The risk engine matches active alerts + forecasts against rule cards to produce forward-looking risk cards with yield-at-risk, estimated loss, and days to mitigate.
4. AI advisor
Retrieval-augmented generation over the literature. Given an alert or risk payload, the advisor retrieves the matching rule card and diagnosis page and returns a grounded answer with four fixed sections: what’s happening, parameters, what to do, how to do it.
Data flow
How to read this diagram- Solid arrows are data movement: satellite pass to risk card to grounded answer.
- Dashed arrows are references or lookups (rule cards being read, RAG retrieval, cross-links).
- Rule cards feed both engines, never the other way around. The alert engine reads driver thresholds and
alert_bindings; the risk engine reads the same cards plusyield_impactandmitigation. Cards are inputs, not outputs. - Literature pages feed the RAG index at build time, not at query time. The advisor queries the index, then references the underlying pages via
literature_refon the rule card. - The risk card is passed to the advisor as context when the user clicks Ask the advisor, so the LLM sees the same numbers the operator sees.
- Scout observations close the loop back into the signal layer and into
farm_historycalibration for the next season.
Rule card schema
Rule cards live alongside their literature page as YAML frontmatter under arule_card: key, so a single .mdx file is both human-readable documentation and the source of truth for the engines.
Field reference
How a risk card is produced
1
Alerts fire from the signal layer
Example:
A2 water stress unresolved (NDMI 0.19 below 0.30 healthy band) and night_temp_low from the weather forecast.2
Risk engine matches active rule cards
For field A2 (rice, booting stage), the engine loads all rule cards where
crop = rice and stage_window contains booting, then evaluates each card’s drivers against the current signal state.3
Severity and impact are computed
The
severity.formula produces a score (e.g. 0.72 → HIGH). The engine resolves yield_impact layer by layer for this field: if ecosystem = irrigated_lowland and variety_group = susceptible, it takes by_ecosystem.irrigated_lowland.booting.high = 25%, then applies the susceptible multiplier × 1.3 = 32.5%. Multiplied by the field’s expected yield and area, this becomes ~5.8 t estimated loss.4
Mitigation window is resolved
The engine reads
mitigation.window_days_by_severity[high] = 3 and applies any window_days_stage_modifier for the current stage. This becomes the “Days to mitigate” number on the card.5
Risk card is rendered
The Risk Monitoring module displays hazard, driver summary, yield-at-risk %, estimated loss,
mitigation.window_days, and the top action from mitigation.actions.6
AI advisor grounds its answer
Clicking Ask the advisor sends the risk payload plus the referenced literature page to the LLM with a fixed output schema:
whats_happening, parameters, what_to_do, how_to_do.Yield-impact layers
A single flat yield-loss table is never right for every field. The same disease behaves very differently across ecosystems and varieties.yield_impact is therefore a layered lookup, from most general to most specific. The engine resolves them in order, and the first match wins:
Ecosystem is a field-data prerequisite
For crops where ecosystem meaningfully changes risk (rice is the clearest example),ecosystem must be a captured property on the field before the risk engine can apply v1 overrides. If the field record is missing an ecosystem tag, the engine falls back to default and marks the risk card as “baseline estimate” so the operator knows precision is limited.
For crops where ecosystem is less discriminating (oil palm, pineapple, rubber), use by_region or by_soil_type in place of by_ecosystem. The layer key is generic on purpose.
Provenance and governance
Everyyield_impact block carries source and citations. Values are one of:
literature_v0- drawn from published references (IRRI, FAO, national extension).agronomy_reviewed- validated by the internal agronomy team.farm_calibrated- refined against the platform’s own field-history observations.
literature_v0 are surfaced in the agronomy review queue and re-checked annually.
Agronomic rate bands
The Alerts and Risk engines produce prescriptions. Before a prescription can be exported or applied, the VRA Maps Input validation guardrail requires every per-zone rate to fall inside a crop-specific agronomic band. The Semai Advisor uses the same band as its safety-critical write gate, and Failure Modes trips a degradation when a requested rate would exceed it. Those three references land here. A band is one row keyed on crop × product with a minimum, a maximum, a unit, a basis (per pass, per season, or per year), and a source. Anything else is out of scope for this table: recommended rate curves live in the crop diagnosis pages, not here. This is the refusal boundary, not the recipe.Units policy
Every band on this page is stated in kg of product per hectare (kg product/ha), never kg of nutrient. The two differ by the product’s nutrient fraction (urea is ~46% N, MOP is ~60% K₂O, kieserite is ~27% MgO), and a validator that silently accepts one for the other corrupts every rate it passes. Where a Malaysian source publishes a recommendation in kg nutrient/ha, the conversion to the specified product form is applied and the source figure is preserved in the citation.
Three exceptions, all explicit:
- Ethephon (pineapple forcing) is stated in mL of formulated product per plant at the labelled active-ingredient concentration, because that is how the Malaysian label directs it and how it is applied. It is not a hectare rate.
- Foliar borate on oil palm is stated in g product per palm, because MPOB extension publishes it per palm and per-hectare conversion depends on stand density.
- The three sourced oil palm peat rows are stated in kg product per palm per year, because MPOB publishes them that way. Added 13 August 2026 — this paragraph said “two exceptions” after those rows had already landed.
Basis: per pass, per season, per year
Sources publish rates on three different time bases and they are not interchangeable:- Per pass — a single application event. This is what the prescription file actually executes. VRA validation compares against this basis.
- Per season — the whole-crop-cycle total, split across passes. Used by DOA and MARDI for rice.
- Per year — the annual total for perennials. Used by MPOB for oil palm and RRIM for rubber, typically split across 2-4 rounds.
total / expected_passes for the field’s declared application schedule. Fields without a declared schedule fall back to a conservative per-pass cap of total / 3 (rice) or total / 4 (oil palm, rubber), documented on the field record so the operator knows what is being enforced.
Stage and tier keying
Two structural decisions the handoff called out explicitly:- Growth stage. Rice N at tillering and rice N at panicle initiation are different recommendations. Where a source gives stage-specific rates, the band carries a
by_stagemap with adefaultfallback, identical in shape toyield_impact.by_ecosystem. Where a source only gives a season total, the band carriesbasis: seasonand the per-pass derivation above applies; no stage keying is invented. - Ecosystem and variety. These are the same tiers the yield-impact resolver already uses. Where a Malaysian source distinguishes (for example, MPOB’s inland-mineral vs coastal-clay vs peat soil groups; DOA’s granary-scheme differences), the band carries
by_ecosystemorby_soil_groupin the same layered-lookup shape. Where the source does not distinguish, the band carries a single row and the resolver returns it for every tier. No parallel structure is introduced.
Band schema
Bands live inproduct_bands.yml alongside the rule cards. One entry per crop-product pair.
Bands
The three rows that are sourced — oil palm on peat
These carry a different unit and a different key from the rows above, and that is the point rather than an inconsistency: the unit is per palm, not per hectare, and the row is keyed by soil group. Per the units policy, a prescription submitted per hectare is rejected before the min/max check runs.
Source for all three: Hasnol Othman, Agronomy & Geospatial Unit, MPOB, Fertilizer Recommendation and Best Management of Oil Palm on Peat Soil, Workshop of MPOB–SOPPOA 2016, Sibu, Sarawak, 10 March 2016, section 8.
Verbatim: “The optimum rate of K fertilizer for oil palm on peat recommended at 4.0 - 6.0 kg MOP palm⁻¹ yr⁻¹.” · “The optimum rate of N fertilizer application is less than 1.0 kg palm⁻¹ yr⁻¹ of urea (0.5 to 0.6 kg).” · “The optimum rate of P fertilizer for oil palm on peat should not exceed 1.0 kg RP palm⁻¹ yr⁻¹.”
Both ceilings carry a stated reason, which is what makes them defensible rather than conventional. The N ceiling is an emissions limit — “Excessively high N inputs will lead to high nitrous oxide and methane emissions”. The P ceiling is pathological — “Excessive P fertilizer application should be avoided to ensure lower Ganoderma basal stem rot incidence”.
Two limits on how far these travel. They are peat only, and applying them to a mineral-soil field would be the same class of error as reading a young stand against a prime band. And the same source defers magnesium and mature-palm micronutrients to leaf analysis rather than publishing a rate — “Mg fertilizer requirement should be base on leaf analysis results” — so the absence of an Mg row here is MPOB’s position, not a gap in our sourcing.
Rice — the LCC lookup was published in 2008 and withdrawn in 2022
This is a documented point recommendation with a trigger, not a band, so it does not fill a row above. It is recorded because the previous note that no lookup exists was wrong, and because it is the nitrogen rule DOA’s own method actually uses. Jabatan Pertanian Malaysia, Pakej Teknologi Padi, Cetakan Pertama 2008, ISBN 978-983-047-139-6, Jadual 11 (tabur terus, p. 29) and Jadual 12 (mengubah, p. 30):- Threshold — “Tahap kritikal, Leaf Color Chart (LCC) No.3 atau bacaan SPAD kurang 32”
- Dose — “Baja Urea (Tambahan) … 50 kg/ha (luar musim) atau 75 kg/ha (musim utama)”
- The rate is kilograms of urea product, not kilograms of N. The document never states urea’s nitrogen content. Converting silently would be a fabricated figure.
- The chart’s panel count is never stated. Shade 3 on a four-panel chart is a different colour from shade 3 on a six-panel chart, and the two are not interchangeable.
- It is variety-blind. No variety column, and MR263 and MR269 do not appear in the document.
- Two Malaysian sources disagree. IADA Pulau Pinang (Manual Tanaman Padi, KPKM, 2019) uses No. 3 for direct-seeded and No. 4 for transplanted, and prints 76 rather than 75 kg/ha. Decision required before either is used.
Bantuan Pemberian Kerajaan, a subsidy allocation rather than an agronomic optimum.
One behaviour that is safe to encode now. Main season carries more nitrogen than off-season — MARDI +15%, DOA +50% on the LCC top-up — and the published reason is leaching under higher main-season rainfall, not the yield gap. Main season yields less and receives more. Any interface implying more fertiliser, more yield reads backwards here.
Decision required
Surfaced explicitly so they are not lost in the table:- Decision required: For rice, does MARDI/DOA publish stage-specific N min/max (tillering, panicle initiation), or only a season total? This determines whether the band uses
by_stageorbasis: seasonwith per-pass derivation. Partly answered, 11 August 2026: stage-specific, and as points rather than a min/max. DOA gives four discrete events with named stages and day windows; MARDI gives a season total of 120:70:80 kg N, P₂O₅, K₂O per hectare with N raised 15% for the main season. Three mutually inconsistent national rates are now on record — DOA 170:80:150, MARDI 120:70:80, and 90–120 N / 12–18 P / 90–120 K cited in the literature — and none is a function of a soil test. Which one the platform treats as the reference is a decision, not a lookup. - Decision required: For oil palm N, K, and Mg, does MPOB publish bands by soil group (inland mineral, coastal clay, peat), or a single band? This determines whether the row uses
by_ecosystem/by_soil_groupor a flat band. Partly answered, 11 August 2026. Peat is handled as its own regime and does carry ranges — see the three sourced rows above. Mineral soils are keyed by soil series, not by the three-way inland / coastal / peat split assumed here, and every mineral-soil schedule retrieved is points. The remaining question is narrower: what does MPOB Oil Palm Bulletin 72 contain? It is reachable atpalmoilis.mpob.gov.my/publications/OPB/opb72-afandi.pdf— no longer a dead link — but it is a scanned document with no text layer, as are the MPOB TT series. Index snippets show a table classifying nutrients very low to very high and a nutrient-balance table. OCR of that one document is the highest-value outstanding action on this page. - Decision required: For rubber, do RRIM recommendations distinguish immature (pre-tapping) vs mature (tapped) stands within the min/max, or only in the recommended rate? Stands change the band, not just the recipe.
- Decision required: For pineapple ethephon, is there a Malaysian primary source (MARDI or the product label registered with Department of Agriculture) that publishes a maximum per-plant dose that can be defended, or should this product be left undefined and therefore refused? If no defensible source, leave undefined; the platform will refuse ethephon prescriptions rather than accept any rate.
- Decision required: For every row above, the exact citation (document title, year, page or section) that will be recorded on the band. Recommendations change across MARDI/DOA/MPOB revisions; the band must name the revision it comes from.
Fail-closed rule for undefined combinations
Where no band exists for a crop and product, the platform declines the prescription. It does not fall back to “any rate is acceptable.” This is the rule that keeps the guardrail honest while the table is being filled in. Concretely:Amended 13 August 2026. The three bullets below were written before Attribution existed, and said flatly that a missing band rejects. A missing band alone no longer rejects — a missing band and no named author does. They are corrected here so this section and the next agree.
- The VRA Maps Input validation guardrail rejects a prescription whose
(crop, product)pair has noproduct_bandsentry, or has an entry withstatus: decision_requiredorstatus: undefined, and whose rate carries no named entitled author. The rejection cites the missing band by id and links here. Where an entitled author has set the rate, it passes as attributed and is labelled as such on the prescription. - The Semai Advisor treats a missing band with no author as a hard refusal, not a confirmation prompt. Confirmation only applies where a band exists and the requested rate is outside it. The advisor may never supply the author itself — a pre-filled rate is unattributed until a person sets or accepts it, so the advisor’s own drafts refuse until someone signs them.
- The Failure Modes trigger “requests an action that would exceed agronomic min/max rates” extends to “requests an action for a crop-product pair with no defined band and no named author.” Both fire the same degradation.
- The unit is checked before the value. A rate whose unit does not match its band — a per-hectare figure against a per-palm band — is refused on the unit, and no min/max comparison is attempted. Converting silently would fabricate a figure.
Attribution: the second route to a permitted rate
The rule above is correct and it has a consequence worth stating plainly: while every band isdecision_required, the guardrail permits nothing. A guardrail that has never let a prescription through is not yet a working feature, and the table may stay unfilled for a long time — no Malaysian authority publishes a min/max for any crop-product pair, only point recommendations.
So there are two routes to a permitted rate, and only two:
A rate with neither a band nor an author is still refused. That is the fail-closed rule, unchanged. What changes is that no source and no accountability stop being the same condition.
What an attributed rate must carry
Every attributed rate recordsset_by, set_by_role, set_at, and a stated reason. These are not a band: a band is a range that validates many prescriptions, and an attributed rate is one figure on one map. Do not add a fifth status value to the bands table for this — the band stays decision_required until somebody sources it, and attribution is recorded on the prescription instead.
Attribution never substitutes for a regulatory limit
Attributed rates apply to fertiliser and soil amendments only. They are refused, regardless of who signs, for:- restricted products — pesticides and high-nitrogen formulations, per the existing safety floor on VRA Maps;
- anything constrained by regulation rather than agronomy — buffer zones near waterways, regulated boundaries, and statutory maximum application rates.
An attributed rate does not become a default
A figure entered once will be copied to the next map, and to the one after that, until it is a house standard nobody has read the reasoning for. That is the same fabricated default this page refuses, arrived at by drift instead of by decision. So a rate carried forward from an earlier map carries its origin with it — who set it, when, and on which map — and the reader is told it is being reused rather than decided. Reuse is not authorship.The difference is visible where the rate is used
A sourced rate and an attributed rate must be distinguishable on the prescription itself, not only in a settings table. The bands table will hold a mix for a long time — the one genuine published range found to date is ethephon on pineapple — and a citation and a judgement that look identical at the point of use are worse than either alone. The wording changes with the route, and both remain answers:- No band, no author — “No published recommendation exists for this crop and product, and nobody has set a rate for it. This prescription is refused.”
- No band, named author — “This rate is not sourced from any published recommendation. It was set by Siti Rahman on 3 August and applies to this map only.”
Who may set one
Decided 11 August 2026.agronomist, approver and estate_manager may set an attributed rate. scout may not — its documented function is ground-truth work, not diagnosis, which is the same reason it cannot raise a finding above medium.
The mismatch that blocked this is resolved: the role matrix on VRA Maps named Operator and Read-only, neither of which is one of the eight. No ninth role was added. Operator’s permissions were distributed — as-applied upload to scout, generation to agronomist and above — and Read-only became viewer with its review permission removed, since attaching a comment is a write.
And the entitlement now has a second half: the approver may not be the author. A rate resting on one person’s judgement, approved by that same person, is a single unchecked point of decision on a physical operation — which is precisely what attribution exists to prevent. The rule and its fallback are on VRA Maps → Approval is a second person.
Provenance
Each row on the bands table carries the same provenance tags asyield_impact:
sourced— attributed to a named Malaysian primary source with an exact citation. Enforced.decision_required— schema exists, numbers do not. Rejected by the fail-closed rule.undefined— the agronomy team has explicitly declined to publish a band for this pair (e.g. product not supported in Malaysia, or no defensible source). Rejected by the fail-closed rule with an explanatory message.agronomy_reviewed— a band adjusted from the primary source by the internal agronomy team, with rationale recorded.
Docs Delta
New section on this page: Agronomic rate bands (schema, units policy, basis rules, stage and tier keying, band table, fail-closed rule, provenance). The following pre-existing references now resolve to a live definition on this page:- VRA Maps — Input validation and Refusals rows in the guardrails table. “Rates must fall within the crop-specific agronomic min/max declared in the Risk Model” now points at the bands table above.
- Semai Advisor overview — safety-critical writes list. “Rates outside agronomic min/max” now resolves to the same bands.
- Semai Advisor failure modes — degradation trigger for “exceed agronomic min/max rates” now resolves to the same bands.
sourced band and no named author produces a refusal, not a silent pass. This applies to VRA validation, Semai Advisor writes, and the failure-modes trigger, uniformly.
Attribution is the second route, added above: where no band exists, a rate may proceed if it carries a named author who was entitled to set it, and it is then rendered as a judgement rather than as a citation. It does not apply to restricted products or to anything constrained by regulation, where the safety floor stands unchanged.
Both decisions that section opened are now closed — agronomist, approver and estate_manager may set an attributed rate, and the approver may not be the author. prescription-maps was reconciled with the role model in the same pass: Operator distributed across the eight, Read-only resolved to viewer, no ninth functional role added.
Not changed: no new guardrail categories, no new telemetry types. Attribution is recorded on the prescription and does not add a status value to the bands table — a band remains decision_required until it is sourced.
Days to mitigate
The “Days to mitigate” number on the risk card is not a static rule constant. It answers a specific question: how long does the operator have before the recommended action stops being effective? The engine resolves it in two steps:- Pick the base window from
mitigation.window_days_by_severityusing the current severity band. - Add any
window_days_stage_modifiervalue for the current phenology stage (usually negative, tightening the window at critical stages like heading or flowering).
3 + (-2) = 1 day. That is honest: at heading, once conditions are right for neck blast, the preventive-spray window is very short.
Rules that describe slow-developing conditions (nutrient deficiencies, salinity buildup) can keep windows in the 10-30 day range. Rules for fast-moving weather-driven diseases should tighten aggressively at HIGH severity.
Activity bindings
alert_bindings covers data-driven inputs. activity_bindings covers team activity - the human side of the platform. Together they let a rule card react to both a falling NDVI and a completed scout report in the same way.
Each binding declares an event, an optional when condition, and an effect on the rule’s current severity for a field.
Effect vocabulary
Bindings are evaluated in order; the first match applies. All severity changes are logged with the triggering event so operators can audit why a card was promoted.
Authoring rule cards
- Start from the existing diagnosis page (for example, Blast).
- Extract the numeric thresholds already stated in prose (e.g. “cool nights below 22 C and humidity above 90% for 3+ nights”) into
drivers. - Ask the agronomy team for a per-stage yield-loss table if one is not already published. This is usually the only new content required.
- Add the
rule_card:block to the page’s frontmatter. The page continues to render normally; the engine reads the block at build time. - Set
last_reviewedto today and open a PR. Cards are validated against a JSON Schema in CI.
What the AI advisor returns
Every advisor response follows the same four-part schema, populated from the rule card and the retrieved literature page:
This structure is what makes the advisor useful in the field: the operator never has to guess whether the AI is speculating or citing your literature.
Related
- Vegetation Indices - the signal layer’s core inputs.
- Field Data Model - fields, zones, seasons, observations the engines run against.
- Alerts - user-facing alert types and delivery.
- Rice Blast - reference page with a fully populated
rule_card:example. - Aggregation Model - how rule card counts and areas roll up to estate, regional, and national dashboards.
- Fields Workspace - the operator view where rule-card findings are read and acted on.
- Crop Cycle Models - Season Type vocabulary that
season_type_filtergates on.