DEV Community

473185670
473185670

Posted on

How I Built a Burnout Thought Detector in 100 Lines of Vanilla JavaScript — No AI, No NLP, No Sentiment Analysis

When you build anything that reads someone's thoughts and says "that's a burnout thought," the assumption is immediate: you need sentiment analysis. An LLM. A fine-tuned classifier. Something that "understands" the nuance of exhaustion and cynicism.

You don't.

I built a tool that takes a thought — "If I rest, everything will collapse" — and classifies it into the specific cognitive distortion driving it — Catastrophizing (predicting catastrophe without evidence) — then generates the targeted CBT reframe, using a domain-adapted keyword mapper. Zero dependencies, zero API calls, zero ML. It runs in the browser, on private thoughts, with no data leaving the device.

This is how burnout thought detection becomes 100 lines of JavaScript — and why a domain-adapted rule-based classifier beats both a generic distortion detector and a language model for a specific, well-studied psychological syndrome.

The problem

Burnout isn't just "feeling tired." It's a cluster of specific, recognizable cognitive distortions that fire in predictable ways — and they're distortions that a generic detector misses because they're about the relationship between rest, performance, and identity:

  • "I can't take this anymore, I'm breaking" → that's All-or-Nothing / Catastrophizing (framing a temporary state as permanent brokenness)
  • "I should be able to handle this" → that's Should Statements (an arbitrary standard that violates biology)
  • "If I rest, everything will collapse" → that's Catastrophizing (predicting disaster from recovery)
  • "I'm a failure at my job" → that's Labeling (a global identity verdict from a temporary state)
  • "It's all on me" → that's Personalization / Self-Blame (taking responsibility for systemic problems)
  • "They'll think I'm lazy if I leave at 5" → that's Mind-Reading (assuming others' judgment)
  • "Nothing I do matters" → that's All-or-Nothing / Helplessness (the classic burnout cognition)
  • "I'm exhausted, I must be failing" → that's Emotional Reasoning (treating a feeling as a fact)

A generic cognitive distortion detector — the kind that classifies any thought into the standard 11 distortions — can catch some of these. But it misses the pattern. "If I rest, everything will collapse" gets classified as "Catastrophizing" — technically correct, but uselessly generic. The burnout-specific reframe is different: it's not "consider that the catastrophe may not happen" (the generic advice), it's "what specifically would collapse if you took a lunch break? Most catastrophes are imagined. The real catastrophe is the burnout itself — chronic overworking destroys the capacity you're trying to protect. Rest is not the risk; burnout is the risk." The taxonomy and the intervention are both domain-specific.

Try it live: CBT for Burnout — Burnout Thought Tracker

The technique: domain-adapted classification

This is the key idea, and it's borrowed from ML without using ML.

In machine learning, transfer learning / domain adaptation means: take a general model trained on broad data, then adapt it to a specific domain by swapping in domain-specific knowledge — without changing the architecture. The structure stays; the weights change.

The rule-based equivalent is simpler and more transparent: keep the keyword-matching architecture, swap the knowledge base. A generic distortion detector has 11 keyword sets mapping to 11 distortions. A burnout detector has 9 burnout-specific keyword sets mapping to 9 burnout-adapted distortion variants, each with a reframe written for the burnout context.

Same algorithm. Different knowledge base. That's domain adaptation without a single gradient step.

This is a fundamentally different design from the generic detector I built before. The generic one asks "which of the 11 standard distortions is this?" The burnout one asks "which of the 9 ways does burnout specifically distort thinking — and what is the burnout-specific reframe?" The architecture is identical (keyword → category + reframe). The taxonomy is the product.

Why not an LLM?

