{"id":21453,"date":"2026-10-05T17:09:12","date_gmt":"2026-10-05T15:09:12","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?page_id=21453"},"modified":"2026-10-05T17:09:12","modified_gmt":"2026-10-05T15:09:12","slug":"saas-support-capacity-planner","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/","title":{"rendered":"SaaS Support Capacity Planner"},"content":{"rendered":"<style>\n.lw-saas{color:var(--text-primary);min-width:0}\n.lw-saas *{box-sizing:border-box}\n.lw-saas h2,.lw-saas h3,.profile-panel:has(.lw-saas) .profile-name{overflow-wrap:anywhere;hyphens:auto}\n.lw-saas .saas-note{padding:16px;border-left:3px solid var(--accent);background:var(--bg-panel);color:var(--text-secondary)}\n.lw-saas fieldset{border:1px solid var(--border);border-radius:10px;padding:18px;margin:20px 0;min-width:0;background:var(--bg-panel)}\n.lw-saas legend{padding:0 8px;font-weight:700;color:var(--text-primary)}\n.lw-saas .saas-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}\n.lw-saas label{display:block;font-size:.9rem;color:var(--text-secondary)}\n.lw-saas input{display:block;width:100%;padding:11px;margin-top:6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-body);color:var(--text-primary);font:inherit}\n.lw-saas input:focus-visible,.lw-saas button:focus-visible,.lw-saas a:focus-visible{outline:3px solid var(--accent);outline-offset:3px}\n.lw-saas input[aria-invalid=true]{border:2px solid var(--accent)}\n.lw-saas .saas-actions{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0}\n.lw-saas button:disabled{opacity:.45;cursor:default;transform:none}\n.lw-saas [data-status]{color:var(--text-primary);font-weight:600}\n.lw-saas .saas-metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}\n.lw-saas .saas-metrics div{padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--bg-panel)}\n.lw-saas dt{color:var(--text-secondary);font-size:.85rem}\n.lw-saas dd{margin:8px 0 0;font-size:1.35rem;font-weight:700;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}\n.lw-saas .saas-table-wrap{max-width:100%;overflow-x:auto;margin:20px 0;border:1px solid var(--border);border-radius:8px}\n.lw-saas table{border-collapse:collapse;width:100%;font-size:.86rem}\n.lw-saas table th,.lw-saas table td{width:auto!important;padding:12px;min-width:120px;word-break:normal;overflow-wrap:normal;text-align:right;border-bottom:1px solid var(--border);color:var(--text-primary);white-space:normal}\n.lw-saas table th{background:var(--bg-panel)}\n.lw-saas table th[scope=row],.lw-saas table thead th:first-child{position:sticky;left:0;z-index:1;border-right:1px solid var(--border)}\n.lw-saas caption{text-align:left;padding:12px;font-weight:700;color:var(--text-primary)}\n.lw-saas .saas-chart{margin:20px 0;color:var(--accent)}\n.lw-saas .saas-chart svg{display:block;width:100%;height:auto}\n.lw-saas figcaption{color:var(--text-secondary);font-size:.85rem}\n.lw-saas .saas-related{display:flex;gap:10px;flex-wrap:wrap;list-style:none;padding:0}\n.lw-saas .saas-related li{list-style:none;margin:0}\n.lw-saas .saas-related a{display:block;border:1px solid var(--border);border-radius:6px;padding:8px 12px}\n.site-container main:has(.lw-saas) .profile-panel{flex:1 1 0;max-width:none;min-width:0}\n.site-container main:has(.lw-saas) .info-panel{flex:0 0 220px;max-width:220px}\n@media(max-width:900px){.site-container main:has(.lw-saas) .profile-panel,.site-container main:has(.lw-saas) .info-panel{flex:auto;width:100%;max-width:100%}}\n@media(max-width:540px){.lw-saas .saas-fields,.lw-saas .saas-metrics{grid-template-columns:1fr}.lw-saas fieldset{padding:12px}}\n<\/style>\n<p><script>(function(scope){'use strict';\nconst number=raw=>{const s=String(raw??'').trim().replace(\/\\s\/g,'').replace(',','.');return \/^(?:\\d+(?:\\.\\d*)?|\\.\\d+)$\/.test(s)&&Number.isFinite(Number(s))?Number(s):NaN;};\nfunction acquisition(v){const leads=v.visits*v.signup\/100,activated=leads*v.activation\/100,payers=activated*v.paid\/100,cost=v.spend+v.hours*v.hour,margin=v.price-v.variable,rate=v.signup\/100*v.activation\/100*v.paid\/100,cac=payers>0?cost\/payers:null;\nreturn {summary:{leads,activated,payers,acquisitionCost:cost,cac,payback:cac!==null&&margin>0?cac\/margin:null,requiredVisits:v.target===0?0:rate>0?Math.ceil(v.target\/rate):null},columns:['paidRate','payers','cac','payback','contribution'],rows:[0.5,1,1.5].map(x=>{const pct=Math.min(100,v.paid*x),n=activated*pct\/100,c=n>0?cost\/n:null;return [pct,n,c,c!==null&&margin>0?c\/margin:null,n*margin];})};}\nfunction cashflow(v){let monthly=v.customers,cash=v.cash,first=null;const cohorts=[],rows=[],annualPrice=v.price*12*(1-v.discount\/100);let minimum=v.cash;\nfor(let month=1;month<=v.months;month++){monthly=monthly*(1-v.churn\/100)+v.newCustomers*(1-v.annualShare\/100);let renewed=0;\nfor(const cohort of cohorts){if((month-cohort.start)%12===0){cohort.n*=v.renewal\/100;renewed+=cohort.n;}}\nconst fresh=v.newCustomers*v.annualShare\/100;cohorts.push({start:month,n:fresh});const annual=cohorts.reduce((sum,c)=>sum+c.n,0),annualBills=renewed+fresh;\nconst receipts=monthly*v.price+annualBills*annualPrice,refunds=receipts*v.refunds\/100,fees=receipts*v.fee\/100+(monthly+annualBills)*v.flat;\nconst outflow=fees+(monthly+annual)*v.variable+v.fixed+(month===1?v.setup:0),net=receipts-refunds-outflow;cash+=net;minimum=Math.min(minimum,cash);if(cash<0&#038;&#038;first===null)first=month;\nrows.push([month,monthly,annual,receipts,refunds,outflow,net,cash,monthly*v.price+annual*annualPrice\/12]);}\nreturn {summary:{finalCash:cash,minimumCash:Math.min(...rows.map(r=>r[7])),firstShortfall:first===null?'none':first,finalMRR:rows.at(-1)[8]},columns:['month','monthlyCustomers','annualCustomers','receipts','refunds','outflow','netCash','closingCash','mrr'],rows};}\nfunction aibudget(v){const attempt=(v.inputTokens*((1-v.cached\/100)*v.inputRate+v.cached\/100*v.cacheRate)+v.outputTokens*v.outputRate)\/1e6+v.overhead,request=attempt*(1+v.retries\/100),usable=v.budget*(1-v.reserve\/100);\nreturn {summary:{attemptCost:attempt,requestCost:request,usableBudget:usable,safeRequests:request>0?Math.floor(usable\/request):'unbounded'},columns:['scenario','requests','attemptsTotal','perCustomer','totalCost','remaining'],rows:[1,2,v.spike].map(x=>{const n=v.requests*x,cost=n*request;return [x+'\u00d7',n,n*v.customers*(1+v.retries\/100),cost,cost*v.customers,usable-cost];})};}\nfunction support(v){const perPerson=v.hours*v.utilization\/100;\nconst calc=rate=>{const tickets=v.customers*v.tickets*(1-rate\/100),hours=tickets*v.minutes\/60*(1+v.reopen\/100),staff=Math.ceil(hours\/perPerson);return {tickets,hours,staff};};const main=calc(v.selfserve),capacity=v.staff*perPerson,cost=v.staff*v.hours*v.hour+v.fixed;\nreturn {summary:{tickets:main.tickets,requiredHours:main.hours,capacity,neededStaff:main.staff,spareHours:capacity-main.hours,teamCost:cost,perCustomerSupport:v.customers>0?cost\/v.customers:null},columns:['avoidRate','tickets','requiredHours','neededStaff','requiredStaffCost'],rows:[0,v.selfserve,Math.min(100,v.selfserve+20)].map(rate=>{const r=calc(rate);return [rate,r.tickets,r.hours,r.staff,r.staff*v.hours*v.hour+v.fixed];})};}\nconst api={number,acquisition,cashflow,aibudget,support};if(typeof module!=='undefined'&&module.exports)module.exports=api;else scope.LWSaasMath=api;\n})(typeof window!=='undefined'?window:globalThis);<\/script><\/p>\n<div class=\"lw-saas\" id=\"saas-support\">\n<p>Plan ticket volume, handling time, available capacity and support cost per customer.<\/p>\n<p class=\"saas-note\">All monetary amounts use the same currency and exclude VAT. Example values are assumptions, not provider prices. Inputs and calculations stay in the browser.<\/p>\n<p><a href=\"\/blog\/en\/toolbox\/#saas\">All SaaS tools<\/a><\/p>\n<form novalidate>\n<fieldset>\n<legend>Shared assumptions<\/legend>\n<div class=\"saas-fields\">\n<label for=\"saas-support-customers\">Active customers<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-customers\" name=\"customers\" value=\"500\"><\/label><br \/>\n<label for=\"saas-support-tickets\">Tickets per customer \/ month<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-tickets\" name=\"tickets\" value=\"0.4\"><\/label><br \/>\n<label for=\"saas-support-minutes\">Handling minutes per ticket<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-minutes\" name=\"minutes\" value=\"15\"><\/label><br \/>\n<label for=\"saas-support-reopen\">Additional workload for follow-ups (%)<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-reopen\" name=\"reopen\" value=\"10\"><\/label><br \/>\n<label for=\"saas-support-selfserve\">Tickets avoided through self-service (%)<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-selfserve\" name=\"selfserve\" value=\"20\"><\/label><br \/>\n<label for=\"saas-support-hours\">Available work hours per person \/ month<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-hours\" name=\"hours\" value=\"80\"><\/label><br \/>\n<label for=\"saas-support-utilization\">Share available for ticket handling (%)<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-utilization\" name=\"utilization\" value=\"70\"><\/label><br \/>\n<label for=\"saas-support-staff\">Planned staff<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-staff\" name=\"staff\" value=\"1\"><\/label><br \/>\n<label for=\"saas-support-hour\">Staff cost per paid work hour<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-hour\" name=\"hour\" value=\"30\"><\/label><br \/>\n<label for=\"saas-support-fixed\">Monthly support software cost<input type=\"text\" inputmode=\"decimal\" autocomplete=\"off\" data-rc-private data-mp-mask id=\"saas-support-fixed\" name=\"fixed\" value=\"50\"><\/label>\n<\/div>\n<\/fieldset>\n<div class=\"saas-actions\"><button type=\"submit\">Calculate<\/button><button type=\"button\" data-reset>Example values<\/button><\/div>\n<\/form>\n<p role=\"alert\" data-status id=\"saas-support-status\">\n<div data-result data-rc-private data-mp-mask aria-live=\"polite\" aria-atomic=\"false\"><\/div>\n<p><button type=\"button\" data-csv disabled>Download CSV<\/button><\/p>\n<h2>Calculation model and assumptions<\/h2>\n<p>Tickets = customers \u00d7 tickets per customer \u00d7 (1 \u2212 self-service share). Required handling hours = tickets \u00d7 minutes \/ 60 \u00d7 (1 + follow-up overhead). Productive monthly hours per person = available work hours \u00d7 planned handling share. Required staff is rounded up to whole people. Current team cost = staff \u00d7 available paid work hours \u00d7 hourly cost + software cost. The table changes the self-service share at constant customer volume. Spare handling hours do not guarantee response times; arrival peaks and queues are excluded from the average model.<\/p>\n<h2>More SaaS tools<\/h2>\n<ul class=\"saas-related\">\n<li><a href=\"\/blog\/en\/toolbox\/saas-profitability-calculator\/\">SaaS Profitability Calculator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/saas-pricing-model-simulator\/\">SaaS Pricing Model Simulator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/automation-cost-comparator\/\">Automation Cost Comparator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/mrr-churn-simulator\/\">MRR and Churn Simulator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/digital-product-profit-calculator\/\">Digital Product Profit Calculator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/saas-customer-acquisition-calculator\/\">SaaS Customer Acquisition Calculator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/saas-cashflow-runway-simulator\/\">SaaS Cash Flow and Runway Simulator<\/a><\/li>\n<li><a href=\"\/blog\/en\/toolbox\/ai-usage-budget-calculator\/\">AI Usage Budget Calculator<\/a><\/li>\n<\/ul>\n<h2>Sources and definitions<\/h2>\n<ul>\n<li><a href=\"https:\/\/docs.stripe.com\/customer-management\" rel=\"noopener\">Stripe: customer portal<\/a><\/li>\n<li><a href=\"https:\/\/www.ycombinator.com\/blog\/balancing-b2b-sales\/\" rel=\"noopener\">Y Combinator: balancing B2B sales<\/a><\/li>\n<\/ul>\n<div class=\"lw-faq\">\n<h2>FAQ<\/h2>\n<h3>Are inputs transmitted?<\/h3>\n<p>The calculator processes inputs locally in the browser. CSV files are also generated locally. The page does not store scenario inputs on the server.<\/p>\n<h3>What are the model limitations?<\/h3>\n<p>Self-service represents assumed avoidance, not measured effectiveness. Staff cost includes planned time outside ticket handling. Setup and documentation maintenance are additional costs. An SLA also requires planning around business hours and peak load.<\/p>\n<\/div>\n<p><script type=\"application\/json\" data-config>{\"kind\": \"support\", \"slug\": \"saas-support-capacity-planner\", \"title\": \"SaaS Support Capacity Planner\", \"locale\": \"en-GB\", \"fields\": [{\"key\": \"customers\", \"value\": 500, \"label\": \"Active customers\", \"min\": 0, \"max\": 1000000000.0, \"integer\": true, \"group\": \"common\"}, {\"key\": \"tickets\", \"value\": 0.4, \"label\": \"Tickets per customer \/ month\", \"min\": 0, \"max\": 1000000000.0, \"integer\": false, \"group\": \"common\"}, {\"key\": \"minutes\", \"value\": 15, \"label\": \"Handling minutes per ticket\", \"min\": 0, \"max\": 1000000000.0, \"integer\": false, \"group\": \"common\"}, {\"key\": \"reopen\", \"value\": 10, \"label\": \"Additional workload for follow-ups (%)\", \"min\": 0, \"max\": 1000, \"integer\": false, \"group\": \"common\"}, {\"key\": \"selfserve\", \"value\": 20, \"label\": \"Tickets avoided through self-service (%)\", \"min\": 0, \"max\": 100, \"integer\": false, \"group\": \"common\"}, {\"key\": \"hours\", \"value\": 80, \"label\": \"Available work hours per person \/ month\", \"min\": 0.01, \"max\": 1000000.0, \"integer\": false, \"group\": \"common\"}, {\"key\": \"utilization\", \"value\": 70, \"label\": \"Share available for ticket handling (%)\", \"min\": 0.01, \"max\": 100, \"integer\": false, \"group\": \"common\"}, {\"key\": \"staff\", \"value\": 1, \"label\": \"Planned staff\", \"min\": 0, \"max\": 10000, \"integer\": true, \"group\": \"common\"}, {\"key\": \"hour\", \"value\": 30, \"label\": \"Staff cost per paid work hour\", \"min\": 0, \"max\": 1000000000.0, \"integer\": false, \"group\": \"common\"}, {\"key\": \"fixed\", \"value\": 50, \"label\": \"Monthly support software cost\", \"min\": 0, \"max\": 1000000000.0, \"integer\": false, \"group\": \"common\"}], \"labels\": {\"revenue\": \"Monthly revenue\", \"cost\": \"Monthly cost\", \"profit\": \"Monthly profit\", \"margin\": \"Contribution margin per customer \/ sale\", \"breakEven\": \"Break-even: customers \/ sales\", \"minimumPrice\": \"Cost-covering net price\", \"scenario\": \"Volume\", \"customers\": \"Customers\", \"sales\": \"Sales\", \"attempts\": \"Attempts per month\", \"outcomes\": \"Expected successful outcomes\", \"packs\": \"Credit packs per month\", \"model\": \"Pricing model\", \"fees\": \"Payment fees\", \"operatingCost\": \"Attempt and fixed costs\", \"subscription\": \"Subscription\", \"credits\": \"Credit packs\", \"outcome\": \"Outcome pricing\", \"platform\": \"Platform\", \"billableUnits\": \"Billing units\", \"fixedPlanHosting\": \"Plan and hosting\", \"overage\": \"Overage cost\", \"maintenance\": \"Maintenance cost\", \"setupMonthly\": \"Setup per month\", \"apiCost\": \"External API cost\", \"total\": \"Total cost per month\", \"perRun\": \"Cost per original run\", \"n8n\": \"n8n\", \"make\": \"Make\", \"code\": \"Custom code\", \"initialMRR\": \"Initial MRR\", \"finalMRR\": \"Final MRR\", \"annualRunRate\": \"Final ARR (MRR \u00d7 12)\", \"finalCustomers\": \"Expected final customers\", \"month\": \"Month\", \"openingMRR\": \"Opening MRR\", \"lostMRR\": \"Churned MRR\", \"newMRR\": \"New MRR\", \"expansionMRR\": \"Expansion MRR\", \"contractionMRR\": \"Contraction MRR\", \"closingMRR\": \"Closing MRR\", \"customersExpected\": \"Expected customers\", \"nrr\": \"NRR excluding new customers\", \"grossRevenue\": \"Revenue before refunds\", \"refundValue\": \"Refunds\", \"retainedRevenue\": \"Retained revenue\", \"leads\": \"Signups \/ demo requests\", \"activated\": \"Activated leads\", \"payers\": \"Expected new payers\", \"acquisitionCost\": \"Acquisition spending\", \"cac\": \"Cost per new customer (CAC)\", \"payback\": \"Payback in months\", \"requiredVisits\": \"Visits required for target\", \"paidRate\": \"Payment rate (%)\", \"contribution\": \"Monthly contribution from new customers\", \"finalCash\": \"Final cash balance\", \"minimumCash\": \"Lowest month-end cash balance\", \"firstShortfall\": \"First month with negative closing balance\", \"none\": \"No negative month-end balance\", \"monthlyCustomers\": \"Monthly customers\", \"annualCustomers\": \"Annual customers\", \"receipts\": \"Cash receipts\", \"refunds\": \"Refunds\", \"outflow\": \"Fees and operating spending\", \"netCash\": \"Net cash flow\", \"closingCash\": \"Closing cash balance\", \"mrr\": \"Active contract MRR\", \"attemptCost\": \"Cost per attempt\", \"requestCost\": \"Cost per original request\", \"usableBudget\": \"Usable budget per customer\", \"safeRequests\": \"Cost limit: original requests \/ customer\", \"unbounded\": \"No finite cost limit\", \"requests\": \"Requests per customer\", \"attemptsTotal\": \"Total attempts\", \"perCustomer\": \"AI cost per customer\", \"totalCost\": \"Total AI cost\", \"remaining\": \"Remaining usable budget per customer\", \"tickets\": \"Remaining tickets\", \"requiredHours\": \"Required handling hours\", \"capacity\": \"Available handling hours\", \"neededStaff\": \"Required staff\", \"spareHours\": \"Spare hours \/ negative shortfall\", \"teamCost\": \"Monthly cost of planned team\", \"perCustomerSupport\": \"Team cost per customer\", \"avoidRate\": \"Self-service share (%)\", \"requiredStaffCost\": \"Cost of required team\"}, \"ui\": {\"calculate\": \"Calculate\", \"reset\": \"Example values\", \"csv\": \"Download CSV\", \"results\": \"Result\", \"table\": \"Scenarios and calculation\", \"unavailable\": \"Not calculable\", \"invalid\": \"Invalid value. Allowed range for\", \"integer\": \"(whole number)\", \"shares\": \"The three customer shares must total 100%.\", \"overflow\": \"The inputs produce excessively large values.\", \"changed\": \"Inputs changed. A new calculation updates the result.\", \"undefinedHelp\": \"Self-service represents assumed avoidance, not measured effectiveness. Staff cost includes planned time outside ticket handling. Setup and documentation maintenance are additional costs. An SLA also requires planning around business hours and peak load.\", \"title\": \"Tool\", \"assumptions\": \"Assumptions\", \"chart\": \"MRR trend; exact values in the table\", \"pricingChart\": \"Monthly profit per pricing model; exact values in the table\", \"method\": \"Calculation model and assumptions\", \"sources\": \"Sources and definitions\", \"faq1\": \"Are inputs transmitted?\", \"faq1a\": \"The calculator processes inputs locally in the browser. CSV files are also generated locally. The page does not store scenario inputs on the server.\", \"faq2\": \"What are the model limitations?\", \"related\": \"More SaaS tools\", \"all\": \"All SaaS tools\"}}<\/script><\/div>\n<p><script>(function(){\n'use strict';\nconst root=document.currentScript.previousElementSibling;\nconst c=JSON.parse(root.querySelector('[data-config]').textContent);\nif(root.dataset.ready)return;root.dataset.ready='1';\nconst form=root.querySelector('form'), result=root.querySelector('[data-result]'), status=root.querySelector('[data-status]'), download=root.querySelector('[data-csv]');\nlet exportRows=null;\nconst fmt=new Intl.NumberFormat(c.locale,{maximumFractionDigits:2});\nconst countFmt=new Intl.NumberFormat(c.locale,{maximumFractionDigits:2});\nconst preciseFmt=new Intl.NumberFormat(c.locale,{maximumFractionDigits:6});\nconst safeMinimum=value=>Math.ceil(value*100)\/100;\nconst e=(tag,text)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;return n;};\nconst label=k=>c.labels[k]||k;\nfunction display(value,key){\n if(value===null)return c.ui.unavailable;\n if(typeof value==='string')return label(value);\n if(key==='minimumPrice')value=safeMinimum(value);\n if(['attemptCost','requestCost','perRun','margin'].includes(key))return preciseFmt.format(value);\n return (['customers','sales','attempts','outcomes','packs','breakEven','finalCustomers','customersExpected','billableUnits','month'].includes(key)?countFmt:fmt).format(value)+(key==='nrr'?' %':'');\n}\nfunction calculate(){\n status.textContent='';result.replaceChildren();download.disabled=true;exportRows=null;\n root.querySelectorAll('[aria-invalid]').forEach(n=>{n.removeAttribute('aria-invalid');n.removeAttribute('aria-describedby');});\n const v={};let bad=null;\n for(const f of c.fields){\n  const input=form.elements.namedItem(f.key), n=LWSaasMath.number(input.value);\n  if(!Number.isFinite(n)||n<f.min||n>f.max||(f.integer&&!Number.isInteger(n))){\n   input.setAttribute('aria-invalid','true');input.setAttribute('aria-describedby',status.id);\n   if(!bad)bad={input,field:f};\n  }v[f.key]=n;\n }\n if(bad){\n  status.textContent=c.ui.invalid+' '+bad.field.label+': '+bad.field.min+'\u2013'+bad.field.max+(bad.field.integer?' '+c.ui.integer:'');\n  bad.input.focus();return;\n }\n let data;\n try{data=LWSaasMath[c.kind](v);}catch(error){status.textContent=c.ui.shares;return;}\n const values=[...Object.values(data.summary),...data.rows.flat()];\n if(values.some(n=>typeof n==='number'&&!Number.isFinite(n))){status.textContent=c.ui.overflow;return;}\n const h=e('h2',c.ui.results);result.append(h);\n const cards=e('dl');cards.className='saas-metrics';\n for(const [key,value]of Object.entries(data.summary)){\n  const box=e('div');box.append(e('dt',label(key)),e('dd',display(value,key)));cards.append(box);\n }result.append(cards);\n if(c.kind==='mrr'||c.kind==='pricing'){\n  const index=c.kind==='mrr'?6:4;\n  const points=data.rows.map(row=>row[index]);\n  const chart=e('figure');chart.className='saas-chart';\n  const svg=document.createElementNS('http:\/\/www.w3.org\/2000\/svg','svg');\n  svg.setAttribute('viewBox','0 0 600 160');svg.setAttribute('role','img');svg.setAttribute('aria-label',c.ui.chart);\n  const max=Math.max(1,...points.map(Math.abs));\n  if(c.kind==='mrr'){\n   const path=document.createElementNS(svg.namespaceURI,'polyline');\n   path.setAttribute('points',points.map((n,i)=>(20+i*560\/Math.max(1,points.length-1))+','+(140-n\/max*120)).join(' '));\n   path.setAttribute('fill','none');path.setAttribute('stroke','currentColor');path.setAttribute('stroke-width','3');svg.append(path);\n  }else{\n   for(let i=0;i<points.length;i++){\n    const rect=document.createElementNS(svg.namespaceURI,'rect');\n    rect.setAttribute('x',30+i*195);rect.setAttribute('width','140');\n    rect.setAttribute('y',points[i]>=0?80-points[i]\/max*60:80);\n    rect.setAttribute('height',Math.abs(points[i])\/max*60);rect.setAttribute('fill','currentColor');\n    const title=document.createElementNS(svg.namespaceURI,'title');title.textContent=label(data.rows[i][0])+': '+display(points[i],'profit');rect.append(title);svg.append(rect);\n   }\n   const axis=document.createElementNS(svg.namespaceURI,'path');axis.setAttribute('d','M20 80H580');axis.setAttribute('stroke','currentColor');svg.append(axis);\n  }\n  chart.append(svg,e('figcaption',c.ui.chart));result.append(chart);\n }\n const scroll=e('div');scroll.className='saas-table-wrap';scroll.tabIndex=0;\n scroll.setAttribute('role','region');scroll.setAttribute('aria-label',c.ui.table);\n const table=e('table');table.append(e('caption',c.ui.table));\n const head=e('thead'), hr=e('tr');\n data.columns.forEach(k=>{const th=e('th',label(k));th.scope='col';hr.append(th);});head.append(hr);table.append(head);\n const body=e('tbody');\n data.rows.forEach(row=>{const tr=e('tr');row.forEach((value,i)=>{const cell=e(i===0?'th':'td',display(value,data.columns[i]));if(i===0)cell.scope='row';tr.append(cell);});body.append(tr);});\n table.append(body);scroll.append(table);result.append(scroll);\n if(Object.values(data.summary).includes(null)||data.rows.flat().includes(null))result.append(e('p',c.ui.undefinedHelp));\n exportRows=[[c.ui.title,c.title],[c.ui.assumptions]];\n for(const f of c.fields)exportRows.push([f.label,v[f.key]]);\n exportRows.push([],['',c.ui.results]);\n for(const [key,value]of Object.entries(data.summary))exportRows.push([label(key),value===null?c.ui.unavailable:key==='minimumPrice'?safeMinimum(value):value]);\n exportRows.push([],data.columns.map(label),...data.rows.map(row=>row.map(x=>typeof x==='string'?label(x):x===null?c.ui.unavailable:x)));\n download.disabled=false;\n root.dispatchEvent(new CustomEvent('saas-calculated',{detail:data}));\n}\nform.addEventListener('submit',event=>{event.preventDefault();calculate();});\nform.addEventListener('input',()=>{exportRows=null;download.disabled=true;status.textContent=c.ui.changed;result.replaceChildren();});\nroot.querySelector('[data-reset]').addEventListener('click',()=>{form.reset();calculate();});\ndownload.addEventListener('click',()=>{\n if(!exportRows)return;\n const csv='\\uFEFF'+exportRows.map(row=>row.map(x=>'\"'+String(x).replace(\/\"\/g,'\"\"')+'\"').join(';')).join('\\r\\n');\n const url=URL.createObjectURL(new Blob([csv],{type:'text\/csv;charset=utf-8'}));\n const a=e('a');a.href=url;a.download=c.slug+'.csv';root.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);\n});\nroot._saasCalculate=calculate;\ncalculate();\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Plan ticket volume, handling time, available capacity and support cost per customer.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":9,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[93051],"class_list":["post-21453","page","type-page","status-publish","hentry","tag-saas-tools-en"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>SaaS Support Capacity Planner | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Plan ticket volume, handling time, available capacity and support cost per customer.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"SaaS Support Capacity Planner | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Plan ticket volume, handling time, available capacity and support cost per customer.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/saas-support-capacity-planner\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/saas-support-capacity-planner\\\/\",\"name\":\"SaaS Support Capacity Planner | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-10-05T15:09:12+00:00\",\"description\":\"Plan ticket volume, handling time, available capacity and support cost per customer.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/saas-support-capacity-planner\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/saas-support-capacity-planner\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/saas-support-capacity-planner\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"SaaS Support Capacity Planner\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"SaaS Support Capacity Planner | Lukas Wojcik","description":"Plan ticket volume, handling time, available capacity and support cost per customer.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/","og_locale":"en_US","og_type":"article","og_title":"SaaS Support Capacity Planner | Lukas Wojcik","og_description":"Plan ticket volume, handling time, available capacity and support cost per customer.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/","og_site_name":"Lukas Wojcik - Blog","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/","name":"SaaS Support Capacity Planner | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-10-05T15:09:12+00:00","description":"Plan ticket volume, handling time, available capacity and support cost per customer.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/saas-support-capacity-planner\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"SaaS Support Capacity Planner"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/21453","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=21453"}],"version-history":[{"count":1,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/21453\/revisions"}],"predecessor-version":[{"id":21465,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/21453\/revisions\/21465"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=21453"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=21453"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}