diff --git a/package-lock.json b/package-lock.json
index 4c3a64bd..173a00d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,7 @@
                 "@breejs/later": "^4.2.0",
                 "@fastify/autoload": "^5.8.0",
                 "@fastify/rate-limit": "^9.1.0",
-                "@fastify/reply-from": "^9.7.0",
+                "@fastify/reply-from": "^9.8.0",
                 "@fastify/sensible": "^5.5.0",
                 "@fastify/swagger": "^8.14.0",
                 "@fastify/swagger-ui": "^3.0.0",
@@ -47,7 +47,7 @@
                 "mkdirp": "^3.0.1",
                 "moment": "^2.30.1",
                 "moment-precise-range-plugin": "^1.3.0",
-                "mqtt": "^5.5.2",
+                "mqtt": "^5.5.3",
                 "ms-teams-wrapper": "^1.0.2",
                 "nodemailer": "^6.9.13",
                 "nodemailer-express-handlebars": "^6.1.2",
@@ -63,7 +63,7 @@
                 "winston": "^3.13.0",
                 "winston-daily-rotate-file": "^5.0.0",
                 "ws": "^8.16.0",
-                "xstate": "^5.10.0"
+                "xstate": "^5.11.0"
             },
             "devDependencies": {
                 "@babel/eslint-parser": "^7.24.1",
@@ -71,7 +71,7 @@
                 "eslint-plugin-import": "^2.29.1",
                 "jest": "^29.7.0",
                 "prettier": "^3.2.5",
-                "snyk": "^1.1288.0"
+                "snyk": "^1.1290.0"
             }
         },
         "node_modules/@aashutoshrathi/word-wrap": {
@@ -1282,17 +1282,16 @@
             }
         },
         "node_modules/@fastify/reply-from": {
-            "version": "9.7.0",
-            "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.7.0.tgz",
-            "integrity": "sha512-/F1QBl3FGlTqStjmiuoLRDchVxP967TZh6FZPwQteWhdLsDec8mqSACE+cRzw6qHUj3v9hfdd7JNgmb++fyFhQ==",
+            "version": "9.8.0",
+            "resolved": "https://registry.npmjs.org/@fastify/reply-from/-/reply-from-9.8.0.tgz",
+            "integrity": "sha512-bPNVaFhEeNI0Lyl6404YZaPFokudCplidE3QoOcr78yOy6H9sYw97p5KPYvY/NJNUHfFtvxOaSAHnK+YSiv/Mg==",
             "dependencies": {
                 "@fastify/error": "^3.0.0",
                 "end-of-stream": "^1.4.4",
                 "fast-content-type-parse": "^1.1.0",
                 "fast-querystring": "^1.0.0",
                 "fastify-plugin": "^4.0.0",
-                "pump": "^3.0.0",
-                "tiny-lru": "^11.0.0",
+                "toad-cache": "^3.7.0",
                 "undici": "^5.19.1"
             }
         },
@@ -6372,9 +6371,9 @@
             }
         },
         "node_modules/mqtt": {
-            "version": "5.5.2",
-            "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.2.tgz",
-            "integrity": "sha512-dlKxINBrrorgMp1A5UHQVf5GAkn1m/dY12W2Sp6LAY794RxQ0OPo0Q9N2S3qrNRjjC1WETA/9oYR6yadhR3siw==",
+            "version": "5.5.3",
+            "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.5.3.tgz",
+            "integrity": "sha512-R5fTibItlB5kvikTrU29ZgImvAch2ihKMyuvN3CJqd6nsZuearCSv3IGqxEdsSIXxflK6lGDgFmqnsnyJqzYtQ==",
             "dependencies": {
                 "@types/readable-stream": "^4.0.5",
                 "@types/ws": "^8.5.9",
@@ -7047,15 +7046,6 @@
             "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
             "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
         },
-        "node_modules/pump": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-            "dependencies": {
-                "end-of-stream": "^1.1.0",
-                "once": "^1.3.1"
-            }
-        },
         "node_modules/punycode": {
             "version": "2.3.0",
             "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
@@ -7517,9 +7507,9 @@
             }
         },
         "node_modules/snyk": {
-            "version": "1.1288.0",
-            "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1288.0.tgz",
-            "integrity": "sha512-IsfjXWVffhuB/UIefM7iqCGVBiLnULv08ax4YBTO/SF/RzIlm8Q92+I2sSwEva8f7kHYNE85Cjn9fg+LlmKUCQ==",
+            "version": "1.1290.0",
+            "resolved": "https://registry.npmjs.org/snyk/-/snyk-1.1290.0.tgz",
+            "integrity": "sha512-AD72kAeGZ9f5AguB4S4LnUFrpkCg3fww9pykAiLRkkBT4ueGWsN2QyAwH4tpdxUVs3R1SUnY1OornrBXRFkdNg==",
             "dev": true,
             "hasInstallScript": true,
             "dependencies": {
@@ -7846,14 +7836,6 @@
                 "real-require": "^0.2.0"
             }
         },