I considered it. Here's why I didn't:

  1. The output space is small and domain-known. Decades of occupational health and CBT research have mapped how burnout distorts thinking. There are ~9 recurring patterns. An LLM generating one of 9 labels from a 175-billion-parameter model is a sledgehammer on a thumbtack.
  2. The classification must be deterministic. Run the detector on "If I rest, everything will collapse" twice and you must get "Catastrophizing" both times — that's what makes it a tool someone can trust and learn from. An LLM's output is stochastic. For a technique meant to teach someone to recognize their own patterns, nondeterminism is a bug.
  3. Privacy is the constraint. People type "I'm a failure at my job and it's all my fault" into this. That sentence sent to an API is a data breach dressed as a feature. Client-side keyword matching means the thought never leaves the browser.
  4. The reframe is the intervention — and it must be clinically precise. An LLM-generated reframe might be plausible but subtly wrong (e.g., "just take a vacation!" — that's not CBT, that's a band-aid that ignores the cognitive distortion maintaining the burnout). The rule-based reframes are written by hand, grounded in the actual CBT technique, and medically honest. For a mental health tool, "sounds right" isn't good enough.
  5. Domain adaptation is a knowledge-base swap, not an architecture change. Adapting an LLM to burnout means fine-tuning, eval sets, drift monitoring, cost. Adapting a rule-based classifier means editing 9 keyword arrays. The maintenance cost difference is 10,000x.

For a known output space, a deterministic classification, a privacy-first context, and a domain that's already been mapped by clinical research, a domain-adapted keyword mapper isn't a downgrade from an LLM — it's the correct architecture.

The algorithm: 9 burnout-specific branches

The core is a single function. Nine keyword sets, nine burnout-adapted distortion variants, nine hand-written reframes:

function dwIdentifyDistortion(thought) {
  var t = (thought || '').toLowerCase();
  if (!t) return { name: 'unidentified', reframe: 'Notice the thought, then identify the distortion.' };

  // 1. "I can't take this anymore" — capacity collapse
  if (t.indexOf('can\'t take') > -1 || t.indexOf('cant take') > -1 ||
      t.indexOf('anymore') > -1 || t.indexOf('breaking') > -1 || t.indexOf('broken') > -1) {
    return { name: 'All-or-Nothing / Catastrophizing',
      reframe: '"I can\'t take this anymore" is all-or-nothing thinking fused with catastrophizing. ' +
               'It frames a temporary state (exhaustion) as a permanent verdict (brokenness). ' +
               'The issue is not capacity; it\'s recovery debt — you\'ve been withdrawing from ' +
               'your energy account without depositing. The solution is not to "take more" but ' +
               'to start depositing (recovery).' };
  }

  // 2. "I should be able to handle this" — biological standard violation
  if (t.indexOf('should') > -1 || t.indexOf('supposed to') > -1 ||
      t.indexOf('ought') > -1 || t.indexOf('need to be able') > -1) {
    return { name: 'Should Statements',
      reframe: '"I should be able to handle this" is a should statement — an arbitrary standard ' +
               'that violates biology. The human nervous system is designed for acute stress with ' +
               'recovery periods, not chronic activation without rest. Expecting yourself to handle ' +
               'unlimited load without breaking is expecting yourself to be non-biological.' };
  }

  // 3. "If I rest, everything will collapse" — recovery catastrophizing
  if (t.indexOf('collapse') > -1 || t.indexOf('fall apart') > -1 ||
      t.indexOf('everything will') > -1 || t.indexOf('disaster') > -1 ||
      t.indexOf('if i rest') > -1 || t.indexOf('if i stop') > -1 || t.indexOf('behind') > -1) {
    return { name: 'Catastrophizing',
      reframe: '"If I rest, everything will collapse" is catastrophizing — predicting catastrophe ' +
               'without evidence. What specifically would collapse if you took a lunch break? ' +
               'Most catastrophes are imagined. The real catastrophe is the burnout itself — ' +
               'chronic overworking destroys the capacity you\'re trying to protect. ' +
               'Rest is not the risk; burnout is the risk.' };
  }

  // 4. "I'm a failure at my job" — global identity label
  if (t.indexOf('failure') > -1 || t.indexOf('fraud') > -1 || t.indexOf('useless') > -1 ||
      t.indexOf('worthless') > -1 || t.indexOf('incompetent') > -1 || t.indexOf('not good enough') > -1) {
    return { name: 'Labeling',
      reframe: '"I\'m a failure at my job" is labeling — a global identity verdict from a temporary ' +
               'state. You are a person experiencing burnout — a predictable response to chronic ' +
               'stress with insufficient recovery. Burnout reduces efficacy (that\'s a symptom, ' +
               'not a permanent trait). You were competent before the burnout; you will be ' +
               'competent after. The burnout is the variable, not you.' };
  }

  // 5. "It's all on me" — systemic personalization
  if (t.indexOf('all on me') > -1 || t.indexOf('my fault') > -1 ||
      t.indexOf('no one else') > -1 || t.indexOf('up to me') > -1) {
    return { name: 'Personalization / Self-Blame',
      reframe: '"It\'s all on me" is personalization — taking responsibility for systemic problems. ' +
               'Is it truly all on you, or are you carrying load that should be distributed ' +
               '(understaffing, unrealistic deadlines, poor management, scope creep)? ' +
               'You can control your recovery and your boundaries; you cannot single-handedly ' +
               'fix a broken system.' };
  }

  // 6. "They'll think I'm lazy if I leave at 5" — mind-reading
  if (t.indexOf('they think') > -1 || t.indexOf('they\'ll think') > -1 ||
      t.indexOf('lazy') > -1 || t.indexOf('slacking') > -1 || t.indexOf('judge') > -1) {
    return { name: 'Mind-Reading',
      reframe: '"They\'ll think I\'m lazy if I leave at 5" is mind-reading — assuming you know ' +
               'others\' thoughts without evidence. More likely: colleagues are too busy with their ' +
               'own work to monitor your hours. Your job is to produce results, not to perform ' +
               'busyness. What is your actual output? If it\'s good, the hours don\'t matter.' };
  }

  // 7. "Nothing I do matters" — helplessness
  if (t.indexOf('nothing') > -1 && (t.indexOf('matters') > -1 ||
      t.indexOf('works') > -1 || t.indexOf('changes') > -1)) {
    return { name: 'All-or-Nothing / Helplessness',
      reframe: '"Nothing I do matters" is all-or-nothing thinking fused with learned helplessness ' +
               '— a classic burnout cognition. It is the exhaustion talking, not reality. Things do ' +
               'matter; you are just too depleted to feel their impact right now (burnout blunts ' +
               'positive emotions). The cynicism dimension makes everything feel pointless — ' +
               'that\'s a symptom, not a truth. Recovery restores the capacity to feel meaning.' };
  }

  // 8. "I'm exhausted, I must be failing" — emotional reasoning
  if (t.indexOf('exhausted') > -1 || t.indexOf('tired') > -1 ||
      t.indexOf('drained') > -1 || t.indexOf('empty') > -1 || t.indexOf('depleted') > -1) {
    return { name: 'Emotional Reasoning',
      reframe: 'Feeling exhausted and concluding "I must be failing" is emotional reasoning — ' +
               'treating a feeling as a fact. Exhaustion is information about your energy state, ' +
               'not your performance or worth. The feeling says "restore me," not "you are failing." ' +
               'Respond to the feeling with recovery, not with more work.' };
  }

  // Fallback: a burnout cognition that doesn't match a specific pattern
  return { name: 'Burnout cognition (general)',
    reframe: 'This thought is a burnout cognition — a distortion maintained by chronic stress and ' +
             'insufficient recovery. Burnout systematically distorts thinking: it makes rest feel ' +
             'like failure, boundaries feel like betrayal, and normal limits feel like weakness. ' +
             'These are symptoms of the syndrome, not truths. The corrective is recovery + ' +
             'cognitive restructuring: challenge the thought, schedule the recovery.' };
}
Enter fullscreen mode Exit fullscreen mode

Nine branches. Each one is a recognition pattern (the keywords burnout thoughts actually use) mapped to a name (the distortion) and a reframe (the burnout-specific intervention). The fallback isn't "I don't know" — it's the general burnout-cognition pattern with the universal corrective (recovery + restructuring).

Notice what's NOT here: no confidence score, no probability, no "match strength." A keyword either fires or it doesn't. This is deliberate. In a clinical technique, the output is "this is Catastrophizing" — not "73% Catastrophizing, 12% Labeling." False precision is dishonest. The honest output is the category the thought matches, the name of the distortion, and the reframe. That's it.

