{
  "name": "Lumeor | Tenant maintenance requests by text",
  "nodes": [
    {
      "parameters": {
        "content": "## Tenant maintenance request starter (text intake)\n\nTenants text your maintenance number. A keyword rule runs **before any AI**: gas, smoke, fire, carbon monoxide and sparking get a scripted safety reply and page on-call immediately. Everything else is classified by Claude into trade, priority and a two-line summary, matched to the preferred vendor for that trade and property, and written to a work order sheet with status \"Needs review\" or \"Ready\". The tenant gets a reply in their own language, with one follow-up question when details are missing. Point the sheet at your PM software's API (Buildium, AppFolio or Yardi, where you have access) or use it as a review queue.\n\n### Setup\n1. Make a Google Sheet with tabs **Tenants** (phone, tenant_name, property, unit), **Vendors** (trade, property, vendor_name, vendor_phone, nte_amount) and **Work orders** (headers as on the append node). Phones in E.164.\n2. In Twilio, set your maintenance number's incoming message webhook to **Tenant text (Twilio)**.\n3. In **Normalize and emergency screen**, review `LIFE_SAFETY`, `URGENT` and the scripted safety message with your maintenance lead.\n4. Replace `REPLACE_WITH_SHEET_ID`, `REPLACE_WITH_TWILIO_NUMBER`, `REPLACE_WITH_ONCALL_CHANNEL_ID` and `REPLACE_WITH_MAINTENANCE_CHANNEL_ID`.\n5. Credentials: Google Sheets OAuth2, Anthropic API, Twilio, Slack.\n\nTenants who reply STOP are blocked by Twilio; this workflow does not reply to opt-out words. Entry notices must follow your state and local rules.\n\nModel: `claude-opus-5-5` handles messy texts and other languages well. `claude-haiku-5-5` is a cheaper option for high-volume, simple classification.\n\nTemplate by Lumeor Studio, https://www.lumeor.studio/guides/automate-tenant-maintenance-requests/",
        "height": 900,
        "width": 580,
        "color": 7
      },
      "id": "a7cb6738-4df5-4cb6-b508-92807341d020",
      "name": "About this template",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -600,
        -300
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "tenant-maintenance-sms",
        "options": {
          "noResponseBody": true
        }
      },
      "id": "fcfa31f6-6e1b-4019-bdbf-967e9020815a",
      "name": "Tenant text (Twilio)",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "webhookId": "49bffb45-cdf3-44e7-8ac4-ca4dd26be379"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Life-safety handling must be predictable, so it is a keyword rule, not a model call.\nconst LIFE_SAFETY = ['gas', 'smell gas', 'smoke', 'fire', 'carbon monoxide', 'co alarm', 'co detector', 'sparking', 'sparks', 'burning smell'];\nconst URGENT = ['flood', 'flooding', 'leak through ceiling', 'leaking through the ceiling', 'water coming through', 'sewage', 'sewer backup', 'no heat', 'locked out', 'lockout', 'no water'];\nconst OPT_OUT = ['STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT', 'REVOKE', 'OPTOUT'];\nconst SAFETY_REPLY = 'If you smell gas, see smoke or fire, or a CO alarm is sounding: leave the unit now and call 911. Then call our emergency line at REPLACE_WITH_EMERGENCY_PHONE. We have paged our on-call team.';\n\nconst has = (text, list) => list.some((k) => new RegExp('\\\\b' + k.replace(/ /g, '\\\\s+') + '\\\\b', 'i').test(text));\n\nreturn $input.all().map((item, i) => {\n  const b = item.json.body || {};\n  const text = String(b.Body || '').trim();\n  const media = [];\n  for (let n = 0; n < Number(b.NumMedia || 0); n++) if (b['MediaUrl' + n]) media.push(b['MediaUrl' + n]);\n  const lifeSafety = has(text, LIFE_SAFETY);\n  return {\n    json: {\n      phone: String(b.From || ''),\n      text,\n      media,\n      isOptOut: OPT_OUT.includes(text.toUpperCase().replace(/[^A-Z]/g, '')),\n      emergency: lifeSafety ? 'life_safety' : has(text, URGENT) ? 'urgent' : 'none',\n      safetyReply: lifeSafety ? SAFETY_REPLY : null,\n      receivedAt: $now.toISO(),\n    },\n    pairedItem: { item: i },\n  };\n});\n"
      },
      "id": "9b52e61d-8d89-4715-aba4-da9fb101f97c",
      "name": "Normalize and emergency screen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Tenants",
          "mode": "name"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "phone",
              "lookupValue": "={{ $json.phone }}"
            }
          ]
        },
        "options": {}
      },
      "id": "dd48e747-b3d7-4421-94e2-e83e720e8b57",
      "name": "Find tenant by phone",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        440,
        300
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const TRADES = ['plumbing', 'electrical', 'hvac', 'appliance', 'pest', 'locks_doors', 'general'];\nconst nul = (type) => ({ anyOf: [{ type }, { type: 'null' }] });\nconst schema = {\n  type: 'object',\n  additionalProperties: false,\n  required: ['trade', 'priority', 'summary', 'location_in_unit', 'started', 'entry_permission', 'pets', 'missing_details', 'reply_to_tenant', 'language'],\n  properties: {\n    trade: { type: 'string', enum: TRADES },\n    priority: { type: 'string', enum: ['emergency', 'urgent', 'routine'] },\n    summary: { type: 'string', description: 'Two lines for the technician, in English.' },\n    location_in_unit: nul('string'),\n    started: nul('string'),\n    entry_permission: { type: 'string', enum: ['granted', 'denied', 'not_stated'] },\n    pets: { type: 'string', enum: ['yes', 'no', 'not_stated'] },\n    missing_details: { type: 'array', items: { type: 'string' } },\n    reply_to_tenant: { type: 'string', description: 'Short reply in the tenant\\'s language. Confirm the request. If details are missing, ask for the single most useful one (or a photo). No promises about timing or cost.' },\n    language: { type: 'string', description: 'Language the tenant wrote in, e.g. English, Spanish.' },\n  },\n};\n\nreturn $input.all().map((item, i) => {\n  const msg = $('Normalize and emergency screen').itemMatching(i).json;\n  const tenant = item.json.phone ? item.json : null;\n  let route;\n  if (msg.isOptOut) route = 'opt-out';\n  else if (msg.emergency === 'life_safety') route = 'life safety';\n  else if (!tenant) route = 'unknown tenant';\n  else route = 'classify';\n  return {\n    json: {\n      ...msg,\n      route,\n      tenant: tenant ? { name: tenant.tenant_name, property: tenant.property, unit: tenant.unit } : null,\n      replyText: msg.safetyReply,\n      reason: route === 'unknown tenant' ? 'Text from a number not in the Tenants tab' + (msg.emergency === 'urgent' ? ' (URGENT keyword, call now)' : '') : null,\n      claudeRequest: route === 'classify' ? {\n        model: 'claude-opus-5-5',\n        max_tokens: 1500,\n        system: 'You turn tenant maintenance texts into work orders for a property manager. Classify the trade and priority, write a short technician summary, and draft a reply to the tenant in their language. Do not give repair instructions unless the issue is a tripped breaker, a tripped GFCI outlet or a jammed garbage disposal, and always say they can skip it and ask for a technician. Never promise arrival times or costs.',\n        messages: [{ role: 'user', content: 'Property: ' + tenant.property + ', unit ' + tenant.unit + '\\nPhotos attached: ' + msg.media.length + '\\nKeyword screen: ' + msg.emergency + '\\n<tenant_text>\\n' + msg.text + '\\n</tenant_text>' }],\n        output_config: { format: { type: 'json_schema', schema } },\n      } : null,\n    },\n    pairedItem: { item: i },\n  };\n});\n"
      },
      "id": "24b90946-ef5b-465c-a03b-50181390e35d",
      "name": "Route and build prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "33efbbec-3050-4212-a6bd-03c968d13472",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "life safety",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "life safety"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "65133d50-9b63-4a1b-8db2-0d7cc892aa82",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "classify",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "classify"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c908ad08-5685-4c73-849f-f68311ac25d7",
                    "leftValue": "={{ $json.route }}",
                    "rightValue": "unknown tenant",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "unknown tenant"
            }
          ]
        },
        "options": {}
      },
      "id": "cb8985db-5789-4539-b78e-745c10fd0cc4",
      "name": "Route request",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_ONCALL_CHANNEL_ID",
          "mode": "id"
        },
        "text": "=<!channel> :fire: LIFE-SAFETY maintenance text from {{ $json.phone }}\n\"{{ $json.text }}\"\nScripted safety reply sent. Call the tenant now.",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "4d4f1e86-1b27-4e7f-829a-8ada35b7e213",
      "name": "Slack: page on-call",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1100,
        60
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.claudeRequest) }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "62c298bb-6973-4584-95ca-2642a823621c",
      "name": "Claude: classify request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Pull the structured JSON out of a Messages API response.\n// Opus 5.5 always thinks, so a (possibly empty) thinking block can come before the text block.\nfunction parseClaude(res) {\n  if (!res || typeof res !== 'object') return { error: 'Empty response from the Messages API' };\n  if (res.stop_reason === 'refusal') {\n    return { error: 'Model declined the request (refusal' + (res.stop_details?.category ? ': ' + res.stop_details.category : '') + ')' };\n  }\n  if (res.stop_reason === 'max_tokens') return { error: 'Response hit max_tokens before the JSON was complete' };\n  const block = (res.content || []).find((b) => b.type === 'text');\n  if (!block) return { error: 'No text block in the response' };\n  try {\n    return { data: JSON.parse(block.text) };\n  } catch (e) {\n    return { error: 'Could not parse model JSON: ' + e.message };\n  }\n}\n\nreturn $input.all().map((item, i) => {\n  const src = $('Route and build prompt').itemMatching(i).json;\n  const { claudeRequest, ...msg } = src;\n  const { data, error } = parseClaude(item.json.error ? null : item.json);\n  const c = data || { trade: 'general', priority: 'routine', summary: msg.text.slice(0, 200), location_in_unit: null, started: null, entry_permission: 'not_stated', pets: 'not_stated', missing_details: [], reply_to_tenant: 'Thanks, we received your maintenance request and our team will follow up.', language: 'unknown' };\n  // The keyword screen wins over the model: never downgrade an urgent keyword hit.\n  const priority = msg.emergency === 'urgent' && c.priority === 'routine' ? 'urgent' : c.priority;\n  const reviewReasons = [];\n  if (error) reviewReasons.push('Classification failed: ' + (item.json.error?.message || error));\n  if (priority !== 'routine') reviewReasons.push('Priority ' + priority);\n  if (c.entry_permission !== 'granted') reviewReasons.push('No entry permission yet');\n  return {\n    json: { ...msg, classification: c, priority, reviewReasons, replyText: c.reply_to_tenant },\n    pairedItem: { item: i },\n  };\n});\n"
      },
      "id": "474de8eb-7a5c-4c36-8165-03c63f86cdfe",
      "name": "Parse and pick vendor key",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        300
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Vendors",
          "mode": "name"
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "trade",
              "lookupValue": "={{ $json.classification.trade }}"
            },
            {
              "lookupColumn": "property",
              "lookupValue": "={{ $json.tenant.property }}"
            }
          ]
        },
        "combineFilters": "AND",
        "options": {}
      },
      "id": "1da71e1d-1b8b-4efd-84f0-55f19c5d5f5b",
      "name": "Find preferred vendor",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        300
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Work orders",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "received_at": "={{ $('Parse and pick vendor key').item.json.receivedAt }}",
            "status": "={{ $('Parse and pick vendor key').item.json.reviewReasons.length || !$json.vendor_name ? 'Needs review' : 'Ready' }}",
            "review_reasons": "={{ $('Parse and pick vendor key').item.json.reviewReasons.join('; ') + (!$json.vendor_name ? ($('Parse and pick vendor key').item.json.reviewReasons.length ? '; ' : '') + 'No preferred vendor for this trade and property' : '') }}",
            "property": "={{ $('Parse and pick vendor key').item.json.tenant.property }}",
            "unit": "={{ $('Parse and pick vendor key').item.json.tenant.unit }}",
            "tenant_name": "={{ $('Parse and pick vendor key').item.json.tenant.name }}",
            "tenant_phone": "={{ $('Parse and pick vendor key').item.json.phone }}",
            "trade": "={{ $('Parse and pick vendor key').item.json.classification.trade }}",
            "priority": "={{ $('Parse and pick vendor key').item.json.priority }}",
            "summary": "={{ $('Parse and pick vendor key').item.json.classification.summary }}",
            "location_in_unit": "={{ $('Parse and pick vendor key').item.json.classification.location_in_unit }}",
            "entry_permission": "={{ $('Parse and pick vendor key').item.json.classification.entry_permission }}",
            "pets": "={{ $('Parse and pick vendor key').item.json.classification.pets }}",
            "photos": "={{ $('Parse and pick vendor key').item.json.media.join(' ') }}",
            "vendor": "={{ $json.vendor_name }}",
            "nte_amount": "={{ $json.nte_amount }}",
            "tenant_text": "={{ $('Parse and pick vendor key').item.json.text }}",
            "reply_to_tenant": "={{ $('Parse and pick vendor key').item.json.replyText }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "received_at",
              "displayName": "received_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "review_reasons",
              "displayName": "review_reasons",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "property",
              "displayName": "property",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "unit",
              "displayName": "unit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tenant_name",
              "displayName": "tenant_name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tenant_phone",
              "displayName": "tenant_phone",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "trade",
              "displayName": "trade",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "priority",
              "displayName": "priority",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "summary",
              "displayName": "summary",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "location_in_unit",
              "displayName": "location_in_unit",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "entry_permission",
              "displayName": "entry_permission",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "pets",
              "displayName": "pets",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "photos",
              "displayName": "photos",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "vendor",
              "displayName": "vendor",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "nte_amount",
              "displayName": "nte_amount",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tenant_text",
              "displayName": "tenant_text",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "reply_to_tenant",
              "displayName": "reply_to_tenant",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "b2e4a34b-7326-47c3-8760-40285fee3437",
      "name": "Append work order",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1760,
        300
      ]
    },
    {
      "parameters": {
        "resource": "sms",
        "operation": "send",
        "from": "REPLACE_WITH_TWILIO_NUMBER",
        "to": "={{ $('Normalize and emergency screen').item.json.phone }}",
        "toWhatsapp": false,
        "message": "={{ $json.replyText ?? $('Parse and pick vendor key').item.json.replyText }}",
        "options": {}
      },
      "id": "60b25520-7a2b-40ce-bde6-ca2979b6e651",
      "name": "Twilio: reply to tenant",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1980,
        160
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_MAINTENANCE_CHANNEL_ID",
          "mode": "id"
        },
        "text": "=:warning: Maintenance text needs a person\nFrom: {{ $('Normalize and emergency screen').item.json.phone }}\nText: \"{{ $('Normalize and emergency screen').item.json.text }}\"\nWhy: {{ $json.reason ?? ($json.error ? 'Error: ' + ($json.error.message ?? JSON.stringify($json.error)) : 'Unknown') }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "df815184-ca1a-46cd-9c90-b7a092508588",
      "name": "Slack: maintenance exceptions",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1980,
        480
      ]
    }
  ],
  "connections": {
    "Tenant text (Twilio)": {
      "main": [
        [
          {
            "node": "Normalize and emergency screen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize and emergency screen": {
      "main": [
        [
          {
            "node": "Find tenant by phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find tenant by phone": {
      "main": [
        [
          {
            "node": "Route and build prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route and build prompt": {
      "main": [
        [
          {
            "node": "Route request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route request": {
      "main": [
        [
          {
            "node": "Twilio: reply to tenant",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack: page on-call",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Claude: classify request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: maintenance exceptions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: classify request": {
      "main": [
        [
          {
            "node": "Parse and pick vendor key",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Parse and pick vendor key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and pick vendor key": {
      "main": [
        [
          {
            "node": "Find preferred vendor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find preferred vendor": {
      "main": [
        [
          {
            "node": "Append work order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append work order": {
      "main": [
        [
          {
            "node": "Twilio: reply to tenant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Twilio: reply to tenant": {
      "main": [
        [],
        [
          {
            "node": "Slack: maintenance exceptions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