-        "node_modules/tiny-lru": {
-            "version": "11.0.1",
-            "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.0.1.tgz",
-            "integrity": "sha512-iNgFugVuQgBKrqeO/mpiTTgmBsTP0WL6yeuLfLs/Ctf0pI/ixGqIRm8sDCwMcXGe9WWvt2sGXI5mNqZbValmJg==",
-            "engines": {
-                "node": ">=12"
-            }
-        },
         "node_modules/tmpl": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -8438,9 +8420,9 @@
             }
         },
         "node_modules/xstate": {
-            "version": "5.10.0",
-            "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.10.0.tgz",
-            "integrity": "sha512-rn3AbPFHngLqtfFTe9KDXJ1wQX4ACDKnQyQ2ShjuKor3iVnxDSKyDL9llFLiOpaBQu+cFMKcZYlpXo5nb/q5rw==",
+            "version": "5.11.0",
+            "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.11.0.tgz",
+            "integrity": "sha512-0MqTLpc7dr/hXFHY25oN4sdnO3Ey6MYy9WkWxOgiwjPV0S6rWwLb5nZlRlPDSku2GEV4/y6AR8bX+GNCOxnEwA==",
             "funding": {
                 "type": "opencollective",
                 "url": "https://opencollective.com/xstate"
diff --git a/package.json b/package.json
index 3c0046a9..446c8877 100644
--- a/package.json
+++ b/package.json
@@ -52,7 +52,7 @@
         "@breejs/later": "^4.2.0",
         "@fastify/autoload": "^5.8.0",
         "@fastify/rate-limit": "^9.1.0",
-        "@fastify/reply-from": "^9.7.0",
+        "@fastify/reply-from": "^9.8.0",
         "@fastify/sensible": "^5.5.0",
         "@fastify/swagger": "^8.14.0",
         "@fastify/swagger-ui": "^3.0.0",
@@ -87,7 +87,7 @@
         "mkdirp": "^3.0.1",
         "moment": "^2.30.1",
         "moment-precise-range-plugin": "^1.3.0",
-        "mqtt": "^5.5.2",
+        "mqtt": "^5.5.3",
         "ms-teams-wrapper": "^1.0.2",
         "nodemailer": "^6.9.13",
         "nodemailer-express-handlebars": "^6.1.2",
@@ -103,7 +103,7 @@
         "winston": "^3.13.0",
         "winston-daily-rotate-file": "^5.0.0",
         "ws": "^8.16.0",
-        "xstate": "^5.10.0"
+        "xstate": "^5.11.0"
     },
     "devDependencies": {
         "@babel/eslint-parser": "^7.24.1",
@@ -111,7 +111,7 @@
         "eslint-plugin-import": "^2.29.1",
         "jest": "^29.7.0",
         "prettier": "^3.2.5",
-        "snyk": "^1.1288.0"
+        "snyk": "^1.1290.0"
     },
     "pkg": {
         "assets": [
diff --git a/src/config/config-gen-api-docs.yaml b/src/config/config-gen-api-docs.yaml
index f0333045..d95c5914 100644
--- a/src/config/config-gen-api-docs.yaml
+++ b/src/config/config-gen-api-docs.yaml
@@ -132,31 +132,49 @@ Butler:
   # Settings for monitoring Qlik Sense licenses
   qlikSenseLicense:
     licenseMonitor:
-      enable: true
-      frequency: every 5 minutes
+      enable: false
+      frequency: every 6 hours        # https://bunkat.github.io/later/parsers.html#text
       destination:
-        influxDb:                     # Send service alerts to InfluxDB
+        influxDb:                     # Store license data in InfluxDB
           enable: true
           tag: 
-            static:                     # Static attributes/dimensions to attach to the data sent to New Relic.
+            static:                   # Static attributes/tags to attach to the data sent to InflixDB
               # - name: foo
               #   value: bar
     licenseRelease:
-      enable: false  
-      # frequency: every 3 hours
-      frequency: every 5 minutes
-      neverReleaseUsers:
-        # - userDir: 'INTERNAL'
-        #   userId: 'sa_repository'
-        # - userDir: 'INTERNAL'
-        #   userId: 'sa_api'
+      enable: false                    # true/false. If true, Butler will release unused licenses according to settings below
+      dryRun: true                    # true/false. If true, Butler will not actually release any licenses, just log what it would have done. 
+      frequency: every 6 hours        # https://bunkat.github.io/later/parsers.html#text
+      neverRelease:                   # Various ways of defining which users should never have their licenses released
+        user:                         # Users who should never have their licenses released
+          # - userDir: 'INTERNAL'
+          #   userId: 'sa_repository'
+          # - userDir: 'INTERNAL'
+          #   userId: 'sa_api'
+          # - userDir: 'USERDIR'
+          #   userId: 'qs_admin_account'
+        tag:                          # Users with these tags will never have their licenses released
+          # - License do not release
+          # - some other tag
+        customProperty:               # Users with these custom properties will never have their licenses released
+          # - name: LicenseManage
+          #   value: do-not-release
+        userDirectory:                # List of user directories whose users should never have their licenses released
+          # - INTERNAL
+          # - ADMIN
+        inactive: Ignore              # Ignore/Yes/No. The value is case insensitive
+                                      #   No = Don't release licenses for users marked as "Inactive=No" in the QMC
+                                      #   Yes = Don't release licenses for users marked as "Inactive=Yes" in the QMC 
+                                      #   Ignore = Disregard this setting
+        blocked: Ignore               # Ignore/Yes/No, No = Don't release licenses for users marked as "Blocked=No" in the QMC
+        removedExternally: ignore     # Ignore/Yes/No, No = Don't release licenses for users marked as "Removed externally=No" in the QMC
       licenseType:                    # License types to monitor and release
         analyzer:                     
-          enable: true
-          releaseThresholdDays: 5
+          enable: true                # Monitor and release Analyzer licenses
+          releaseThresholdDays: 30    # Number of days a license can be unused before it is released
         professional:
-          enable: true
-          releaseThresholdDays: 5
+          enable: true                # Monitor and release Professional licenses
+          releaseThresholdDays: 30    # Number of days a license can be unused before it is released
       destination:
         influxDb:                     # Store info about released licenses in InfluxDB
           enable: true
diff --git a/src/config/production_template.yaml b/src/config/production_template.yaml
index 7a0f258a..5f87c1c4 100644
--- a/src/config/production_template.yaml
+++ b/src/config/production_template.yaml
@@ -139,7 +139,7 @@ Butler:
   # Settings for monitoring Qlik Sense licenses
   qlikSenseLicense:
     licenseMonitor:
-      enable: true
+      enable: false
       frequency: every 6 hours        # https://bunkat.github.io/later/parsers.html#text
       destination:
         influxDb:                     # Store license data in InfluxDB
@@ -149,15 +149,32 @@ Butler:
               - name: foo
                 value: bar
     licenseRelease:
-      enable: true
+      enable: false                    # true/false. If true, Butler will release unused licenses according to settings below
+      dryRun: true                    # true/false. If true, Butler will not actually release any licenses, just log what it would have done. 
       frequency: every 6 hours        # https://bunkat.github.io/later/parsers.html#text
-      neverReleaseUsers:              # Users that should never have their license released
-        - userDir: 'INTERNAL'
-          userId: 'sa_repository'
-        - userDir: 'INTERNAL'
-          userId: 'sa_api'
-        - userDir: 'USERDIR'
-          userId: 'qs_admin_account'
+      neverRelease:                   # Various ways of defining which users should never have their licenses released
+        user:                         # Users who should never have their licenses released
+          - userDir: 'INTERNAL'
+            userId: 'sa_repository'
+          - userDir: 'INTERNAL'
+            userId: 'sa_api'
+          - userDir: 'USERDIR'
+            userId: 'qs_admin_account'
+        tag:                          # Users with these tags will never have their licenses released
+          - License do not release
+          - some other tag
+        customProperty:               # Users with these custom properties will never have their licenses released
+          - name: LicenseManage
+            value: do-not-release
+        userDirectory:                # List of user directories whose users should never have their licenses released
+          - INTERNAL
+          - ADMIN
+        inactive: Ignore              # Ignore/Yes/No. The value is case insensitive
+                                      #   No = Don't release licenses for users marked as "Inactive=No" in the QMC
+                                      #   Yes = Don't release licenses for users marked as "Inactive=Yes" in the QMC 
+                                      #   Ignore = Disregard this setting
+        blocked: Ignore               # Ignore/Yes/No, No = Don't release licenses for users marked as "Blocked=No" in the QMC
+        removedExternally: ignore     # Ignore/Yes/No, No = Don't release licenses for users marked as "Removed externally=No" in the QMC
       licenseType:                    # License types to monitor and release
         analyzer:                     
           enable: true                # Monitor and release Analyzer licenses
diff --git a/src/lib/assert/assert_config_file.js b/src/lib/assert/assert_config_file.js
index 53df804a..fcc9161a 100644
--- a/src/lib/assert/assert_config_file.js
+++ b/src/lib/assert/assert_config_file.js
@@ -1029,51 +1029,57 @@ export const configFileStructureAssert = async (config, logger) => {
         configFileCorrect = false;
     }
 
+    // License release dry run
+    if (!config.has('Butler.qlikSenseLicense.licenseRelease.dryRun')) {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.dryRun"');
+        configFileCorrect = false;
+    }
+
     if (!config.has('Butler.qlikSenseLicense.licenseRelease.frequency')) {
         logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.frequency"');
         configFileCorrect = false;
     }
 
-    // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers are objects with the following properties:
+    // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.user are objects with the following properties:
     // {
     //     userDir: 'string'
     //     userId: 'string',
     // }
-    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers')) {
-        const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers');
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.user')) {
+        const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user');
 
         if (neverReleaseUsers) {
             if (!Array.isArray(neverReleaseUsers)) {
-                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers" is not an array');
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user" is not an array');
                 configFileCorrect = false;
             } else {
                 neverReleaseUsers.forEach((user, index) => {
                     if (typeof user !== 'object') {
                         logger.error(
-                            `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not an object`
+                            `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not an object`
                         );
                         configFileCorrect = false;
                     } else {
                         if (!Object.prototype.hasOwnProperty.call(user, 'userId')) {
                             logger.error(
-                                `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]"`
+                                `ASSERT CONFIG: Missing "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`
                             );
                             configFileCorrect = false;
                         } else if (typeof user.userId !== 'string') {
                             logger.error(
-                                `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not a string`
+                                `ASSERT CONFIG: "userId" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`
                             );
                             configFileCorrect = false;
                         }
 
                         if (!Object.prototype.hasOwnProperty.call(user, 'userDir')) {
                             logger.error(
-                                `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]"`
+                                `ASSERT CONFIG: Missing "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]"`
                             );
                             configFileCorrect = false;
                         } else if (typeof user.userDir !== 'string') {
                             logger.error(
-                                `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers[${index}]" is not a string`
+                                `ASSERT CONFIG: "userDir" property in "Butler.qlikSenseLicense.licenseRelease.neverRelease.user[${index}]" is not a string`
                             );
                             configFileCorrect = false;
                         }
@@ -1082,7 +1088,125 @@ export const configFileStructureAssert = async (config, logger) => {
             }
         }
     } else {
-        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers"');
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.user"');
+        configFileCorrect = false;
+    }
+
+    // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.tag objects with the following properties:
+    // - 'string'
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag')) {
+        const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag');
+
+        if (neverReleaseTags) {
+            if (!Array.isArray(neverReleaseTags)) {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag" is not an array');
+                configFileCorrect = false;
+            } else {
+                neverReleaseTags.forEach((tag, index) => {
+                    if (typeof tag !== 'string') {
+                        logger.error(`ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag[${index}]" is not a string`);
+                        configFileCorrect = false;
+                    }
+                });
+            }
+        }
+    } else {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.tag"');
+        configFileCorrect = false;
+    }
+
+    // The custom properties specified in the Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty array
+    // should meet the following requirements:
+    // - Each array item should be an object with the following properties:
+    //   {
+    //       name: 'string',
+    //       value: 'string'
+    //   }
+
+    // Make sure all entries in Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory objects with the following properties:
+    // - 'string'
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory')) {
+        const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory');
+
+        if (neverReleaseUserDirectories) {
+            if (!Array.isArray(neverReleaseUserDirectories)) {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory" is not an array');
+                configFileCorrect = false;
+            } else {
+                neverReleaseUserDirectories.forEach((userDirectory, index) => {
+                    if (typeof userDirectory !== 'string') {
+                        logger.error(
+                            `ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory[${index}]" is not a string`
+                        );
+                        configFileCorrect = false;
+                    }
+                });
+            }
+        }
+    } else {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory"');
+        configFileCorrect = false;
+    }
+
+    // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive meets the following requirements:
+    // - Value is either Yes or No
+    // - Disregard case
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive')) {
+        const inactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive');
+
+        if (inactive) {
+            if (typeof inactive !== 'string') {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" is not a string');
+                configFileCorrect = false;
+            } else if (!['yes', 'no', 'ignore'].includes(inactive.toLowerCase())) {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive" must be either "Yes" or "No"');
+                configFileCorrect = false;
+            }
+        }
+    } else {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive"');
+        configFileCorrect = false;
+    }
+
+    // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked meets the following requirements:
+    // - Value is either Yes or No
+    // - Disregard case
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked')) {
+        const blocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked');
+
+        if (blocked) {
+            if (typeof blocked !== 'string') {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" is not a string');
+                configFileCorrect = false;
+            } else if (!['yes', 'no', 'ignore'].includes(blocked.toLowerCase())) {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked" must be either "Yes" or "No"');
+                configFileCorrect = false;
+            }
+        }
+    } else {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked"');
+        configFileCorrect = false;
+    }
+
+    // Make sure the value of Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally meets the following requirements:
+    // - Value is either Yes or No
+    // - Disregard case
+    if (config.has('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')) {
+        const removedExternally = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally');
+
+        if (removedExternally) {
+            if (typeof removedExternally !== 'string') {
+                logger.error('ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" is not a string');
+                configFileCorrect = false;
+            } else if (!['yes', 'no', 'ignore'].includes(removedExternally.toLowerCase())) {
+                logger.error(
+                    'ASSERT CONFIG: "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally" must be either "Yes" or "No"'
+                );
+                configFileCorrect = false;
+            }
+        }
+    } else {
+        logger.error('ASSERT CONFIG: Missing config file entry "Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally"');
         configFileCorrect = false;
     }
 
diff --git a/src/lib/qliksense_license.js b/src/lib/qliksense_license.js
index d7f1ea19..b84842a0 100644
--- a/src/lib/qliksense_license.js
+++ b/src/lib/qliksense_license.js
@@ -54,26 +54,77 @@ async function checkQlikSenseLicenseStatus(config, logger) {
 }
 
 // Function to release professional licenses
-async function licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers) {
-    const result1 = await qrsInstance.Get(`license/professionalaccesstype/full`);
-
-    // Is status code 200 or body is empty?
+async function licenseReleaseProfessional(config, logger, qrsInstance) {
+    // Build date filter to be used when fetching licenses with old lastUsed date
+    // Get the current date and time
+    const currentDate = new Date();
+
+    // Get the release threshold (days) from the configuration
+    const releaseThresholdDays = config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays');
+
+    // Subtract the release threshold (days) from the current date, then round to the last moment of that day
+    const cutoffDate = new Date(currentDate);
+    cutoffDate.setDate(cutoffDate.getDate() - releaseThresholdDays);
+    cutoffDate.setHours(23, 59, 59, 999);
+
+    // verbose log, format dates as yyyy-mm-ddThh:mm:ss.sssZ
+    logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: currentDate: ${currentDate.toISOString()}`);
+    logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: releaseThresholdDays: ${releaseThresholdDays}`);
+    logger.verbose(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: cutoffDate: ${cutoffDate.toISOString()}`);
+
+    // Get all assigned professional licenses
+    const url = `license/professionalaccesstype/full?filter=lastUsed le '${cutoffDate.toISOString()}'`;
+    logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Query URL: ${url}`);
+    const result1 = await qrsInstance.Get(url);
+
+    // Is status code other than 200 or body is empty?
     if (result1.statusCode !== 200 || !result1.body) {
-        logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result1.statusCode}`);
+        logger.error(
+            `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Could not get list of assigned professional licenses. HTTP status code ${result1.statusCode}`
+        );
         return false;
     }
 
     // Debug log
-    logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Allocated: ${JSON.stringify(result1.body)}`);
+    logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: Assigned: ${JSON.stringify(result1.body)}`);
 
     // Determnine which allocated licenses to release.
     // Only release licenses that are NOT quarantined
-    // Take into account the releaese threshold (days), i.e. days since last use
     // Loop over all licenses retrived in previous step, add licenses to be released to releaseProfessional array
     const releaseProfessional = [];
+
+    const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user');
+    const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag');
+    const neverReleaseCustomProperties = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty');
+    const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory');
+
     // eslint-disable-next-line no-restricted-syntax
     for (const license of result1.body) {
         if (!license.quarantined) {
+            // Get full user info
+            let currentUser;
+            try {
+                // eslint-disable-next-line no-await-in-loop
+                const res = await qrsInstance.Get(`user/${license.user.id}`);
+                if (res.statusCode !== 200 || !res.body) {
+                    logger.error(
+                        `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`
+                    );
+                    return false;
+                }
+                currentUser = res.body;
+            } catch (err) {
+                logger.error(
+                    `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`
+                );
+                if (err.stack) {
+                    logger.error(
+                        `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}`
+                    );
+                }
+                return false;
+            }
+
             // Get days since last use
             const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24));
 
@@ -81,88 +132,279 @@ async function licenseReleaseProfessional(config, logger, qrsInstance, neverRele
             // Compare userDir and userId
             // If the user is in the neverReleaseUsers array, do not release the license
             let doNotRelease = false;
+            let doNotReleaseReason = '';
+
+            // Check do-not-release user names
             // eslint-disable-next-line no-restricted-syntax
             for (const user of neverReleaseUsers) {
                 if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) {
                     doNotRelease = true;
+                    doNotReleaseReason = 'User is in the neverRelease.user list';
                     break;
                 }
             }
 
-            // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license
+            // Check do-not-release tags
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not haven any neverReleaseTags set
+            if (!doNotRelease) {
+                // Check if the user has any of the neverReleaseTags set
+                // currentUser.tags is an array of tag objects. Each object has properties id and name
+                // eslint-disable-next-line no-restricted-syntax
+                for (const tag of currentUser.tags) {
+                    // eslint-disable-next-line no-restricted-syntax
+                    for (const neverReleaseTag of neverReleaseTags) {
+                        if (tag.name === neverReleaseTag) {
+                            doNotRelease = true;
+                            doNotReleaseReason = `User tagged with '${neverReleaseTag}', which is in the neverRelease.tag list`;
+                            break;
+                        }
+                    }
+                    if (doNotRelease) {
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release custom properties
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not have any neverReleaseCustomProperties set
+            if (!doNotRelease) {
+                // currentUser.customProperties is an array of custom property objects.
+                // Each object looks like this:
+                // {
+                //     "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f",
+                //     "value": "foo",
+                //     "definition": {
+                //         "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f",
+                //         "name": "bar",
+                //         "valueType": "Text"
+                //     }
+                // }
+                // eslint-disable-next-line no-restricted-syntax
+                for (const customProperty of currentUser.customProperties) {
+                    // eslint-disable-next-line no-restricted-syntax
+                    for (const neverReleaseCustomProperty of neverReleaseCustomProperties) {
+                        if (
+                            customProperty.definition.name === neverReleaseCustomProperty.name &&
+                            customProperty.value === neverReleaseCustomProperty.value
+                        ) {
+                            doNotRelease = true;
+                            doNotReleaseReason = `User has custom property '${neverReleaseCustomProperty.name}' set to '${neverReleaseCustomProperty.value}', which is in the neverRelease.customProperty list`;
+                            break;
+                        }
+                    }
+                    if (doNotRelease) {
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release user directory
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not have any neverReleaseUserDirectories set
+            if (!doNotRelease) {
+                // eslint-disable-next-line no-restricted-syntax
+                for (const neverReleaseUserDir of neverReleaseUserDirectories) {
+                    if (license.user.userDirectory === neverReleaseUserDir) {
+                        doNotRelease = true;
+                        doNotReleaseReason = `User's user directory is '${neverReleaseUserDir}', which is in the neverRelease.userDirectory list`;
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release inactive users
+            if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase() !== 'ignore') {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'No' (case insensitive) and currentUser.inactive===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'Yes' (case insensitive) and currentUser.inactive===true
+                const neverReleaseInactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase();
+                if (
+                    (neverReleaseInactive === 'no' && currentUser.inactive === false) ||
+                    (neverReleaseInactive === 'yes' && currentUser.inactive === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has inactive status '${currentUser.inactive}'`;
+                }
+            }
+
+            // Check do-not-release blocked users
+            if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase() !== 'ignore') {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'No' (case insensitive) and currentUser.blacklisted===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'Yes' (case insensitive) and currentUser.blacklisted===true
+                const neverReleaseBlocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase();
+                if (
+                    (neverReleaseBlocked === 'no' && currentUser.blacklisted === false) ||
+                    (neverReleaseBlocked === 'yes' && currentUser.blacklisted === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has blocked status '${currentUser.blacklisted}'`;
+                }
+            }
+
+            // Check do-not-release removed externally users
             if (
                 !doNotRelease &&
-                daysSinceLastUse >= config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays')
+                config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally').toLowerCase() !== 'ignore'
             ) {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'No' (case insensitive) and currentUser.removedExternally===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'Yes' (case insensitive) and currentUser.removedExternally===true
+                const neverReleaseRemovedExternally = config
+                    .get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')
+                    .toLowerCase();
+                if (
+                    (neverReleaseRemovedExternally === 'no' && currentUser.removedExternally === false) ||
+                    (neverReleaseRemovedExternally === 'yes' && currentUser.removedExternally === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has removedExternally status '${currentUser.removedExternally}'`;
+                }
+            }
+
+            // Should currentUser be released?
+            if (!doNotRelease) {
+                logger.info(
+                    `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseProfessional array`
+                );
                 releaseProfessional.push({
                     licenseId: license.id,
                     userDir: license.user.userDirectory,
                     userId: license.user.userId,
                     daysSinceLastUse,
                 });
+            } else {
+                logger.info(
+                    `QLIKSENSE LICENSE RELEASE PROFESSIONAL: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}`
+                );
             }
         }
     }
 
-    // Release all licenses in the releaseProfessional array
-    // eslint-disable-next-line no-restricted-syntax
-    for (const licenseRelease of releaseProfessional) {
-        logger.info(
-            `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`
-        );
-
-        // Release license
-        // eslint-disable-next-line no-await-in-loop
-        const result2 = await qrsInstance.Delete(`license/professionalaccesstype/${licenseRelease.licenseId}`);
+    logger.verbose(
+        `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Professional licenses to be released: ${JSON.stringify(releaseProfessional, null, 2)}`
+    );
+
+    // Is license release dry-run enabled? If so, do not release any licenses
+    if (config.get('Butler.qlikSenseLicense.licenseRelease.dryRun') === true) {
+        logger.info('QLIKSENSE LICENSE RELEASE PROFESSIONAL: Dry-run enabled. No licenses will be released');
+    } else {
+        // Release all licenses in the releaseProfessional array
+        // eslint-disable-next-line no-restricted-syntax
+        for (const licenseRelease of releaseProfessional) {
+            logger.info(
+                `QLIKSENSE LICENSE RELEASE PROFESSIONAL: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`
+            );
+
+            // Release license
+            // eslint-disable-next-line no-await-in-loop
+            const result2 = await qrsInstance.Delete(`license/professionalaccesstype/${licenseRelease.licenseId}`);
 
-        // Is status code 204? Error if it's nmt
-        if (result2.statusCode !== 204) {
-            logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result2.statusCode}`);
-            return false;
-        }
+            // Is status code 204? Error if it's nmt
+            if (result2.statusCode !== 204) {
+                logger.error(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: HTTP status code ${result2.statusCode}`);
+                return false;
+            }
 
-        // Debug log
-        logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: ${JSON.stringify(result2.body)}`);
+            // Debug log
+            logger.debug(`QLIKSENSE LICENSE RELEASE PROFESSIONAL: ${JSON.stringify(result2.body)}`);
 
-        // Write info about released license to InfluxDB?
-        if (
-            config.get('Butler.influxDb.enable') === true &&
-            config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') >= 0
-        ) {
-            // eslint-disable-next-line no-await-in-loop
-            await postQlikSenseLicenseReleasedToInfluxDB({
-                licenseType: 'professional',
-                licenseId: licenseRelease.licenseId,
-                userDir: licenseRelease.userDir,
-                userId: licenseRelease.userId,
-                daysSinceLastUse: licenseRelease.daysSinceLastUse,
-            });
+            // Write info about released license to InfluxDB?
+            if (
+                config.get('Butler.influxDb.enable') === true &&
+                config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.releaseThresholdDays') >= 0
+            ) {
+                // eslint-disable-next-line no-await-in-loop
+                await postQlikSenseLicenseReleasedToInfluxDB({
+                    licenseType: 'professional',
+                    licenseId: licenseRelease.licenseId,
+                    userDir: licenseRelease.userDir,
+                    userId: licenseRelease.userId,
+                    daysSinceLastUse: licenseRelease.daysSinceLastUse,
+                });
+            }
         }
     }
     return true;
 }
 
 // Function to release analyzer licenses