Notice what IS here that the generic detector lacks: the reframe for "If I rest, everything will collapse" doesn't just say "you're catastrophizing." It says "the real catastrophe is the burnout itself." That's the domain-specific insight — burnout catastrophizing has a unique structure where the predicted catastrophe (rest → collapse) is the inverse of the actual risk (no rest → burnout → collapse). The generic "consider the catastrophe may not happen" misses this entirely. The taxonomy IS the product.

The behavioral insight layer

Detecting the distortion is half the tool. The other half is what you do about it — and burnout has characteristic behavioral responses that are just as classifiable as the thoughts. This is where the burnout detector is richer than the other domain-adapted detectors I've built: it tracks three behavioral rates and two trends:

// Overwork rate (pushed through + skipped break + after hours + weekend work)
var overworkCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('pushed') > -1 || b.indexOf('skipped') > -1 ||
         b.indexOf('after hours') > -1 || b.indexOf('weekend') > -1;
}).length;
var overworkRate = Math.round(overworkCount / total * 100);

// Recovery rate (took break + set boundary + recovery activity)
var recoveryCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('took a break') > -1 || b.indexOf('set a boundary') > -1 ||
         b.indexOf('recovery activity') > -1;
}).length;
var recoveryRate = Math.round(recoveryCount / total * 100);

// Disconnection/numbing rate (disconnected + numbed + snapped + withdrew)
var numbCount = dwEntries.filter(function(e) {
  var b = (e.behavior || '').toLowerCase();
  return b.indexOf('disconnected') > -1 || b.indexOf('numbed') > -1 ||
         b.indexOf('snapped') > -1 || b.indexOf('withdrew') > -1;
}).length;
var numbRate = Math.round(numbCount / total * 100);
Enter fullscreen mode Exit fullscreen mode

Three behavioral rates, each a keyword filter over the logged actions. Together they paint the behavioral profile: are you overworking, recovering, or numbing? The recoveryRate is the recovery metric. The overworkRate is the risk metric. The numbRate is the severity metric — disconnection and cynicism are the third dimension of burnout (exhaustion + cynicism + inefficacy), and they show up behaviorally as withdrawal, snapping, and numbing before they show up as self-reported cynicism.

And two trends — distress and energy — computed via split-half comparison across chronologically-sorted entries:

var chrono = dwEntries.slice().sort(function(a, b) {
  return new Date(a.when) - new Date(b.when);
});

// Distress trend
if (chrono.length >= 4) {
  var half = Math.floor(chrono.length / 2);
  var firstAvg  = chrono.slice(0, half).reduce(function(s, e) { return s + e.distress; }, 0) / half;
  var secondAvg = chrono.slice(half).reduce(function(s, e) { return s + e.distress; }, 0) / (chrono.length - half);
  if (secondAvg < firstAvg - 5) trendStr = 'decreasing (recovery is working — keep going!)';
  else if (secondAvg > firstAvg + 5) trendStr = 'increasing (are you still overworking? schedule recovery as non-negotiable)';
}

// Energy trend (the second trend — unique to burnout)
if (chrono.length >= 4) {
  var firstE  = chrono.slice(0, half).reduce(function(s, e) { return s + e.energy; }, 0) / half;
  var secondE = chrono.slice(half).reduce(function(s, e) { return s + e.energy; }, 0) / (chrono.length - half);
  if (secondE > firstE + 0.5) energyTrendStr = 'improving (energy returning — recovery is working!)';
  else if (secondE < firstE - 0.5) energyTrendStr = 'declining (energy still dropping — increase recovery urgently)';
}
Enter fullscreen mode Exit fullscreen mode

Split-half trend detection: compare the average in the first half of entries to the second half. Distress decreasing + energy improving = recovery is working. Distress increasing + energy declining = burnout is deepening — increase recovery urgently. Needs ≥4 entries to be meaningful — honest about when it doesn't know.

The dual-trend design is deliberate. Distress and energy are not just inverses of each other. You can have high distress AND improving energy (you're pushing through a hard week but recovery is starting to work). You can have low distress AND declining energy (you've numbed out — the cynicism dimension has blunted the emotional signal even as the energy account empties). Tracking both independently catches the numbed-out case that a single "distress" metric would miss. That's a domain-specific design choice the generic detector doesn't make.

