diff --git a/.dir-locals.el b/.dir-locals.el
index 63008bd395e87a835bb05e3b74a4e31cfe4fe17b..305472b75754a24b24143e6b9cc0ffbab7c152f9 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -23,6 +23,8 @@
                               (macros/case 0)
                               (match 1)
                               (mbql.match/match 1)
+                              (mt/test-drivers 1)
+                              (mt/query 1)
                               (mbql.match/match-one 1)
                               (mbql.match/replace 1)
                               (mbql.match/replace-in 2)
diff --git a/frontend/src/metabase/dashboard/components/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
index fcfa5945478c28f530ae1f56d61149820914958e..36722735bd896ea12884f6f81b5e5d22b7cd56ee 100644
--- a/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
+++ b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { t } from "ttag";
-import { PARAMETER_SECTIONS } from "metabase/meta/Dashboard";
+import { getParameterSections } from "metabase/meta/Dashboard";
 import Icon from "metabase/components/Icon";
 import { getParameterIconName } from "metabase/meta/Parameter";
 import styled from "styled-components";
@@ -32,15 +32,17 @@ export default class ParametersPopover extends Component {
     this.state = {};
   }
 
+  PARAMETER_SECTIONS = getParameterSections();
+
   render() {
     const { section } = this.state;
     const { onClose, onAddParameter } = this.props;
     if (section == null) {
       return (
         <ParameterOptionsSectionsPane
-          sections={PARAMETER_SECTIONS}
+          sections={this.PARAMETER_SECTIONS}
           onSelectSection={selectedSection => {
-            const parameterSection = _.findWhere(PARAMETER_SECTIONS, {
+            const parameterSection = _.findWhere(this.PARAMETER_SECTIONS, {
               id: selectedSection.id,
             });
             if (parameterSection && parameterSection.options.length === 1) {
@@ -53,7 +55,9 @@ export default class ParametersPopover extends Component {
         />
       );
     } else {
-      const parameterSection = _.findWhere(PARAMETER_SECTIONS, { id: section });
+      const parameterSection = _.findWhere(this.PARAMETER_SECTIONS, {
+        id: section,
+      });
       return (
         <ParameterOptionsPane
           options={parameterSection && parameterSection.options}
diff --git a/frontend/src/metabase/meta/Dashboard.js b/frontend/src/metabase/meta/Dashboard.js
index f999dfdf5d0606f789fe2bd05778938372fce4a6..d58479732e7ee582abf4fb9e2d58e869f59d5fcb 100644
--- a/frontend/src/metabase/meta/Dashboard.js
+++ b/frontend/src/metabase/meta/Dashboard.js
@@ -1,3 +1,4 @@
+import MetabaseSettings from "metabase/lib/settings";
 import Question from "metabase-lib/lib/Question";
 import { ExpressionDimension } from "metabase-lib/lib/Dimension";
 
@@ -12,7 +13,7 @@ import type {
 import {
   dimensionFilterForParameter,
   variableFilterForParameter,
-  PARAMETER_OPTIONS,
+  getParameterOptions,
   PARAMETER_OPERATOR_TYPES,
   getOperatorDisplayName,
 } from "metabase/meta/Parameter";
@@ -29,67 +30,81 @@ export type ParameterSection = {
   options: ParameterOption[],
 };
 
-export const PARAMETER_SECTIONS: ParameterSection[] = [
-  {
-    id: "date",
-    name: t`Time`,
-    description: t`Date range, relative date, time of day, etc.`,
-    options: PARAMETER_OPERATOR_TYPES["date"].map(option => {
-      return {
-        ...option,
-        sectionId: "date",
-        combinedName: getOperatorDisplayName(option, "date", t`Date`),
-      };
-    }),
-  },
-  {
-    id: "location",
-    name: t`Location`,
-    description: t`City, State, Country, ZIP code.`,
-    options: PARAMETER_OPERATOR_TYPES["string"].map(option => {
-      return {
-        ...option,
-        sectionId: "location",
-        combinedName: getOperatorDisplayName(option, "string", t`Location`),
-      };
-    }),
-  },
-  {
-    id: "id",
-    name: t`ID`,
-    description: t`User ID, product ID, event ID, etc.`,
-    options: [
-      {
-        ..._.findWhere(PARAMETER_OPTIONS, { type: "id" }),
-        sectionId: "id",
-      },
-    ],
-  },
-  {
-    id: "number",
-    name: t`Number`,
-    description: t`Subtotal, Age, Price, Quantity, etc.`,
-    options: PARAMETER_OPERATOR_TYPES["number"].map(option => {
-      return {
-        ...option,
-        sectionId: "number",
-        combinedName: getOperatorDisplayName(option, "number", t`Number`),
-      };
-    }),
-  },
-  {
-    id: "category",
-    name: t`Other Categories`,
-    description: t`Category, Type, Model, Rating, etc.`,
-    options: PARAMETER_OPERATOR_TYPES["string"].map(option => {
-      return {
-        ...option,
-        sectionId: "category",
-        combinedName: getOperatorDisplayName(option, "string", t`Category`),
-      };
-    }),
-  },
-];
+const areFieldFilterOperatorsEnabled = () =>
+  MetabaseSettings.get("field-filter-operators-enabled?");
+
+export function getParameterSections(): ParameterSection[] {
+  const parameterOptions = getParameterOptions();
+  const LOCATION_OPTIONS = areFieldFilterOperatorsEnabled()
+    ? PARAMETER_OPERATOR_TYPES["string"].map(option => {
+        return {
+          ...option,
+          sectionId: "location",
+          combinedName: getOperatorDisplayName(option, "string", t`Location`),
+        };
+      })
+    : parameterOptions.filter(option => option.type.startsWith("location"));
+
+  const CATEGORY_OPTIONS = areFieldFilterOperatorsEnabled()
+    ? PARAMETER_OPERATOR_TYPES["string"].map(option => {
+        return {
+          ...option,
+          sectionId: "category",
+          combinedName: getOperatorDisplayName(option, "string", t`Category`),
+        };
+      })
+    : parameterOptions.filter(option => option.type.startsWith("category"));
+
+  return [
+    {
+      id: "date",
+      name: t`Time`,
+      description: t`Date range, relative date, time of day, etc.`,
+      options: PARAMETER_OPERATOR_TYPES["date"].map(option => {
+        return {
+          ...option,
+          sectionId: "date",
+          combinedName: getOperatorDisplayName(option, "date", t`Date`),
+        };
+      }),
+    },
+    {
+      id: "location",
+      name: t`Location`,
+      description: t`City, State, Country, ZIP code.`,
+      options: LOCATION_OPTIONS,
+    },
+    {
+      id: "id",
+      name: t`ID`,
+      description: t`User ID, product ID, event ID, etc.`,
+      options: [
+        {
+          ..._.findWhere(parameterOptions, { type: "id" }),
+          sectionId: "id",
+        },
+      ],
+    },
+    areFieldFilterOperatorsEnabled() && {
+      id: "number",
+      name: t`Number`,
+      description: t`Subtotal, Age, Price, Quantity, etc.`,
+      options: PARAMETER_OPERATOR_TYPES["number"].map(option => {
+        return {
+          ...option,
+          sectionId: "number",
+          combinedName: getOperatorDisplayName(option, "number", t`Number`),
+        };
+      }),
+    },
+    {
+      id: "category",
+      name: t`Other Categories`,
+      description: t`Category, Type, Model, Rating, etc.`,
+      options: CATEGORY_OPTIONS,
+    },
+  ].filter(Boolean);
+}
 
 export function getParameterMappingOptions(
   metadata: Metadata,
diff --git a/frontend/src/metabase/meta/Parameter.js b/frontend/src/metabase/meta/Parameter.js
index f612a61deae954910cd492dd0694ec85093daec8..340135d2fb89577a9d089adda3be47ba7220ffd9 100644
--- a/frontend/src/metabase/meta/Parameter.js
+++ b/frontend/src/metabase/meta/Parameter.js
@@ -1,3 +1,4 @@
+import MetabaseSettings from "metabase/lib/settings";
 import type { DatasetQuery } from "metabase-types/types/Card";
 import type {
   TemplateTag,
@@ -38,6 +39,9 @@ type TemplateTagFilter = (tag: TemplateTag) => boolean;
 type FieldPredicate = (field: Field) => boolean;
 type VariableFilter = (variable: Variable) => boolean;
 
+const areFieldFilterOperatorsEnabled = () =>
+  MetabaseSettings.get("field-filter-operators-enabled?");
+
 export const PARAMETER_OPERATOR_TYPES = {
   number: [
     {
@@ -160,15 +164,41 @@ const OPTIONS_WITH_OPERATOR_SUBTYPES = [
   },
 ];
 
-export const PARAMETER_OPTIONS: ParameterOption[] = [
-  {
-    type: "id",
-    name: t`ID`,
-  },
-  ...OPTIONS_WITH_OPERATOR_SUBTYPES.map(option =>
-    buildOperatorSubtypeOptions(option),
-  ),
-].flat();
+export function getParameterOptions(): ParameterOption[] {
+  return [
+    {
+      type: "id",
+      name: t`ID`,
+    },
+    ...(areFieldFilterOperatorsEnabled()
+      ? OPTIONS_WITH_OPERATOR_SUBTYPES.map(option =>
+          buildOperatorSubtypeOptions(option),
+        )
+      : [
+          ...PARAMETER_OPERATOR_TYPES["date"],
+          {
+            type: "category",
+            name: t`Category`,
+          },
+          {
+            type: "location/city",
+            name: t`City`,
+          },
+          {
+            type: "location/state",
+            name: t`State`,
+          },
+          {
+            type: "location/zip_code",
+            name: t`ZIP or Postal Code`,
+          },
+          {
+            type: "location/country",
+            name: t`Country`,
+          },
+        ]),
+  ].flat();
+}
 
 function buildOperatorSubtypeOptions({ type, typeName }) {
   return PARAMETER_OPERATOR_TYPES[type].map(option => ({
@@ -204,6 +234,7 @@ function getParameterSubType(parameter) {
 
 function fieldFilterForParameter(parameter: Parameter): FieldPredicate {
   const type = getParameterType(parameter);
+  const subtype = getParameterSubType(parameter);
   switch (type) {
     case "date":
       return (field: Field) => field.isDate();
@@ -212,11 +243,24 @@ function fieldFilterForParameter(parameter: Parameter): FieldPredicate {
     case "category":
       return (field: Field) => field.isCategory();
     case "location":
-      return (field: Field) =>
-        field.isCity() ||
-        field.isState() ||
-        field.isZipCode() ||
-        field.isCountry();
+      return (field: Field) => {
+        if (areFieldFilterOperatorsEnabled()) {
+          return field.isLocation();
+        } else {
+          switch (subtype) {
+            case "city":
+              return field.isCity();
+            case "state":
+              return field.isState();
+            case "zip_code":
+              return field.isZipCode();
+            case "country":
+              return field.isCountry();
+            default:
+              return false;
+          }
+        }
+      };
     case "number":
       return (field: Field) => field.isNumber() && !field.isCoordinate();
     case "string":
@@ -227,14 +271,14 @@ function fieldFilterForParameter(parameter: Parameter): FieldPredicate {
 }
 
 export function parameterOptionsForField(field: Field): ParameterOption[] {
-  return PARAMETER_OPTIONS.filter(option =>
-    fieldFilterForParameter(option)(field),
-  ).map(option => {
-    return {
-      ...option,
-      name: option.combinedName || option.name,
-    };
-  });
+  return getParameterOptions()
+    .filter(option => fieldFilterForParameter(option)(field))
+    .map(option => {
+      return {
+        ...option,
+        name: option.combinedName || option.name,
+      };
+    });
 }
 
 export function dimensionFilterForParameter(
@@ -293,7 +337,11 @@ export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
       id: tag.id,
       type:
         tag["widget-type"] ||
-        (tag.type === "date" ? "date/single" : "string/="),
+        (tag.type === "date"
+          ? "date/single"
+          : areFieldFilterOperatorsEnabled()
+          ? "string/="
+          : "category"),
       target:
         tag.type === "dimension"
           ? ["dimension", ["template-tag", tag.name]]
@@ -510,6 +558,10 @@ export function getParameterIconName(parameter: ?Parameter) {
 }
 
 export function mapUIParameterToQueryParameter(type, value, target) {
+  if (!areFieldFilterOperatorsEnabled()) {
+    return { type, value, target };
+  }
+
   const [fieldType, maybeOperatorName] = splitType(type);
   const operatorName = getParameterOperatorName(maybeOperatorName);
 
diff --git a/frontend/test/__runner__/backend.js b/frontend/test/__runner__/backend.js
index 0f37e3462b9720e3ef8c1229da6b37931742266e..cc18b6a899233d246ede84c373d48881ffa2ac88 100644
--- a/frontend/test/__runner__/backend.js
+++ b/frontend/test/__runner__/backend.js
@@ -68,6 +68,7 @@ export const BackendResource = createSharedResource("BackendResource", {
               (process.env["MB_EDITION"] === "ee" &&
                 process.env["ENTERPRISE_TOKEN"]) ||
               undefined,
+            "MB_FIELD_FILTER_OPERATORS_ENABLED?": "true",
           },
           stdio:
             process.env["DISABLE_LOGGING"] ||
diff --git a/frontend/test/__support__/cypress.js b/frontend/test/__support__/cypress.js
index fcb974cd30d1ead309c5b19d7dae52fd4e1ed8b3..7d4802739a8d9fd8443d9846137b8cfc41647796 100644
--- a/frontend/test/__support__/cypress.js
+++ b/frontend/test/__support__/cypress.js
@@ -315,3 +315,16 @@ export function getIframeBody(selector = "iframe") {
     .should("not.be.null")
     .then(cy.wrap);
 }
+
+export function mockSessionProperty(propertyOrObject, value) {
+  cy.intercept("GET", "/api/session/properties", req => {
+    req.reply(res => {
+      if (typeof propertyOrObject === "object") {
+        Object.assign(res.body, propertyOrObject);
+      }
+      {
+        res.body[propertyOrObject] = value;
+      }
+    });
+  });
+}
diff --git a/frontend/test/metabase/meta/Parameter.unit.spec.js b/frontend/test/metabase/meta/Parameter.unit.spec.js
index 02871c9445c47d806b39a5b55d3fc4ff72b6a07e..044e514767f7a7cf9002d4ef8f37afa8ddc45964 100644
--- a/frontend/test/metabase/meta/Parameter.unit.spec.js
+++ b/frontend/test/metabase/meta/Parameter.unit.spec.js
@@ -7,6 +7,7 @@ import {
   mapUIParameterToQueryParameter,
   deriveFieldOperatorFromParameter,
 } from "metabase/meta/Parameter";
+import MetabaseSettings from "metabase/lib/settings";
 
 describe("metabase/meta/Parameter", () => {
   describe("dateParameterValueToMBQL", () => {
@@ -169,6 +170,7 @@ describe("metabase/meta/Parameter", () => {
       isCountry: () => false,
       isNumber: () => false,
       isString: () => false,
+      isLocation: () => false,
     };
     it("should return relevantly typed options for date field", () => {
       const dateField = {
@@ -183,14 +185,14 @@ describe("metabase/meta/Parameter", () => {
     });
 
     it("should return relevantly typed options for location field", () => {
-      const stringField = {
+      const locationField = {
         ...field,
-        isString: () => true,
+        isCity: () => true,
       };
-      const availableOptions = parameterOptionsForField(stringField);
+      const availableOptions = parameterOptionsForField(locationField);
       expect(
         availableOptions.length > 0 &&
-          availableOptions.every(option => option.type.startsWith("string")),
+          availableOptions.every(option => option.type.startsWith("location")),
       ).toBe(true);
     });
   });
@@ -214,7 +216,22 @@ describe("metabase/meta/Parameter", () => {
     });
   });
 
-  describe("mapParameterTypeToFieldType", () => {
+  describe("mapParameterTypeToFieldType (field-filter-operators-enabled? === true)", () => {
+    let fieldFilterOperatorsEnabled;
+    beforeAll(() => {
+      fieldFilterOperatorsEnabled = MetabaseSettings.get(
+        "field-filter-operators-enabled?",
+      );
+      MetabaseSettings.set("field-filter-operators-enabled?", true);
+    });
+
+    afterAll(() => {
+      MetabaseSettings.set(
+        "field-filter-operators-enabled?",
+        fieldFilterOperatorsEnabled,
+      );
+    });
+
     it("should return the proper parameter type of location/category parameters", () => {
       expect(mapUIParameterToQueryParameter("category", "foo", "bar")).toEqual({
         type: "string/=",
@@ -274,6 +291,39 @@ describe("metabase/meta/Parameter", () => {
     });
   });
 
+  describe("mapParameterTypeToFieldType (field-filter-operators-enabled? === false)", () => {
+    let fieldFilterOperatorsEnabled;
+    beforeAll(() => {
+      fieldFilterOperatorsEnabled = MetabaseSettings.get(
+        "field-filter-operators-enabled?",
+      );
+      MetabaseSettings.set("field-filter-operators-enabled?", false);
+    });
+
+    afterAll(() => {
+      MetabaseSettings.set(
+        "field-filter-operators-enabled?",
+        fieldFilterOperatorsEnabled,
+      );
+    });
+
+    it("return given args in a map", () => {
+      expect(mapUIParameterToQueryParameter("category", "foo", "bar")).toEqual({
+        type: "category",
+        value: "foo",
+        target: "bar",
+      });
+
+      expect(
+        mapUIParameterToQueryParameter("location/city", ["foo"], "bar"),
+      ).toEqual({
+        type: "location/city",
+        value: ["foo"],
+        target: "bar",
+      });
+    });
+  });
+
   describe("deriveFieldOperatorFromParameter", () => {
     describe("when parameter is associated with an operator", () => {
       it("should return relevant operator object", () => {
diff --git a/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js
index c72529869f9ac4017e2dda8c451ac588cc58c2b6..61fd786a8cca96ad8688b9d3f370cf4b0d422e63 100644
--- a/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js
+++ b/frontend/test/metabase/scenarios/dashboard/dashboard_data_permissions.cy.spec.js
@@ -1,4 +1,9 @@
-import { restore, popover, selectDashboardFilter } from "__support__/cypress";
+import {
+  restore,
+  popover,
+  selectDashboardFilter,
+  mockSessionProperty,
+} from "__support__/cypress";
 
 function filterDashboard(suggests = true) {
   cy.visit("/dashboard/1");
@@ -21,6 +26,8 @@ function filterDashboard(suggests = true) {
 
 describe("support > permissions (metabase#8472)", () => {
   beforeEach(() => {
+    mockSessionProperty("field-filter-operators-enabled?", true);
+
     restore();
     cy.signInAsAdmin();
 
diff --git a/frontend/test/metabase/scenarios/dashboard/old-parameters.cy.spec.js b/frontend/test/metabase/scenarios/dashboard/old-parameters.cy.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bae6b1a52caef5e578bbae19d5aa6caac1e13ffc
--- /dev/null
+++ b/frontend/test/metabase/scenarios/dashboard/old-parameters.cy.spec.js
@@ -0,0 +1,86 @@
+import { popover, restore, mockSessionProperty } from "__support__/cypress";
+// NOTE: some overlap with parameters-embedded.cy.spec.js
+
+describe("scenarios > dashboard > parameters", () => {
+  beforeEach(() => {
+    mockSessionProperty("field-filter-operators-enabled?", false);
+    restore();
+    cy.signInAsAdmin();
+  });
+
+  it("should hide field operator parameters/show old options", () => {
+    cy.visit("/dashboard/1");
+
+    // Add a filter
+    cy.icon("pencil").click();
+    cy.icon("filter").click();
+
+    cy.findByText("Number").should("not.exist");
+
+    cy.findByText("Location").click();
+    cy.findByText("Contains").should("not.exist");
+    cy.findByText("City").should("exist");
+
+    cy.icon("filter").click();
+    cy.findByText("Contains").should("not.exist");
+  });
+
+  it("should filter by city", () => {
+    cy.visit("/dashboard/1");
+
+    cy.icon("pencil").click();
+    cy.icon("filter").click();
+
+    cy.findByText("Location").click();
+    cy.findByText("City").click();
+
+    cy.findByText("Select…").click();
+    popover().within(() => {
+      cy.findByText("City").click();
+    });
+
+    cy.findByText("No default").click();
+    cy.findByPlaceholderText("Search by City")
+      .click()
+      .type("B");
+    cy.findByText("Baker").click();
+    cy.findByText("Add filter").click();
+    cy.get(".Button--primary")
+      .contains("Done")
+      .click();
+
+    cy.findByText("Save").click();
+    cy.findByText("You're editing this dashboard.").should("not.exist");
+    cy.findByText("Baker");
+
+    cy.contains("of 8");
+  });
+
+  it("should filter by category", () => {
+    cy.visit("/dashboard/1");
+
+    cy.icon("pencil").click();
+    cy.icon("filter").click();
+
+    cy.findByText("Other Categories").click();
+
+    cy.findByText("Select…").click();
+    popover().within(() => {
+      cy.findByText("Name").click();
+    });
+
+    cy.contains("Done").click();
+
+    cy.findByText("Save").click();
+    cy.findByText("You're editing this dashboard.").should("not.exist");
+
+    cy.findByText("Category").click();
+    cy.findByPlaceholderText("Search by Name")
+      .click()
+      .type("bb");
+    cy.findByText("Abbie Parisian").click();
+    cy.findByText("Add filter").click();
+
+    cy.contains("of 18");
+  });
+});
diff --git a/modules/drivers/mongo/src/metabase/driver/mongo/parameters.clj b/modules/drivers/mongo/src/metabase/driver/mongo/parameters.clj
index 12d4db9d1a8fe7403beec6293e74bf0295634f0f..def6e73faece6d01fd36d8e43ad277f7e6e3caac 100644
--- a/modules/drivers/mongo/src/metabase/driver/mongo/parameters.clj
+++ b/modules/drivers/mongo/src/metabase/driver/mongo/parameters.clj
@@ -123,6 +123,7 @@
                                            [:template-tag
                                             [:field (field->name (:field v) false)
                                              {:base-type (get-in v [:field :base_type])}]])
+                                    params/throw-if-field-filter-operators-not-enabled
                                     ops/to-clause
                                     ;; desugar only impacts :does-not-contain -> [:not [:contains ... but it prevents
                                     ;; an optimization of [:= 'field 1 2 3] -> [:in 'field [1 2 3]] since that
diff --git a/project.clj b/project.clj
index 279582d8a12a2759c87cf93adb98a28caf1b5ab3..84e4f01a4f89179cd1ec61d1330da8a8b0d9ac70 100644
--- a/project.clj
+++ b/project.clj
@@ -237,6 +237,7 @@
 
     :env
     {:mb-run-mode       "dev"
+     :mb-field-filter-operators-enabled? "true"
      :mb-test-setting-1 "ABCDEFG"}
 
     :jvm-opts
@@ -330,6 +331,7 @@
     {:mb-run-mode     "test"
      :mb-db-in-memory "true"
      :mb-jetty-join   "false"
+     :mb-field-filter-operators-enabled? "true"
      :mb-api-key      "test-api-key"
      ;; use a random port between 3001 and 3501. That way if you run multiple sets of tests at the same time locally
      ;; they won't stomp on each other
diff --git a/src/metabase/api/embed.clj b/src/metabase/api/embed.clj
index 3834def9b3d758e239311e5ccb70917859e968f8..8340697927b206f7ec3c7c64f31a48f99587e459 100644
--- a/src/metabase/api/embed.clj
+++ b/src/metabase/api/embed.clj
@@ -23,6 +23,7 @@
             [metabase.api.dashboard :as dashboard-api]
             [metabase.api.dataset :as dataset-api]
             [metabase.api.public :as public-api]
+            [metabase.driver.common.parameters :as params]
             [metabase.models.card :refer [Card]]
             [metabase.models.dashboard :refer [Dashboard]]
             [metabase.models.dashboard-card :refer [DashboardCard]]
@@ -134,7 +135,9 @@
         :when                         (and tag-type
                                            (or widget-type (not= tag-type :dimension)))]
     {:id      (:id tag)
-     :type    (or widget-type (if (= tag-type :date) :date/single :string/=))
+     :type    (or widget-type (cond (= tag-type :date)                       :date/single
+                                    (params/field-filter-operators-enabled?) :string/=
+                                    :else                                    :category))
      :target  (if (= tag-type :dimension)
                 [:dimension [:template-tag (:name tag)]]
                 [:variable  [:template-tag (:name tag)]])
diff --git a/src/metabase/driver/common/parameters.clj b/src/metabase/driver/common/parameters.clj
index 44a64a7b63649dd8bc207604ff69b015b64f8283..e85a086b53b83e245f55f9802a69b59d6d528493 100644
--- a/src/metabase/driver/common/parameters.clj
+++ b/src/metabase/driver/common/parameters.clj
@@ -1,10 +1,31 @@
 (ns metabase.driver.common.parameters
   "Various record types below are used as a convenience for differentiating the different param types."
-  (:require [metabase.util.schema :as su]
+  (:require [metabase.models.setting :refer [defsetting]]
+            [metabase.query-processor.error-type :as qp.error-type]
+            [metabase.util.i18n :as i18n :refer [deferred-tru tru]]
+            [metabase.util.schema :as su]
             [potemkin.types :as p.types]
             [pretty.core :refer [PrettyPrintable]]
             [schema.core :as s]))
 
+(defsetting field-filter-operators-enabled?
+  (deferred-tru "Enable the new field-filter operators")
+  :type       :boolean
+  :visibility :public
+  :setter     :none)
+
+(defn throw-if-field-filter-operators-not-enabled
+  "Feature flag check for new field filter operations. Assumed already has been checked that `ops/operator?` is
+  true. Throws if the new field filter operators are not enabled. Intended to be removed when feature flag is no
+  longer necessary; designed so it can be used in a threading context and just raised out."
+  {:added "0.39.0"}
+  [field-filter]
+  (if (field-filter-operators-enabled?)
+    field-filter
+    (throw (ex-info (tru "New field filter operators are not enabled")
+                    {:type qp.error-type/invalid-parameter
+                     :operator (:type field-filter)}))))
+
 ;; "FieldFilter" is something that expands to a clause like "some_field BETWEEN 1 AND 10"
 ;;
 ;; `field` is a Field Toucan instance
diff --git a/src/metabase/driver/sql/parameters/substitution.clj b/src/metabase/driver/sql/parameters/substitution.clj
index f0987281bb29a7b8a793696f99cb9f3771f1d69a..fd6e7a118046b07bd3dd9190bf89597d1e9ad09a 100644
--- a/src/metabase/driver/sql/parameters/substitution.clj
+++ b/src/metabase/driver/sql/parameters/substitution.clj
@@ -243,9 +243,11 @@
     (cond
       (ops/operator? param-type)
       (let [[snippet & args]
-            (->> (ops/to-clause (assoc params :target
-                                       [:template-tag [:field (field->identifier driver field param-type)
-                                                       {:base-type (:base_type field)}]]))
+            (->> (assoc params :target
+                        [:template-tag [:field (field->identifier driver field param-type)
+                                        {:base-type (:base_type field)}]])
+                 i/throw-if-field-filter-operators-not-enabled
+                 ops/to-clause
                  mbql.u/desugar-filter-clause
                  wrap-value-literals/wrap-value-literals-in-mbql
                  (sql.qp/->honeysql driver)
diff --git a/src/metabase/query_processor/middleware/parameters/mbql.clj b/src/metabase/query_processor/middleware/parameters/mbql.clj
index e5e364365d7478e3bc30c60f4648c7f5748d55e4..d9283ae92e69881e2c9a007036fb7595ffa39869 100644
--- a/src/metabase/query_processor/middleware/parameters/mbql.clj
+++ b/src/metabase/query_processor/middleware/parameters/mbql.clj
@@ -1,6 +1,7 @@
 (ns metabase.query-processor.middleware.parameters.mbql
   "Code for handling parameter substitution in MBQL queries."
-  (:require [metabase.driver.common.parameters.dates :as date-params]
+  (:require [metabase.driver.common.parameters :as i]
+            [metabase.driver.common.parameters.dates :as date-params]
             [metabase.driver.common.parameters.operators :as ops]
             [metabase.mbql.schema :as mbql.s]
             [metabase.mbql.util :as mbql.u]
@@ -45,7 +46,7 @@
   [{param-type :type, param-value :value, [_ field :as target] :target, :as param}]
   (cond
     (ops/operator? param-type)
-    (ops/to-clause param)
+    (ops/to-clause (i/throw-if-field-filter-operators-not-enabled param))
     ;; multipe values. Recursively handle them all and glue them all together with an OR clause
     (sequential? param-value)
     (mbql.u/simplify-compound-filter
diff --git a/test/metabase/driver/sql/parameters/substitute_test.clj b/test/metabase/driver/sql/parameters/substitute_test.clj
index 4175882fff7ade321bf789e697a95f5c554463f3..e3934d1cef4edddd3163df124eb14fa93f111685 100644
--- a/test/metabase/driver/sql/parameters/substitute_test.clj
+++ b/test/metabase/driver/sql/parameters/substitute_test.clj
@@ -166,7 +166,17 @@
                    (substitute query {"param" (i/map->FieldFilter
                                                {:field (Field (mt/id :venues field))
                                                 :value {:type  operator
-                                                        :value value}})})))))))))
+                                                        :value value}})})))))))
+    (testing "Throws if not enabled (#15488)"
+      (with-redefs [i/field-filter-operators-enabled? (constantly false)]
+        (is (= :invalid-parameter
+               (try
+                 (substitute ["select * from venues where " (param "param")]
+                             {"param" (i/map->FieldFilter
+                                       {:field (Field (mt/id :venues :price))
+                                        :value {:type  :number/>=
+                                                :value [3]}})})
+                 (catch Exception e (:type (ex-data e))))))))))
 
 
 ;;; -------------------------------------------- Referenced Card Queries ---------------------------------------------
diff --git a/test/metabase/query_processor/middleware/parameters/mbql_test.clj b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
index af3e53a33ce3c76eabbbb29ab06e5254c5f930f4..ee9f2106d9a57e13d3e91219097a0f4934bfafc7 100644
--- a/test/metabase/query_processor/middleware/parameters/mbql_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
@@ -2,8 +2,10 @@
   "Tests for *MBQL* parameter substitution."
   (:require [clojure.test :refer :all]
             [metabase.driver :as driver]
+            [metabase.driver.common.parameters :as params]
             [metabase.mbql.normalize :as normalize]
             [metabase.query-processor :as qp]
+            [metabase.query-processor.error-type :as qp.error-type]
             [metabase.query-processor.middleware.parameters.mbql :as mbql-params]
             [metabase.test :as mt]))
 
@@ -159,7 +161,18 @@
                        :parameters [{:name   "name"
                                      :type   :string/starts-with
                                      :target $name
-                                     :value ["B"]}]})))))))))
+                                     :value ["B"]}]})))))
+        (with-redefs [params/field-filter-operators-enabled? (constantly false)]
+          (testing "Throws if not enabled (#15488)"
+            (is (= {:type     qp.error-type/invalid-parameter
+                    :operator :number/between}
+                   (try (f (mt/query venues
+                             {:query      {:aggregation [[:count]]}
+                              :parameters [{:name   "price"
+                                            :type   :number/between
+                                            :target $price
+                                            :value  [2 5]}]}))
+                        (catch Exception e (ex-data e)))))))))))
 
 (deftest basic-where-test
   (mt/test-drivers (params-test-drivers)