-async function licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers) {
-    const result3 = await qrsInstance.Get(`license/analyzeraccesstype/full`);
+async function licenseReleaseAnalyzer(config, logger, qrsInstance) {
+    // Build date filter to be used when fetching licenses with old lastUsed date
+    // Get the current date and time
+    const currentDate = new Date();
+
+    // Get the release threshold (days) from the configuration
+    const releaseThresholdDays = config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays');
+
+    // Subtract the release threshold (days) from the current date, then round to the last moment of that day
+    const cutoffDate = new Date(currentDate);
+    cutoffDate.setDate(cutoffDate.getDate() - releaseThresholdDays);
+    cutoffDate.setHours(23, 59, 59, 999);
+
+    // verbose log, format dates as yyyy-mm-ddThh:mm:ss.sssZ
+    logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: currentDate: ${currentDate.toISOString()}`);
+    logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: releaseThresholdDays: ${releaseThresholdDays}`);
+    logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: cutoffDate: ${cutoffDate.toISOString()}`);
+
+    // Get all assigned analyzer licenses
+    const url = `license/analyzeraccesstype/full?filter=lastUsed le '${cutoffDate.toISOString()}'`;
+    logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Query URL: ${url}`);
+    const result3 = await qrsInstance.Get(url);
 
     // Is status code 200 or body is empty?
     if (result3.statusCode !== 200 || !result3.body) {
-        logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result3.statusCode}`);
+        logger.error(
+            `QLIKSENSE LICENSE RELEASE ANALYZER: Could not get list of assigned analyzer licenses. HTTP status code ${result3.statusCode}`
+        );
         return;
     }
 
     // Debug log
-    logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Allocated: ${JSON.stringify(result3.body)}`);
+    logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: Assigned: ${JSON.stringify(result3.body)}`);
 
     // Determnine which allocated licenses to release.
     // Only release licenses that are NOT quarantined
-    // Take into account the releaese threshold (days), i.e. days since last use
     // Loop over all licenses retrived in previous step, add licenses to be released to releaseAnalyzer array
     const releaseAnalyzer = [];
+
+    const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.user');
+    const neverReleaseTags = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.tag');
+    const neverReleaseCustomProperties = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.customProperty');
+    const neverReleaseUserDirectories = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.userDirectory');
+
     // eslint-disable-next-line no-restricted-syntax
     for (const license of result3.body) {
         if (!license.quarantined) {
+            // Get full user info
+            let currentUser;
+            try {
+                // eslint-disable-next-line no-await-in-loop
+                const res = await qrsInstance.Get(`user/${license.user.id}`);
+                if (res.statusCode !== 200 || !res.body) {
+                    logger.error(
+                        `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`
+                    );
+                    return;
+                }
+                currentUser = res.body;
+            } catch (err) {
+                logger.error(
+                    `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}`
+                );
+                if (err.stack) {
+                    logger.error(
+                        `QLIKSENSE LICENSE RELEASE ANALYZER: Failed getting user info for user [${license.user.id}] ${license.user.userDirectory}\\${license.user.userId}. ${err.stack}`
+                    );
+                }
+                return;
+            }
+
             // Get days since last use
             const daysSinceLastUse = Math.floor((new Date() - new Date(license.lastUsed)) / (1000 * 60 * 60 * 24));
 
@@ -170,64 +412,203 @@ async function licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseU
             // Compare userDir and userId
             // If the user is in the neverReleaseUsers array, do not release the license
             let doNotRelease = false;
+            let doNotReleaseReason = '';
+
+            // Check do-not-release user names
             // eslint-disable-next-line no-restricted-syntax
             for (const user of neverReleaseUsers) {
                 if (license.user.userDirectory === user.userDir && license.user.userId === user.userId) {
                     doNotRelease = true;
+                    doNotReleaseReason = 'User is in the neverRelease.user list';
                     break;
                 }
             }
 
-            // If the user is not in the neverReleaseUsers array, and the days since last use is greater than the release threshold, release the license
+            // Check do-not-release tags
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not haven any neverReleaseTags set
+            if (!doNotRelease) {
+                // Check if the user has any of the neverReleaseTags set
+                // currentUser.tags is an array of tag objects. Each object has properties id and name
+                // eslint-disable-next-line no-restricted-syntax
+                for (const tag of currentUser.tags) {
+                    // eslint-disable-next-line no-restricted-syntax
+                    for (const neverReleaseTag of neverReleaseTags) {
+                        if (tag.name === neverReleaseTag) {
+                            doNotRelease = true;
+                            doNotReleaseReason = `User tagged with '${neverReleaseTag}', which is in the neverRelease.tag list`;
+                            break;
+                        }
+                    }
+                    if (doNotRelease) {
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release custom properties
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not have any neverReleaseCustomProperties set
+            if (!doNotRelease) {
+                // currentUser.customProperties is an array of custom property objects.
+                // Each object looks like this:
+                // {
+                //     "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f",
+                //     "value": "foo",
+                //     "definition": {
+                //         "id": "f4f1d1d0-5d5d-4e4e-8e8e-7f7f7f7f7f7f",
+                //         "name": "bar",
+                //         "valueType": "Text"
+                //     }
+                // }
+                // eslint-disable-next-line no-restricted-syntax
+                for (const customProperty of currentUser.customProperties) {
+                    // eslint-disable-next-line no-restricted-syntax
+                    for (const neverReleaseCustomProperty of neverReleaseCustomProperties) {
+                        if (
+                            customProperty.definition.name === neverReleaseCustomProperty.name &&
+                            customProperty.value === neverReleaseCustomProperty.value
+                        ) {
+                            doNotRelease = true;
+                            doNotReleaseReason = `User has custom property '${neverReleaseCustomProperty.name}' set to '${neverReleaseCustomProperty.value}', which is in the neverRelease.customProperty list`;
+                            break;
+                        }
+                    }
+                    if (doNotRelease) {
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release user directory
+            // If...
+            // - the user is not already marked as doNotRelease=true and
+            // - the currentUser does not have any neverReleaseUserDirectories set
+            if (!doNotRelease) {
+                // eslint-disable-next-line no-restricted-syntax
+                for (const neverReleaseUserDir of neverReleaseUserDirectories) {
+                    if (license.user.userDirectory === neverReleaseUserDir) {
+                        doNotRelease = true;
+                        doNotReleaseReason = `User's user directory is '${neverReleaseUserDir}', which is in the neverRelease.userDirectory list`;
+                        break;
+                    }
+                }
+            }
+
+            // Check do-not-release inactive users
+            if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase() !== 'ignore') {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'No' (case insensitive) and currentUser.inactive===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive === 'Yes' (case insensitive) and currentUser.inactive===true
+                const neverReleaseInactive = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.inactive').toLowerCase();
+                if (
+                    (neverReleaseInactive === 'no' && currentUser.inactive === false) ||
+                    (neverReleaseInactive === 'yes' && currentUser.inactive === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has inactive status '${currentUser.inactive}'`;
+                }
+            }
+
+            // Check do-not-release blocked users
+            if (!doNotRelease && config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase() !== 'ignore') {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'No' (case insensitive) and currentUser.blacklisted===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked === 'Yes' (case insensitive) and currentUser.blacklisted===true
+                const neverReleaseBlocked = config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.blocked').toLowerCase();
+                if (
+                    (neverReleaseBlocked === 'no' && currentUser.blacklisted === false) ||
+                    (neverReleaseBlocked === 'yes' && currentUser.blacklisted === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has blocked status '${currentUser.blacklisted}'`;
+                }
+            }
+
+            // Check do-not-release removed externally users
             if (
                 !doNotRelease &&
-                daysSinceLastUse >= config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays')
+                config.get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally').toLowerCase() !== 'ignore'
             ) {
+                // Do not release user if...
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'No' (case insensitive) and currentUser.removedExternally===false
+                // - config setting Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally === 'Yes' (case insensitive) and currentUser.removedExternally===true
+                const neverReleaseRemovedExternally = config
+                    .get('Butler.qlikSenseLicense.licenseRelease.neverRelease.removedExternally')
+                    .toLowerCase();
+                if (
+                    (neverReleaseRemovedExternally === 'no' && currentUser.removedExternally === false) ||
+                    (neverReleaseRemovedExternally === 'yes' && currentUser.removedExternally === true)
+                ) {
+                    doNotRelease = true;
+                    doNotReleaseReason = `User has removedExternally status '${currentUser.removedExternally}'`;
+                }
+            }
+
+            // Should currentUser be released?
+            if (!doNotRelease) {
+                logger.info(
+                    `QLIKSENSE LICENSE RELEASE ANALYZER: Adding user ${license.user.userDirectory}\\${license.user.userId} (days since last use: ${daysSinceLastUse}) to releaseAnalyzer array`
+                );
                 releaseAnalyzer.push({
                     licenseId: license.id,
                     userDir: license.user.userDirectory,
                     userId: license.user.userId,
                     daysSinceLastUse,
                 });
+            } else {
+                logger.info(
+                    `QLIKSENSE LICENSE RELEASE ANALYZER: License for user ${license.user.userDirectory}\\${license.user.userId} not released because: ${doNotReleaseReason}`
+                );
             }
         }
     }
 
