{
  "name": "Lumeor | PTO requests and approvals",
  "nodes": [
    {
      "parameters": {
        "content": "## PTO requests and approvals (Slack, Claude, BambooHR)\n\n**What it does**\nAn employee types something like \"/timeoff out Thu and Fri next week for a wedding\" in Slack. Claude turns the text into dates, partial days and a leave type. Code then checks the projected BambooHR balance as of the leave date, working hours (skipping weekends and company holidays), notice, blackout dates and how many teammates are already out. The manager gets one Slack message with all of that and Approve or Decline buttons. Approved requests are recorded in BambooHR, and the employee gets a direct message either way.\n\nThis template covers the PTO flow. The timesheet reminders and checks described in the guide reuse the same approval pattern.\n\n**Setup**\n1. Create a Slack app with a slash command /timeoff whose request URL is this workflow's production webhook URL. Add a bot token with chat:write and im:write.\n2. Credentials: Slack, Google Sheets, Anthropic (API key), and HTTP Basic Auth for BambooHR (API key as the user name, any text as the password).\n3. Create a People tab with columns slack_user_id, name, bamboo_employee_id, manager_slack_id, team. Replace REPLACE_WITH_SHEET_ID.\n4. Replace REPLACE_WITH_BAMBOOHR_SUBDOMAIN in the three BambooHR URLs, set TYPE_MAP, notice rules and BLACKOUTS in *Check policy*, and set the time zone in *Prepare request*.\n5. Replace REPLACE_WITH_SLACK_EXCEPTIONS_CHANNEL_ID.\n\n**Notes**\n- Model: claude-opus-5-5 at low effort with structured outputs. claude-haiku-5-5 is a cheaper option for high-volume, simple classification like this parsing step.\n- The model never records medical details. Sick leave reasons are not needed and are not stored.\n\nTemplate by Lumeor Studio, https://www.lumeor.studio/guides/automate-timesheet-and-pto-approvals/",
        "height": 860,
        "width": 560,
        "color": 5
      },
      "id": "1d79288a-ca7c-4f98-b4f8-c598bce96717",
      "name": "About this template",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        0
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "slack-timeoff",
        "responseMode": "onReceived",
        "options": {
          "responseData": "Got it. I am checking your request and will message you shortly."
        }
      },
      "id": "93d627a1-af3c-497f-93f8-a7c61ff66925",
      "name": "Slack /timeoff command",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "webhookId": "c860d533-e3f9-41b6-b08d-14133e65d8d8"
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "REPLACE_WITH_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "People",
          "mode": "name"
        },
        "options": {}
      },
      "id": "8cb428bf-c7a6-4f09-9cae-b454a8496555",
      "name": "Read People sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        220,
        300
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Finds the requester in the People sheet (columns: slack_user_id, name,\n// bamboo_employee_id, manager_slack_id, team) and lists teammates for the\n// coverage check. Throws to the exceptions path if the person is not mapped.\nconst TZ = 'America/New_York';\nconst cmd = $('Slack /timeoff command').first().json.body || {};\nconst people = $input.all().map((i) => i.json);\n\nconst me = people.find((p) => String(p.slack_user_id).trim() === String(cmd.user_id));\nif (!me) throw new Error(`Slack user ${cmd.user_id} (${cmd.user_name}) is not in the People sheet`);\nif (!me.manager_slack_id) throw new Error(`${me.name} has no manager_slack_id in the People sheet`);\nconst text = String(cmd.text || '').trim();\nif (text.length < 5) throw new Error(`${me.name} sent /timeoff with no details`);\n\nconst teammates = people.filter((p) => p.team && p.team === me.team && p.slack_user_id !== me.slack_user_id).map((p) => String(p.bamboo_employee_id));\nreturn [{ json: {\n  requester_slack_id: me.slack_user_id,\n  requester_name: me.name,\n  employee_id: String(me.bamboo_employee_id),\n  manager_slack_id: me.manager_slack_id,\n  team: me.team || '',\n  teammate_ids: teammates,\n  team_size: teammates.length + 1,\n  request_text: text,\n  today: $now.setZone(TZ).toISODate(),\n  weekday: $now.setZone(TZ).toFormat('cccc'),\n  context_line: `Time-off request from ${me.name}: \"${text}\"`,\n} }];\n"
      },
      "id": "ec8eb578-195e-4403-9bb4-301274382df7",
      "name": "Prepare request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ],
      "onError": "continueErrorOutput"
    },
    {
      "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"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={\n  \"model\": \"claude-opus-5-5\",\n  \"max_tokens\": 2000,\n  \"system\": \"You convert an employee's short time-off message into structured dates. Resolve relative dates such as \\\"next Thursday\\\" against today's date and weekday, which you are given. Use partial_days only for days that are not full days off. Pick the closest leave type; do not record or repeat medical or personal details. If the dates are ambiguous, set needs_clarification and ask one short question.\",\n  \"messages\": [\n    {\n      \"role\": \"user\",\n      \"content\": {{ JSON.stringify('Today is ' + $json.weekday + ', ' + $json.today + '.\\nMessage: ' + $json.request_text) }}\n    }\n  ],\n  \"output_config\": {\n    \"effort\": \"low\",\n    \"format\": {\n      \"type\": \"json_schema\",\n      \"schema\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"is_time_off_request\",\n          \"leave_type\",\n          \"start_date\",\n          \"end_date\",\n          \"partial_days\",\n          \"needs_clarification\",\n          \"clarification_question\"\n        ],\n        \"properties\": {\n          \"is_time_off_request\": {\n            \"type\": \"boolean\"\n          },\n          \"leave_type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"vacation\",\n              \"sick\",\n              \"personal\",\n              \"bereavement\",\n              \"jury_duty\",\n              \"other\"\n            ]\n          },\n          \"start_date\": {\n            \"type\": \"string\",\n            \"description\": \"YYYY-MM-DD\"\n          },\n          \"end_date\": {\n            \"type\": \"string\",\n            \"description\": \"YYYY-MM-DD, same as start_date for one day\"\n          },\n          \"partial_days\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": false,\n              \"required\": [\n                \"date\",\n                \"hours\"\n              ],\n              \"properties\": {\n                \"date\": {\n                  \"type\": \"string\",\n                  \"description\": \"YYYY-MM-DD\"\n                },\n                \"hours\": {\n                  \"type\": \"number\",\n                  \"description\": \"Hours off that day, e.g. 4 for a half day\"\n                }\n              }\n            }\n          },\n          \"needs_clarification\": {\n            \"type\": \"boolean\"\n          },\n          \"clarification_question\": {\n            \"type\": \"string\",\n            \"description\": \"Empty string unless clarification is needed.\"\n          }\n        }\n      }\n    }\n  }\n}",
        "options": {
          "timeout": 180000
        }
      },
      "id": "3512780c-6300-4202-8e63-c67d2d06a082",
      "name": "Parse request with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        300
      ],
      "credentials": {
        "anthropicApi": {
          "id": "",
          "name": "Anthropic account"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Validates Claude's reading of the request before any policy check runs.\nconst req = $('Prepare request').first().json;\nconst res = $input.first().json;\nconst fail = (reason) => [{ json: { ...req, ok: false, reason } }];\n\nif (res.stop_reason === 'refusal') return fail('Claude declined to read this request (stop_reason refusal)');\nif (res.stop_reason === 'max_tokens') return fail('Parsing was cut off (stop_reason max_tokens)');\n// Opus 5.5 always thinks, so a thinking block can come first: find the text block.\nconst block = (res.content || []).find((b) => b.type === 'text');\nif (!block) return fail('Claude response has no text block');\nlet d;\ntry { d = JSON.parse(block.text); } catch (e) { return fail('Could not parse model output: ' + e.message); }\n\nconst iso = /^\\d{4}-\\d{2}-\\d{2}$/;\nif (!d.is_time_off_request) return fail('This does not look like a time-off request. Ask the employee to rephrase.');\nif (d.needs_clarification) return fail(`Needs clarification: ${d.clarification_question}`);\nif (!iso.test(d.start_date) || !iso.test(d.end_date) || d.end_date < d.start_date) return fail(`Dates look wrong: ${d.start_date} to ${d.end_date}`);\nif (d.start_date < req.today) return fail(`Start date ${d.start_date} is in the past`);\n\nreturn [{ json: { ...req, ok: true, leave_type: d.leave_type, start_date: d.start_date, end_date: d.end_date, partial_days: d.partial_days } }];\n"
      },
      "id": "cdc51cd7-2e57-46b3-b781-b24896f41385",
      "name": "Parse request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "4a3fef10-75a1-4e3f-928c-edd17dd2e39f",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "c4253724-8d55-401e-981a-b7d707994205",
      "name": "Parsed OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.bamboohr.com/api/gateway.php/REPLACE_WITH_BAMBOOHR_SUBDOMAIN/v1/employees/{{ $json.employee_id }}/time_off/calculator?end={{ $json.start_date }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "68eaf97f-ae75-446a-b4d0-1151097ad53e",
      "name": "Get projected balance",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        200
      ],
      "credentials": {
        "httpBasicAuth": {
          "id": "",
          "name": "BambooHR API key (basic auth)"
        }
      },
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.bamboohr.com/api/gateway.php/REPLACE_WITH_BAMBOOHR_SUBDOMAIN/v1/time_off/whos_out/?start={{ $('Parse request').first().json.start_date }}&end={{ $('Parse request').first().json.end_date }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {}
      },
      "id": "b773350b-6058-4be8-adeb-feece44d888d",
      "name": "Get who's out",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        200
      ],
      "credentials": {
        "httpBasicAuth": {
          "id": "",
          "name": "BambooHR API key (basic auth)"
        }
      },
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Policy checks in code: hours requested (skipping weekends and company\n// holidays), projected balance on the start date, notice, blackout dates and\n// how many teammates are already out. Warnings go to the manager; the manager decides.\nconst HOURS_PER_DAY = 8;\nconst TYPE_MAP = { // leave type from the model -> BambooHR time off type ID\n  vacation: 'REPLACE_WITH_BAMBOO_VACATION_TYPE_ID',\n  personal: 'REPLACE_WITH_BAMBOO_VACATION_TYPE_ID',\n  sick: 'REPLACE_WITH_BAMBOO_SICK_TYPE_ID',\n  bereavement: 'REPLACE_WITH_BAMBOO_BEREAVEMENT_TYPE_ID',\n  jury_duty: 'REPLACE_WITH_BAMBOO_JURY_DUTY_TYPE_ID',\n  other: 'REPLACE_WITH_BAMBOO_VACATION_TYPE_ID',\n};\nconst MIN_NOTICE_DAYS = { short: 3, long: 14 }; // long = 3 or more days off\nconst BLACKOUTS = [ /* { start: '2026-12-28', end: '2026-12-31', label: 'Year-end close' } */ ];\nconst MAX_TEAM_OUT_SHARE = 0.34;\n\nconst req = $('Parse request').first().json;\n// n8n splits JSON arrays from the HTTP Request node into one item per element.\nconst balances = $('Get projected balance').all().map((i) => i.json);\nconst whosOut = $input.all().map((i) => i.json).filter((x) => x && x.type);\n\nconst holidays = new Set();\nfor (const h of whosOut.filter((x) => x.type === 'holiday')) {\n  for (let d = DateTime.fromISO(h.start); d <= DateTime.fromISO(h.end); d = d.plus({ days: 1 })) holidays.add(d.toISODate());\n}\nconst partial = Object.fromEntries((req.partial_days || []).map((p) => [p.date, Number(p.hours)]));\nlet hours = 0, workDays = 0;\nfor (let d = DateTime.fromISO(req.start_date); d <= DateTime.fromISO(req.end_date); d = d.plus({ days: 1 })) {\n  if (d.weekday > 5 || holidays.has(d.toISODate())) continue;\n  workDays++;\n  hours += partial[d.toISODate()] ?? HOURS_PER_DAY;\n}\n\nconst typeId = TYPE_MAP[req.leave_type] || TYPE_MAP.other;\nconst bal = balances.find((b) => b && String(b.timeOffType) === String(typeId));\nconst balanceHours = bal ? Number(bal.balance) : null;\n\nconst warnings = [];\nif (hours === 0) warnings.push('No working hours in this range (weekend or holiday only).');\nif (balanceHours === null) warnings.push('Could not find a projected balance for this leave type.');\nelse if (balanceHours - hours < 0) warnings.push(`Balance would go negative: ${balanceHours} h available on ${req.start_date}, ${hours} h requested.`);\nconst noticeDays = Math.floor(DateTime.fromISO(req.start_date).diff(DateTime.fromISO(req.today), 'days').days);\nconst needed = workDays >= 3 ? MIN_NOTICE_DAYS.long : MIN_NOTICE_DAYS.short;\nif (req.leave_type !== 'sick' && req.leave_type !== 'bereavement' && noticeDays < needed) warnings.push(`Short notice: ${noticeDays} days, policy asks for ${needed}.`);\nfor (const b of BLACKOUTS) if (req.start_date <= b.end && req.end_date >= b.start) warnings.push(`Overlaps blackout period: ${b.label}.`);\nconst teamOut = whosOut.filter((x) => x.type === 'timeOff' && req.teammate_ids.includes(String(x.employeeId)));\nconst outNames = [...new Set(teamOut.map((x) => x.name))];\nif (req.team_size > 1 && (outNames.length + 1) / req.team_size > MAX_TEAM_OUT_SHARE) warnings.push(`Coverage: ${outNames.length} of ${req.team_size - 1} teammates are also out (${outNames.join(', ')}).`);\n\nconst fmt = (iso) => DateTime.fromISO(iso).toFormat('ccc LLL d');\nconst approval_text = [\n  `*Time-off request from ${req.requester_name}*`,\n  `${req.leave_type.replace('_', ' ')}: ${fmt(req.start_date)} to ${fmt(req.end_date)}, ${hours} hours over ${workDays} working day(s)`,\n  `Their words: \"${req.request_text}\"`,\n  `Projected balance on ${fmt(req.start_date)}: ${balanceHours ?? 'unknown'} h, after approval: ${balanceHours === null ? 'unknown' : balanceHours - hours} h`,\n  `Teammates out in this period: ${outNames.length ? outNames.join(', ') : 'none'}`,\n  warnings.length ? `\\n:warning: ${warnings.join('\\n:warning: ')}` : '\\nNo policy warnings.',\n].join('\\n');\n\nreturn [{ json: { ...req, hours, work_days: workDays, time_off_type_id: typeId, balance_hours: balanceHours, warnings, approval_text } }];\n"
      },
      "id": "2db20af1-5b10-4cdd-ab0d-0f70daf1ec68",
      "name": "Check policy",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        200
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "sendAndWait",
        "select": "user",
        "user": {
          "__rl": true,
          "value": "={{ $json.manager_slack_id }}",
          "mode": "id"
        },
        "message": "={{ $json.approval_text }}",
        "responseType": "approval",
        "approvalOptions": {
          "values": {
            "approvalType": "double",
            "approveLabel": "Approve",
            "disapproveLabel": "Decline"
          }
        },
        "options": {
          "limitWaitTime": {
            "values": {
              "limitType": "afterTimeInterval",
              "resumeAmount": 3,
              "resumeUnit": "days"
            }
          }
        }
      },
      "id": "4177bf73-4e50-4039-881f-725c7809b65c",
      "name": "Manager approval in Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1980,
        200
      ],
      "webhookId": "415df4ce-34ba-4d14-b1c9-9e2620601f28",
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "11460056-0672-48d9-a696-93d288d09e21",
              "leftValue": "={{ !!($json.data && $json.data.approved === true) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "0e8ec8ef-12a6-44cf-b2e8-0610a8be7079",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2200,
        200
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.bamboohr.com/api/gateway.php/REPLACE_WITH_BAMBOOHR_SUBDOMAIN/v1/employees/{{ $('Check policy').first().json.employee_id }}/time_off/request",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ status: 'approved', start: $('Check policy').first().json.start_date, end: $('Check policy').first().json.end_date, timeOffTypeId: $('Check policy').first().json.time_off_type_id, amount: $('Check policy').first().json.hours, notes: [{ from: 'manager', note: 'Approved in Slack. Request: ' + $('Check policy').first().json.request_text }] }) }}",
        "options": {}
      },
      "id": "dd2330b7-bc43-4c61-8fbb-eb1d100ae384",
      "name": "Record approved time off",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2420,
        100
      ],
      "credentials": {
        "httpBasicAuth": {
          "id": "",
          "name": "BambooHR API key (basic auth)"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "user",
        "user": {
          "__rl": true,
          "value": "={{ $('Check policy').first().json.requester_slack_id }}",
          "mode": "id"
        },
        "messageType": "text",
        "text": "={{ $('Manager approval in Slack').first().json.data && $('Manager approval in Slack').first().json.data.approved === true ? ':white_check_mark: Your time off for ' + $('Check policy').first().json.start_date + ' to ' + $('Check policy').first().json.end_date + ' (' + $('Check policy').first().json.hours + ' h) was approved and recorded.' : ($('Manager approval in Slack').first().json.data ? ':x: Your time-off request for ' + $('Check policy').first().json.start_date + ' to ' + $('Check policy').first().json.end_date + ' was declined. Please talk to your manager.' : ':hourglass: Your manager has not answered your request for ' + $('Check policy').first().json.start_date + ' yet. Please follow up with them directly.') }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "7bede850-e136-4cce-b4c1-a5c8e157cbe8",
      "name": "Tell the employee",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2640,
        200
      ],
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_SLACK_EXCEPTIONS_CHANNEL_ID",
          "mode": "id"
        },
        "messageType": "text",
        "text": "=:warning: *PTO request* needs attention\n{{ $json.reason || ($json.error ? ($json.error.message || JSON.stringify($json.error)) : 'Unknown error') }}\n{{ $json.context_line || '' }}\nExecution: {{ $execution.id }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "1a25bfe7-44f3-49a0-b1de-f3e029c5860d",
      "name": "Post exception to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1320,
        540
      ],
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack account"
        }
      }
    }
  ],
  "connections": {
    "Slack /timeoff command": {
      "main": [
        [
          {
            "node": "Read People sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read People sheet": {
      "main": [
        [
          {
            "node": "Prepare request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare request": {
      "main": [
        [
          {
            "node": "Parse request with Claude",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse request with Claude": {
      "main": [
        [
          {
            "node": "Parse request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse request": {
      "main": [
        [
          {
            "node": "Parsed OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parsed OK?": {
      "main": [
        [
          {
            "node": "Get projected balance",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get projected balance": {
      "main": [
        [
          {
            "node": "Get who's out",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get who's out": {
      "main": [
        [
          {
            "node": "Check policy",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check policy": {
      "main": [
        [
          {
            "node": "Manager approval in Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manager approval in Slack": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Record approved time off",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the employee",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record approved time off": {
      "main": [
        [
          {
            "node": "Tell the employee",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post exception to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
