The Planning Loop
At every solar milestone and band transition, Iris (our OpenClaw AI agent) runs as an OpenClaw agent over a structured greenhouse context window. Routine checks and smaller deviations route through Gemma 4 26B A4B (MoE), served locally under the gemma4-26b alias; larger milestone reviews and major deviations can route to a heavyweight cloud peer. Iris does not choose what the greenhouse should target. The crop target band handles that, computed from the diurnal profiles of the currently active control crops with smooth hour-by-hour interpolation. As of the May 8 launch check, Verdify has four active-control crop profiles; a fifth active DB crop record, the center-zone Vanda orchid, is treated as observed/reference while the center zone remains offline. The planner chooses how aggressively the controller should chase those targets given forecast, memory, equipment state, and known physical limits. When the plan lands, Slack Operations is the human-readable layer: plan ID, prior scorecard, forecast, tactical posture, experiment, and watch items.
Each plan is a hypothesis. The system measures what happened, scores the result, and extracts lessons when predictions diverge from reality.
Control split: crop profiles define target bands, Iris chooses bounded tactics, MCP and the dispatcher validate writes, and ESP32 firmware decides relay state every 5 seconds.
The Closed Loop
Assemble context from sensors, forecast, crop bands, equipment, lessons, scorecards, prior plans, and static greenhouse documentation.
Score the previous plan and extract a lesson when reality diverged from the hypothesis.
Send routine or minor events to Gemma 4 26B A4B (MoE), served locally under the gemma4-26b alias; reserve heavyweight cloud reasoning for major reviews and deviations.
Balance temperature, VPD, water, fog, heat, light, cost, lessons, and retrieved context against the next 72 hours.
Write tactical waypoints for target tightening, hysteresis, misting, fog, fan staging, and water budget.
Every 5 minutes, push changed setpoints to the ESP32; firmware enforces physical state every 5 seconds.
Post the operator-facing Slack summary: what changed, why it changed, and what a human should watch.
Logged outcomes become scorecard data and validated lessons for the next planning cycle.
Step 0: Validate the Previous Plan
Before writing anything new, the planner reviews what happened since its last plan. It reads the plan_journal entry — hypothesis, experiment, expected outcome — then checks actual data: stress hours, zone temperatures, VPD compliance, equipment runtimes, water usage. It scores the plan 1–10.
If something unexpected happened, the planner extracts a lesson. Lessons accumulate in the planner_lessons table with confidence that increases each time the pattern is re-confirmed. The claim is not that the AI rewrites itself; it is that future plans can read validated outcomes from earlier failures.
Step 1: Gather Context
The planner assembles structured context from the database:
Zone temperatures, VPD, humidity, CO2, lux, outdoor weather, active ESP32 mode, relays, and tunables.
Yesterday's hourly temperature, VPD, RH, outdoor conditions, and today's heat, cold, and VPD stress hours.
72-hour forecast, solar radiation, cloud cover, wind, and crop target bands for each hour.
Active crops, zones, growth stages, VPD targets, health scores, disease risk, and DIF.
Validated lessons, previous waypoints, plan journal hypotheses, and scorecard outcomes.
Website pages describing the greenhouse structure, equipment, zones, crops, known limits, and build notes.
The local-memory story matters. Iris is not reasoning from a blank prompt. The context bundle pulls current telemetry and prior outcomes, the static-context builder makes the website’s greenhouse documentation available to the agent, and the lessons table gives durable findings a structured place to live. OpenClaw now uses trigger-scoped planner sessions so the local gemma4-26b route gets bounded context while memory remains database-backed and auditable. Semantic similarity is already used for observation history where embeddings exist; plan history and scorecards are exposed today as structured memory so the planner can compare the next decision against what actually happened last time.
Step 1b: Route the Planner Instance
Not every planning event needs the same model. Verdify uses a dual-Iris policy:
gemma4-26b aliasRoutine heartbeats, transitions, and minor forecast/deviation checks. Cheap, local, frequent, and bounded. See Local Inference Setup.
Sunrise, sunset, midnight, and major forecast or deviation events that need deeper reasoning over the full context. This route is explicit and audit-stamped, not a hidden fallback.
Both instances use OpenClaw, MCP tools, trigger IDs, planner-instance stamps, setpoint validation, scorecards, and the same ESP32 safety boundary.
Step 2: Reason Across All Systems
The planner doesn’t optimize one variable. It reasons across seven interconnected systems simultaneously:
- Temperature — The south zone runs 9°F hotter than east at peak. temp_high applies to the average, so south is already in COOL_S2 when the average hits threshold.
- VPD — On a 14% RH day, the system fights VPD stress for 8+ hours. Lowering vpd_high means misters engage earlier — more water, more humidity, less plant stress.
- Misting — 60-second pulses with 45-second gaps is the tuned sweet spot. Higher VPD weight concentrates water on the driest zone.
- Economiser — When outdoor enthalpy is lower than indoor, the vent opens for free cooling. The planner understands enthalpy dynamics when planning ventilation strategy.
- Grow lights — Two circuits (816W + 630W) supplement natural DLI. The planner adjusts the lux threshold based on the DLI forecast.
- Irrigation — Misting serves dual purpose: humidity control AND irrigation via foliar absorption.
- Cost — Electric heat costs 3.9× more per BTU than gas. Running fog during solar production can be cheap, but it still appears in the resource accounting.
Step 3: Write the Plan
The planner writes two categories of parameters: Target tightening (optional, clamped to crop band): The crop band sets the outer envelope (e.g., temp 72-78F at midday). The planner can tighten within the band on mild days. If it sets temp_high=75, the ESP32 targets 75 instead of 78. On extreme days, it leaves these alone and the band edges are used. Values are always clamped to the band. The planner cannot exceed crop science limits. Tactical parameters are the planner’s main output:
vpd_hysteresis, temp_hysteresis, and optional target tightening inside crop limits.
d_cool_stage_2, fan cycling limits, and enthalpy-based economiser enablement.
mister_engage_kpa, pulse duration, pulse gap, and daily water budget.
fog_burst_min and escalation timing when misters cannot control VPD.
Minimum heat on/off times and staged electric/gas heat behavior.
The Slack brief is not a separate source of truth. It is the readable operator version of the same plan_journal and setpoint_plan records: plan ID, hypothesis, forecast context, expected risks, and watch items. See Slack Operations for screenshots of a successful SUNRISE plan, daily operator tasks, and a forecast-deviation adjustment.
AI-Writable Tunables
Iris can write only registry-approved tunables. set_plan is stricter than set_tunable: routine sunrise/sunset plans must include the 24 tactical parameters below at each waypoint and may not include crop-band parameters. set_tunable can make a one-shot adjustment to any planner-pushable registry entry; band-owned values are still clamped or overwritten by the band dispatcher.
Write path: Iris calls MCP set_plan or set_tunable; MCP validates names and registry bounds, stamps trigger audit data, and writes setpoint_plan. v_active_plan resolves the active value per parameter. The dispatcher pushes changed values to ESPHome number/switch entities and logs actual ESP32 reports in setpoint_changes; /setpoints is the 5-minute HTTP fallback. setpoint_snapshot stores cfg_* readbacks from firmware so pushed intent can be checked against actual configured state.
| Tunable lane | Planner can do | Firmware still owns |
|---|---|---|
| Routine plan contract | Write 24 tactical parameters at each waypoint. | Relay timing, mode priority, hard safety rails, and dwell enforcement. |
| One-shot knobs | Make bounded tactical adjustments through set_tunable. | Clamp behavior, readback truth, and physical actuator state. |
| Band-owned values | See and occasionally request clamped overrides. | Crop-band recomputation and controller enforcement. |
| Non-writable safety rails | Read for context only. | Emergency heat/cool/VPD safety behavior. |
Routine Plan Contract
These 24 parameters are required in every multi-waypoint plan. Bounds are registry and firmware clamps.
| Parameter | Default | Bounds | Relay/state-machine impact |
|---|---|---|---|
vpd_hysteresis | 0.30 kPa | 0.05-0.50 | Sets seal/dehumidify exit deadband; larger values reduce SEALED_MIST churn. |
vpd_watch_dwell_s | 60 s | 15-120 | Delay before high VPD can enter SEALED_MIST. |
mister_engage_kpa | 1.6 kPa | 0.5-2.5 | Global VPD threshold for first mister-zone demand. |
mister_all_kpa | 1.9 kPa | 1.0-2.5 | Escalation threshold for all-zone rotation. |
mister_pulse_on_s | 60 s | 30-90 | Mister valve on-time per pulse. |
mister_pulse_gap_s | 45 s | 10-60 | Evaporation gap between mister pulses. |
mister_vpd_weight | 1.5 | 0.5-5.0 | Extends pulse duration for the most VPD-stressed zone. |
mister_water_budget_gal | 500 gal | 100-600 | Daily mister budget; blocks misters unless VPD safety override fires. |
mist_vent_close_lead_s | 15 s | 0-60 | Lead time before closed-vent misting. |
mist_max_closed_vent_s | 600 s | 120-900 | SEALED_MIST max duration before THERMAL_RELIEF. |
mist_vent_reopen_delay_s | 45 s | 0-120 | Holds vent closed after misting so evaporation can settle. |
mist_thermal_relief_s | 90 s | 30-300 | Mandatory vent-open relief duration after sealed misting. |
enthalpy_open | -2.0 kJ/kg | -5.0-0.0 | Economiser unblocks venting when outdoor air is sufficiently lower enthalpy. |
enthalpy_close | 1.0 kJ/kg | -5.0-20.0 | Economiser blocks venting when outdoor air is too costly/wet/hot. |
min_vent_on_s | 60 s | 10-300 | Minimum vent relay on dwell. |
min_vent_off_s | 60 s | 10-300 | Minimum vent relay off dwell. |
min_fog_on_s | 60 s | 15-300 | Minimum fog relay on dwell. |
min_fog_off_s | 60 s | 15-300 | Minimum fog relay off dwell. |
fog_escalation_kpa | 0.4 kPa | 0.1-1.0 | VPD margin above high band that escalates from misters to fog. |
d_cool_stage_2 | 3.0F | 2.0-15.0 | Extra degrees above cooling target before both fans run. |
bias_heat | 0.0F | -10.0-10.0 | Shifts heating target; positive values preheat and reduce cold stress. |
bias_cool | 0.0F | -10.0-10.0 | Shifts cooling target; positive values delay ventilation. |
min_heat_on_s | 120 s | 30-300 | Minimum heat relay on dwell. |
min_heat_off_s | 180 s | 60-600 | Minimum heat relay off dwell; protects against gas-valve rapid cycling. |
Other Tier-1 One-Shot Knobs
These are planner-pushable through set_tunable, but are not required in every routine plan.
| Parameter | Default | Bounds | Relay/state-machine impact |
|---|---|---|---|
d_heat_stage_2 | 5.0F | 2.0-15.0 | Temperature drop below heating target before gas heat stage 2 latches. |
temp_hysteresis | 1.5F | 0.5-3.0 | Temperature mode deadband for VENTILATE/IDLE transitions. |
heat_hysteresis | 1.0F | 0.0-3.0 | Heat-stage clear margin; higher values hold heat longer. |
mister_engage_delay_s | 45 s | 30-900 | Delay before first mister pulse during moisture demand. |
mister_all_delay_s | 300 s | 60-900 | Delay before all-zone mister escalation. |
sw_summer_vent_enabled | 1 | 0/1 | Enables the outdoor-cooler-and-drier gate that preempts SEALED_MIST in favor of VENTILATE. |
vent_prefer_temp_delta_f | 5.0F | 2.0-15.0 | Outdoor air must be this much cooler before summer vent preemption. |
vent_prefer_dp_delta_f | 5.0F | 2.0-15.0 | Outdoor dew point must be this much lower before summer vent preemption. |
outdoor_staleness_max_s | 600 s | 120-1800 | Disables summer vent gate when outdoor data is stale. |
summer_vent_min_runtime_s | 180 s | 60-600 | Minimum runtime for summer vent gate. |
sw_fog_closes_vent | 1 | 0/1 | Suppresses fog while the vent is physically open, except v2 vent-assist paths. |
sw_mister_closes_vent | 0 | 0/1 | Suppresses misters while the vent is physically open. |
sw_dwell_gate_enabled | 0 | 0/1 | Enables the non-safety mode dwell gate. |
dwell_gate_ms | 300000 ms | 60000-1800000 | Minimum non-safety mode hold time; safety and VPD rescue still preempt. |
sw_fsm_controller_enabled | 1 | 0/1 | Controller v2 band-first state machine. Locked ON by dispatcher/MCP guardrails; legacy fallback requires operator/firmware rollback. |
mist_backoff_s | 600 s | 60-3600 | Lockout after sealed humidification times out. |
Band-Owned Values
These are visible to Iris and technically planner-pushable as one-shot overrides, but routine plans do not own them. /setpoints recomputes temp_low, temp_high, vpd_low, and vpd_high from crop profiles and clamps any one-shot value inside the active crop band. Per-zone VPD targets are also dispatcher-owned from crop data.
| Parameter | Default | Bounds | Owner and impact |
|---|---|---|---|
temp_low | 40.0F | 30.0-80.0 | Crop-band low edge; firmware targets the interior of this band for heat decisions. |
temp_high | 95.0F | 40.0-100.0 | Crop-band high edge; cooling and safety-adjacent seal gates reference it. |
vpd_low | 0.35 kPa | 0.1-1.0 | Low-VPD band edge; below it firmware enters DEHUM_VENT when economiser allows. |
vpd_high | 2.8 kPa | 0.4-3.0 | High-VPD band edge; above it firmware starts the VPD watch and may seal/mist. |
vpd_target_south | 1.3 kPa | 0.3-3.0 | South-zone mister scoring target. |
vpd_target_west | 1.2 kPa | 0.3-3.0 | West-zone mister scoring target. |
vpd_target_east | 1.0 kPa | 0.3-3.0 | East-zone stress proxy; boosts adjacent misting because east has no mister. |
vpd_target_center | 0.8 kPa | 0.1-3.0 | Center/average VPD target for center mister scoring. |
Tier-2 Escape Hatch
Iris can set these through set_tunable, but normal planning rarely should. They are mostly operator posture, irrigation, lighting, diagnostic, or older duty-cycle controls.
| Group | Parameters | Defaults and bounds | Impact |
|---|---|---|---|
| Mister scoring | mister_center_penalty, east_adjacency_factor | 0.5 and 0.3; both 0.0-1.0 | Reweights which zone gets the next mister pulse. |
| Legacy mister duty | mister_on_s, mister_off_s, mister_all_on_s, mister_all_off_s, mister_max_runtime_min | 300 s 60-600; 600 s 120-900; 480 s 120-600; 420 s 120-600; 120 min 5-480 | Legacy duty-cycle and runtime caps; current control primarily uses pulse rotation. |
| Fog safety window | fog_rh_ceiling_pct, fog_min_temp_f, fog_time_window_start, fog_time_window_end | 90% 75-98; 55F 40-65; 6 h 5-12; 18 h 14-20 | Blocks fog when too humid, too cold, or outside the allowed local-hour window. |
| Relief and dehum | max_relief_cycles, dehum_aggressive_kpa, vent_latch_timeout_ms, safety_max_seal_margin_f | 3 cycles 1-10; 0.6 kPa 0.05-1.0; 1800000 ms 60000-7200000; 5F 1-15 | Controls relief-cycle breaker, aggressive dehumidifying, forced-vent retry timeout, and hot seal blocking. |
| Relay dwell extras | min_fan_on_s, min_fan_off_s, lead_rotate_s | 120 s 30-300; 90 s 30-300; 600 s 60-1800 | Fan relay dwell and lead/lag rotation. |
| Manual burst durations | fan_burst_min, vent_bypass_min, fog_burst_min | 10 min each; 1-60 | Durations used by manual HA burst/bypass buttons. |
| Economiser posture | econ_heat_margin_f, sw_economiser_enabled | 5F 1-15; switch default 1 | Blocks or enables enthalpy-based venting, especially when heat demand exists. |
| Irrigation schedule | irrig_wall_start_hour, irrig_wall_start_min, irrig_wall_duration_min, irrig_wall_fert_duration_min, irrig_wall_fert_every_n, irrig_wall_flush_min, irrig_wall_interval_days, irrig_center_start_hour, irrig_center_start_min, irrig_center_duration_min, irrig_center_fert_duration_min, irrig_center_fert_every_n, irrig_center_flush_min, irrig_center_interval_days | Wall 06:00, 10 min, fert 5 min every 0, flush 2 min, interval 1 day. Center 06:30 with same durations. Hours 0-23, minutes 0-59, durations 1-120 or 0-60, every-n 0-30, flush 0-30, interval 1-14. | Schedules drip and fertigation state machines; not climate relay control. |
| Irrigation VPD boost | irrig_vpd_boost_pct, irrig_vpd_boost_threshold_hrs | 25% 0-200; 3 h 0-24 | Extends irrigation after sustained high-VPD stress. |
| Grow lights | gl_dli_target, gl_lux_threshold, gl_lux_hysteresis, gl_sunrise_hour, gl_sunset_hour, sw_gl_auto_mode | 14 mol 1-50; 3000 lx 100-50000; 1500 lx 0-10000; 7 h 0-23; 19 h 0-23; switch default 1 | Controls supplemental light eligibility and cycling. |
| Irrigation switches | sw_irrigation_enabled, sw_irrigation_wall_enabled, sw_irrigation_center_enabled, sw_irrigation_weather_skip | switches default 1 | Enables irrigation branches and rain-skip behavior. |
| Occupancy switch | sw_occupancy_inhibit | switch default 1 | Blocks mist/fog while the greenhouse is occupied. |
Not AI-Writable
Safety rails and site constants are registry entries but MCP rejects planner writes to them: safety_min, safety_max, safety_vpd_min, safety_vpd_max, and site_pressure_hpa. Firmware still consumes the safety rails directly: safety_min forces SAFETY_HEAT, safety_max forces SAFETY_COOL, and the VPD rails trigger dry/humid safety rescue paths.
Step 4: Journal the Hypothesis
Every plan writes a structured journal entry: what the world looks like, what the planner thinks will happen, one specific experiment being tested, and a measurable expected outcome. The next cycle validates the hypothesis and scores the result. This isn’t logging for debugging. It’s the experimental protocol that turns each cycle into a learning opportunity.
Step 5: Dispatch to the ESP32
The dispatcher runs every 5 minutes inside the ingestor service. It reads the active plan, compares each value to what the ESP32 currently reports, and pushes only changed values via aioesphomeapi (encrypted, immediate). The ESP32 also pulls /setpoints via HTTP every 5 minutes as a fallback.
Between dispatches, the ESP32 runs completely autonomously. If the AI layer goes offline, the controller keeps the last valid setpoints and hard safety rails.
The Learning System
- Low: First observation. Might be coincidence.
- Medium: Confirmed 2+ times under similar conditions.
- High: Validated 5+ times. Mandatory unless conditions clearly differ. The planner must check every active lesson before finalizing a plan. See Lessons Learned for the full list.
Planning Goals (Priority Order)
- Minimize VPD stress hours — plant transpiration balance
- Minimize heat stress hours — physics-limited by cooling capacity
- Maximize DLI — daily light integral for growth
- Minimize water usage — misting water isn’t free
- Minimize energy cost — USD 0.111/kWh, USD 0.83/therm, USD 0.00484/gal
- Maintain positive DIF — 8–15°F day warmer than night
- Maximize compliance % — time within setpoint bands These are ordered. The planner will spend water and energy to reduce VPD stress. It won’t sacrifice plant health for a lower utility bill.
Control Loop Performance
Full dashboards: Controller ↗ · Planning ↗