-    // Release all licenses in the releaseAnalyzer array
-    // eslint-disable-next-line no-restricted-syntax
-    for (const licenseRelease of releaseAnalyzer) {
-        logger.info(
-            `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`
-        );
+    logger.verbose(`QLIKSENSE LICENSE RELEASE ANALYZER: Analyzer licenses to be released: ${JSON.stringify(releaseAnalyzer, null, 2)}`);
 
-        // Release license
-        // eslint-disable-next-line no-await-in-loop
-        const result4 = await qrsInstance.Delete(`license/analyzeraccesstype/${licenseRelease.licenseId}`);
+    // Is license release dry-run enabled? If so, do not release any licenses
+    if (config.get('Butler.qlikSenseLicense.licenseRelease.dryRun') === true) {
+        logger.info('QLIKSENSE LICENSE RELEASE ANALYZER: Dry-run enabled. No licenses will be released');
+    } else {
+        // Release all licenses in the releaseAnalyzer array
+        // eslint-disable-next-line no-restricted-syntax
+        for (const licenseRelease of releaseAnalyzer) {
+            logger.info(
+                `QLIKSENSE LICENSE RELEASE ANALYZER: Releasing license for user ${licenseRelease.userDir}\\${licenseRelease.userId} (days since last use: ${licenseRelease.daysSinceLastUse})`
+            );
 
-        // Is status code 204? Error if it's nmt
-        if (result4.statusCode !== 204) {
-            logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result4.statusCode}`);
-            return;
-        }
+            // Release license
+            // eslint-disable-next-line no-await-in-loop
+            const result4 = await qrsInstance.Delete(`license/analyzeraccesstype/${licenseRelease.licenseId}`);
 
-        // Debug log
-        logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: ${JSON.stringify(result4.body)}`);
+            // Is status code 204? Error if it's nmt
+            if (result4.statusCode !== 204) {
+                logger.error(`QLIKSENSE LICENSE RELEASE ANALYZER: HTTP status code ${result4.statusCode}`);
+                return;
+            }
 
-        // Write info about released license to InfluxDB?
-        if (
-            config.get('Butler.influxDb.enable') === true &&
-            config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') >= 0
-        ) {
-            // eslint-disable-next-line no-await-in-loop
-            await postQlikSenseLicenseReleasedToInfluxDB({
-                licenseType: 'analyzer',
-                licenseId: licenseRelease.licenseId,
-                userDir: licenseRelease.userDir,
-                userId: licenseRelease.userId,
-                daysSinceLastUse: licenseRelease.daysSinceLastUse,
-            });
+            // Debug log
+            logger.debug(`QLIKSENSE LICENSE RELEASE ANALYZER: ${JSON.stringify(result4.body)}`);
+
+            // Write info about released license to InfluxDB?
+            if (
+                config.get('Butler.influxDb.enable') === true &&
+                config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.releaseThresholdDays') >= 0
+            ) {
+                // eslint-disable-next-line no-await-in-loop
+                await postQlikSenseLicenseReleasedToInfluxDB({
+                    licenseType: 'analyzer',
+                    licenseId: licenseRelease.licenseId,
+                    userDir: licenseRelease.userDir,
+                    userId: licenseRelease.userId,
+                    daysSinceLastUse: licenseRelease.daysSinceLastUse,
+                });
+            }
         }
     }
+    return true;
 }
 
 // Function to release Qlik Sense licenses
