Agricultural operations run on tight margins and tight schedules. A delayed irrigation trigger, a missed equipment service, or a missed commodity price spike can cost thousands. Yet most farm management still relies on manual checks, spreadsheets, and phone calls.
n8n changes that. It's open-source, self-hostable, and connects 400+ services — including IoT platforms, USDA APIs, GPS telematics, and your existing spreadsheets. No per-task pricing. Your field data stays on your infrastructure.
Here are 5 production-ready n8n automations for agriculture and AgTech teams, with full import-ready JSON for each.
1. Soil Sensor Alert & Irrigation Trigger
The problem: Crop stress from under- or over-watering costs yield and water — but manually checking soil sensors across multiple zones is impractical.
The workflow:
- Schedule: Every 30 minutes
- HTTP Request: Poll each zone's IoT sensor API for soil moisture reading
- Code node: Compare reading vs. field-specific threshold (e.g., <30% VWC = dry)
- IF: If dry → Slack alert to #field-ops + HTTP Request to irrigation controller API to activate zone
- Sheets: Log reading, timestamp, action taken
{
"name": "Soil Sensor Alert & Irrigation Trigger",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "minutes", "minutesInterval": 30}]}
},
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {
"url": "={{$vars.SENSOR_API_URL}}/zones",
"authentication": "headerAuth",
"options": {}
},
"name": "Get Sensor Readings",
"type": "n8n-nodes-base.httpRequest",
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const zones = $input.all().map(i => i.json);\nconst DRY_THRESHOLD = 30;\nreturn zones.map(z => ({\n json: {\n zone_id: z.zone_id,\n zone_name: z.name,\n moisture_pct: z.soil_moisture_pct,\n is_dry: z.soil_moisture_pct < DRY_THRESHOLD,\n reading_ts: new Date().toISOString()\n }\n}));"
},
"name": "Evaluate Moisture",
"type": "n8n-nodes-base.code",
"position": [680, 300]
},
{
"parameters": {
"conditions": {
"boolean": [{"value1": "={{$json.is_dry}}", "value2": true}]
}
},
"name": "IF Dry",
"type": "n8n-nodes-base.if",
"position": [900, 300]
},
{
"parameters": {
"url": "={{$vars.IRRIGATION_API_URL}}/zones/={{$json.zone_id}}/activate",
"requestMethod": "POST",
"body": {"duration_minutes": 20}
},
"name": "Activate Irrigation",
"type": "n8n-nodes-base.httpRequest",
"position": [1120, 200]
},
{
"parameters": {
"channel": "#field-ops",
"text": "🌱 Zone *{{$json.zone_name}}* is DRY ({{$json.moisture_pct}}% VWC). Irrigation activated for 20 min.",
"otherOptions": {}
},
"name": "Slack Alert",
"type": "n8n-nodes-base.slack",
"position": [1120, 360]
}
],
"connections": {
"Schedule Trigger": {"main": [[{"node": "Get Sensor Readings", "type": "main", "index": 0}]]},
"Get Sensor Readings": {"main": [[{"node": "Evaluate Moisture", "type": "main", "index": 0}]]},
"Evaluate Moisture": {"main": [[{"node": "IF Dry", "type": "main", "index": 0}]]},
"IF Dry": {"main": [[{"node": "Activate Irrigation", "type": "main", "index": 0}], [{"node": "Slack Alert", "type": "main", "index": 0}]]}
}
}
2. Harvest Schedule & Crew Coordination Notifier
The problem: Harvest windows are narrow — a 2-day weather window for wheat or a 3-day peak for tomatoes. Coordinating crews, equipment, and logistics manually across multiple fields burns time you don't have.
The workflow:
- Schedule: Daily at 6 AM
- Sheets: Read crop calendar (field, crop, expected harvest date, crew lead, equipment needed)
- Code: Filter rows where harvest_date is within the next 7 days; calculate days remaining
- Gmail: Send personalized reminder to each field manager with their assigned crops and crew instructions
- Slack: Post daily harvest countdown summary to #harvest-crew
{
"name": "Harvest Schedule & Crew Notifier",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "hours", "hoursInterval": 24, "triggerAtHour": 6}]}
},
"name": "Daily 6AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {
"operation": "read",
"sheetName": "CropCalendar"
},
"name": "Read Crop Calendar",
"type": "n8n-nodes-base.googleSheets",
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nreturn $input.all()\n .filter(item => {\n const harvestDate = new Date(item.json.harvest_date);\n const daysLeft = Math.ceil((harvestDate - today) / 86400000);\n return daysLeft >= 0 && daysLeft <= 7;\n })\n .map(item => {\n const harvestDate = new Date(item.json.harvest_date);\n const daysLeft = Math.ceil((harvestDate - today) / 86400000);\n return { json: { ...item.json, days_left: daysLeft,\n urgency: daysLeft <= 2 ? 'CRITICAL' : daysLeft <= 4 ? 'URGENT' : 'UPCOMING' } };\n });"
},
"name": "Filter & Classify",
"type": "n8n-nodes-base.code",
"position": [680, 300]
},
{
"parameters": {
"fromEmail": "ops@farmhq.com",
"toEmail": "={{$json.manager_email}}",
"subject": "[{{$json.urgency}}] Harvest in {{$json.days_left}} day(s): {{$json.field_name}} — {{$json.crop}}",
"text": "Field: {{$json.field_name}}\nCrop: {{$json.crop}}\nHarvest Date: {{$json.harvest_date}} ({{$json.days_left}} day(s))\nCrew Lead: {{$json.crew_lead}}\nEquipment: {{$json.equipment_needed}}\n\nPlease confirm crew availability and equipment readiness."
},
"name": "Email Field Manager",
"type": "n8n-nodes-base.gmail",
"position": [900, 200]
},
{
"parameters": {
"channel": "#harvest-crew",
"text": "*Harvest Alert* — {{$json.urgency}}: {{$json.field_name}} ({{$json.crop}}) in *{{$json.days_left}} day(s)*. Lead: {{$json.crew_lead}} | Equipment: {{$json.equipment_needed}}",
"otherOptions": {}
},
"name": "Slack Crew Alert",
"type": "n8n-nodes-base.slack",
"position": [900, 400]
}
],
"connections": {
"Daily 6AM": {"main": [[{"node": "Read Crop Calendar", "type": "main", "index": 0}]]},
"Read Crop Calendar": {"main": [[{"node": "Filter & Classify", "type": "main", "index": 0}]]},
"Filter & Classify": {"main": [[{"node": "Email Field Manager", "type": "main", "index": 0}], [{"node": "Slack Crew Alert", "type": "main", "index": 0}]]}
}
}
3. Commodity Price Monitor & Alert
The problem: Corn, wheat, soybean, and livestock prices move fast. Missing a 5% intraday spike — or a contract-trigger price — can mean the difference between locking in margin and leaving money on the table.
The workflow:
- Schedule: Daily at 7 AM (or every 2 hours during market hours)
- HTTP Request: Fetch commodity prices from USDA AMS API or CME Group feed
- Code: Compare today's price vs. yesterday's; calculate delta %; check if any commodity hit a target price
- IF: If price change > 2% or target hit → Slack alert to #management + email to farm owner
- Sheets: Append price history for trend tracking
{
"name": "Commodity Price Monitor",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "hours", "hoursInterval": 24, "triggerAtHour": 7}]}
},
"name": "Daily 7AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {
"url": "https://marsapi.ams.usda.gov/services/v1.2/reports/2193",
"options": {"headers": {"API_KEY": "={{$vars.USDA_API_KEY}}"}}
},
"name": "Fetch Commodity Prices",
"type": "n8n-nodes-base.httpRequest",
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const prices = $input.first().json.results || [];\nconst TARGET_PRICES = { CORN: 4.80, WHEAT: 6.20, SOYBEANS: 11.50 };\nreturn prices.map(p => {\n const prev = parseFloat(p.prev_price || p.price);\n const curr = parseFloat(p.price);\n const delta_pct = ((curr - prev) / prev * 100).toFixed(2);\n const target = TARGET_PRICES[p.commodity];\n return { json: {\n commodity: p.commodity,\n price: curr,\n prev_price: prev,\n delta_pct: parseFloat(delta_pct),\n target_hit: target ? curr >= target : false,\n alert: Math.abs(delta_pct) > 2 || (target && curr >= target)\n }};\n}).filter(i => i.json.alert);"
},
"name": "Calculate Alerts",
"type": "n8n-nodes-base.code",
"position": [680, 300]
},
{
"parameters": {
"channel": "#management",
"text": "📈 *Commodity Alert* — {{$json.commodity}}: ${{$json.price}} ({{$json.delta_pct > 0 ? '+' : ''}}{{$json.delta_pct}}% vs yesterday){{$json.target_hit ? ' 🎯 TARGET HIT' : ''}}",
"otherOptions": {}
},
"name": "Slack Price Alert",
"type": "n8n-nodes-base.slack",
"position": [900, 300]
}
],
"connections": {
"Daily 7AM": {"main": [[{"node": "Fetch Commodity Prices", "type": "main", "index": 0}]]},
"Fetch Commodity Prices": {"main": [[{"node": "Calculate Alerts", "type": "main", "index": 0}]]},
"Calculate Alerts": {"main": [[{"node": "Slack Price Alert", "type": "main", "index": 0}]]}
}
}
4. Equipment Maintenance Tracker & Alert
The problem: A tractor breakdown during harvest is a catastrophic event. Most farm equipment has service intervals measured in engine hours or calendar time — but tracking that manually across 10–20 pieces of equipment means something always slips.
The workflow:
- Schedule: Daily at 6 AM
- Sheets: Read equipment inventory (machine, last service date, service interval, telematics hours)
- Code: Calculate days/hours since last service; compare vs. interval; classify OVERDUE / DUE_SOON / OK
- Gmail: Email mechanic with list of equipment due or overdue for service
- Slack: Alert #equipment channel
{
"name": "Equipment Maintenance Tracker",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "hours", "hoursInterval": 24, "triggerAtHour": 6}]}
},
"name": "Daily 6AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {
"operation": "read",
"sheetName": "Equipment"
},
"name": "Read Equipment Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nreturn $input.all().map(item => {\n const e = item.json;\n const lastService = new Date(e.last_service_date);\n const daysSince = Math.floor((today - lastService) / 86400000);\n const intervalDays = parseInt(e.service_interval_days || 90);\n const daysUntil = intervalDays - daysSince;\n let status;\n if (daysUntil < 0) status = 'OVERDUE';\n else if (daysUntil <= 7) status = 'DUE_THIS_WEEK';\n else if (daysUntil <= 14) status = 'DUE_SOON';\n else status = 'OK';\n return { json: { ...e, days_since_service: daysSince, days_until_service: daysUntil, status } };\n}).filter(i => i.json.status !== 'OK');"
},
"name": "Calculate Service Status",
"type": "n8n-nodes-base.code",
"position": [680, 300]
},
{
"parameters": {
"fromEmail": "ops@farmhq.com",
"toEmail": "mechanic@farmhq.com",
"subject": "[{{$json.status}}] Equipment Service Due: {{$json.equipment_name}}",
"text": "Equipment: {{$json.equipment_name}}\nStatus: {{$json.status}}\nLast Service: {{$json.last_service_date}} ({{$json.days_since_service}} days ago)\nInterval: Every {{$json.service_interval_days}} days\nDays Until/Overdue: {{$json.days_until_service}}\n\nPlease schedule service as soon as possible."
},
"name": "Email Mechanic",
"type": "n8n-nodes-base.gmail",
"position": [900, 200]
},
{
"parameters": {
"channel": "#equipment",
"text": "🔧 *{{$json.status}}* — {{$json.equipment_name}}: last serviced {{$json.days_since_service}}d ago (interval: {{$json.service_interval_days}}d). Days until/overdue: {{$json.days_until_service}}",
"otherOptions": {}
},
"name": "Slack Equipment Alert",
"type": "n8n-nodes-base.slack",
"position": [900, 400]
}
],
"connections": {
"Daily 6AM": {"main": [[{"node": "Read Equipment Sheet", "type": "main", "index": 0}]]},
"Read Equipment Sheet": {"main": [[{"node": "Calculate Service Status", "type": "main", "index": 0}]]},
"Calculate Service Status": {"main": [[{"node": "Email Mechanic", "type": "main", "index": 0}], [{"node": "Slack Equipment Alert", "type": "main", "index": 0}]]}
}
}
5. Weekly Farm Operations Report
The problem: Farm owners need a weekly pulse — yield progress, water usage, labor hours, equipment downtime, and revenue outlook — but generating it manually from multiple spreadsheets takes hours.
The workflow:
- Schedule: Every Friday at 5 PM
- Sheets: Read yield data, irrigation log, labor hours, equipment downtime log
- Code: Calculate key KPIs — bushels/acre vs. target, irrigation cost/acre, labor cost/acre, projected harvest revenue vs. expense
- Gmail: Send formatted HTML report to farm owner (+ agronomist BCC)
{
"name": "Weekly Farm Operations Report",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "cronExpression", "expression": "0 17 * * 5"}]}
},
"name": "Friday 5PM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [240, 300]
},
{
"parameters": {"operation": "read", "sheetName": "YieldData"},
"name": "Yield Data",
"type": "n8n-nodes-base.googleSheets",
"position": [460, 200]
},
{
"parameters": {"operation": "read", "sheetName": "IrrigationLog"},
"name": "Irrigation Log",
"type": "n8n-nodes-base.googleSheets",
"position": [460, 400]
},
{
"parameters": {
"mode": "multiplex"
},
"name": "Merge Data",
"type": "n8n-nodes-base.merge",
"position": [700, 300]
},
{
"parameters": {
"jsCode": "const yieldData = $input.all().filter(i => i.json.source === 'yield');\nconst irrigData = $input.all().filter(i => i.json.source === 'irrigation');\n\nconst totalAcres = yieldData.reduce((s, i) => s + parseFloat(i.json.acres || 0), 0);\nconst totalBushels = yieldData.reduce((s, i) => s + parseFloat(i.json.bushels_harvested || 0), 0);\nconst buPerAcre = totalAcres > 0 ? (totalBushels / totalAcres).toFixed(1) : 0;\nconst totalWaterGal = irrigData.reduce((s, i) => s + parseFloat(i.json.gallons_applied || 0), 0);\nconst waterCostPerAcre = totalAcres > 0 ? ((totalWaterGal * 0.001) / totalAcres).toFixed(2) : 0;\n\nconst html = `<h2>Weekly Farm Operations Report</h2>\n<table border='1' cellpadding='8'>\n<tr><th>Metric</th><th>This Week</th></tr>\n<tr><td>Acres in Production</td><td>${totalAcres}</td></tr>\n<tr><td>Bushels Harvested</td><td>${totalBushels.toLocaleString()}</td></tr>\n<tr><td>Avg Yield (bu/acre)</td><td>${buPerAcre}</td></tr>\n<tr><td>Water Applied (gal)</td><td>${totalWaterGal.toLocaleString()}</td></tr>\n<tr><td>Water Cost/Acre</td><td>$${waterCostPerAcre}</td></tr>\n</table>`;\n\nreturn [{ json: { html, buPerAcre, totalAcres, totalBushels, totalWaterGal, waterCostPerAcre } }];"
},
"name": "Build Report",
"type": "n8n-nodes-base.code",
"position": [920, 300]
},
{
"parameters": {
"fromEmail": "ops@farmhq.com",
"toEmail": "owner@farmhq.com",
"subject": "Weekly Farm Report — {{new Date().toLocaleDateString()}}",
"html": "={{$json.html}}"
},
"name": "Email Report",
"type": "n8n-nodes-base.gmail",
"position": [1140, 300]
}
],
"connections": {
"Friday 5PM": {"main": [[{"node": "Yield Data", "type": "main", "index": 0}]]},
"Yield Data": {"main": [[{"node": "Merge Data", "type": "main", "index": 0}]]},
"Irrigation Log": {"main": [[{"node": "Merge Data", "type": "main", "index": 1}]]},
"Merge Data": {"main": [[{"node": "Build Report", "type": "main", "index": 0}]]},
"Build Report": {"main": [[{"node": "Email Report", "type": "main", "index": 0}]]}
}
}
Why n8n fits agriculture better than Zapier or Make.com
| Feature | n8n | Zapier | Make.com |
|---|---|---|---|
| Self-hosted (field data stays on-site) | ✅ | ❌ | ❌ |
| Offline-capable edge deployment | ✅ | ❌ | ❌ |
| Cost at 50,000+ ops/month | ~$0 (self-hosted) | $299–999+/mo | $99–299+/mo |
| Custom IoT integrations (sensors, SCADA) | ✅ HTTP Request node | Limited | Limited |
| Git-versioned workflow JSON | ✅ | ❌ | ❌ |
| Runs on Raspberry Pi / edge hardware | ✅ | ❌ | ❌ |
Data sovereignty matters here. Field maps, soil data, crop yields, commodity positions, and equipment telemetry are commercially sensitive — and often gathered in areas with unreliable internet. Self-hosted n8n on a local server (or a $35 Raspberry Pi 5) keeps everything on your network and keeps running during connectivity gaps.
Get the templates
All 5 workflows above are available as import-ready JSON in the FlowKit n8n Automation Template Library at stripeai.gumroad.com.
The library includes 15 workflows across automation categories — each ready to import into your n8n instance, configure your credentials, and run.
Have questions or need a custom workflow? Drop a comment below.
Tags: n8n, agriculture, agtech, automation, iot