The results

The detector handles real burnout thoughts:

Input:  "If I rest, everything will collapse."
Match:  "if i rest" + "collapse"  →  Catastrophizing
Reframe: "What specifically would collapse if you took a lunch break? Most
         catastrophes are imagined. The real catastrophe is the burnout
         itself — chronic overworking destroys the capacity you're trying
         to protect. Rest is not the risk; burnout is the risk."

Input:  "I should be able to handle this workload."
Match:  "should" + "handle"  →  Should Statements
Reframe: "This is an arbitrary standard that violates biology. The human
         nervous system is designed for acute stress with recovery periods,
         not chronic activation without rest. Expecting yourself to handle
         unlimited load without breaking is expecting yourself to be
         non-biological."

Input:  "Nothing I do matters anymore."
Match:  "nothing" + "matters" + "anymore"  →  All-or-Nothing / Helplessness
Reframe: "This is a classic burnout cognition — the exhaustion talking, not
         reality. Things do matter; you are just too depleted to feel their
         impact right now. The cynicism makes everything feel pointless —
         that's a symptom, not a truth. Recovery restores the capacity to
         feel meaning."
Enter fullscreen mode Exit fullscreen mode

And the behavioral profile across a month of entries:

30 entries logged.
  Avg distress:     72/100
  Avg energy:       3.2/10
  Overwork rate:    73%  (pushed through, skipped breaks, after hours, weekends)
  Recovery rate:    10%  (took breaks, set boundaries, recovery activities)
  Numbing rate:     17%  (disconnected, numbed, snapped, withdrew)
  Distress trend:   stable
  Energy trend:     declining (energy still dropping — increase recovery urgently)
→ Profile: classic burnout loop — overwork the hours, numb the feelings, never
  recover. The 10% recovery rate + declining energy is the lever. The numbing
  rate (17%) is the severity signal — cynicism has set in.
Enter fullscreen mode Exit fullscreen mode

Total code: ~100 lines (classifier + 3 behavioral rates + 2 trends). Total dependencies: 0. Total API calls: 0. Total thoughts sent to a server: 0.

Takeaways for builders

  1. Domain adaptation is a knowledge-base swap, not an architecture change. The burnout detector uses the exact same keyword-matching architecture as the generic distortion detector and the imposter detector I built before it. The only difference is the 9 keyword arrays and 9 reframes. When someone says "we need to fine-tune a model for our domain," ask first: is our domain already mapped by existing research? If yes, a rule-based knowledge-base swap is 10,000x cheaper and more transparent.
  2. The generic classifier is the wrong abstraction for a known domain. A generic 11-distortion detector classifies "If I rest, everything will collapse" as "Catastrophizing." Technically correct, uselessly generic. The burnout-specific variant classifies it as "Catastrophizing" and gives the burnout-specific reframe ("the real catastrophe is the burnout itself — rest is not the risk; burnout is the risk"). The taxonomy IS the product.
  3. Dual trends catch what a single metric misses. Tracking distress and energy independently catches the numbed-out burnout case (low distress + declining energy) that a single "how bad do you feel?" metric would miss. This is a domain-specific design choice — burnout has three dimensions (exhaustion, cynicism, inefficacy), and cynicism blunts the distress signal even as the energy account empties. Two trends, not one.
  4. Behavioral rates are a second classifier over the same data. The overwork/recovery/numbing rates are just keyword filters over the behavior field — a second classification pass on a different dimension of the same entries. Two classifiers, one data structure, zero extra storage. The recoveryRate is the single most important number in the tool: it's the recovery metric. The numbRate is the severity metric.

If you want to see the full code or use the tools:

If you prefer a structured Notion template for tracking your thoughts (the "paper notebook" version of these tools), I made one: CBT Thought Record for Notion ($7).


What's a domain where you're about to fine-tune an LLM — but the patterns are already mapped by existing research, and a 100-line knowledge-base swap would do?

Top comments (0)