@@ -248,30 +629,33 @@ async function checkQlikSenseLicenseRelease(config, logger) {
         };
         const qrsInstance = new QrsInteract(configQRS);
 
-        // Which user accounts should never be released?
-        // Get info from config file
-        const neverReleaseUsers = config.get('Butler.qlikSenseLicense.licenseRelease.neverReleaseUsers');
+        // Is license release enabled for professional access licenses?
+        if (config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.professional.enable') === true) {
+            // Release licenses of type "professional"
+            const res = await licenseReleaseProfessional(config, logger, qrsInstance);
 
-        // Release licenses of type "professional"
-        let res = await licenseReleaseProfessional(config, logger, qrsInstance, neverReleaseUsers);
-
-        // Success?
-        if (!res) {
-            return false;
+            // Success?
+            if (!res) {
+                return false;
+            }
         }
 
-        // Release licenses of type "analyzer"
-        res = await licenseReleaseAnalyzer(config, logger, qrsInstance, neverReleaseUsers);
-        // Success?
-        if (!res) {
-            return false;
+        // Is license release enabled for analyzer access licenses?
+        if (config.get('Butler.qlikSenseLicense.licenseRelease.licenseType.analyzer.enable') === true) {
+            // Release licenses of type "analyzer"
+            const res = await licenseReleaseAnalyzer(config, logger, qrsInstance);
+
+            // Success?
+            if (!res) {
+                return false;
+            }
         }
 
-        return true
+        return true;
     } catch (err) {
-        logger.error(`QLIKSENSE LICENSE MONITOR: ${err}`);
+        logger.error(`QLIKSENSE LICENSE RELEASE: ${err}`);
         if (err.stack) {
-            logger.error(`QLIKSENSE LICENSE MONITOR: ${err.stack}`);
+            logger.error(`QLIKSENSE LICENSE RELEASE: ${err.stack}`);
         }
         return false;
     }