diff --git a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj
index 4eacbc75f4e478a4e4f01873a3b1fdb616acda54..6089512e1ba6bb23e755d66e8ff68f42cc28e670 100644
--- a/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/serialization/cmd_test.clj
@@ -69,6 +69,8 @@
           :dataset_query          "{}"
           :creator_id             (u/the-id user)
           :visualization_settings "{}"
+          :parameters             "[]"
+          :parameter_mappings     "[]"
           :created_at             :%now
           :updated_at             :%now)
         ;; serialize "everything" (which should just be the card and user), which should succeed if #16931 is fixed
diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js
index dc9de872d160be2526709b0a743fc21f5388364e..df6c909604c446b9a7d935ab6703f51772068e06 100644
--- a/frontend/src/metabase/entities/questions.js
+++ b/frontend/src/metabase/entities/questions.js
@@ -79,6 +79,8 @@ const Questions = createEntity({
     "display",
     "description",
     "visualization_settings",
+    "parameters",
+    "parameter_mappings",
     "archived",
     "enable_embedding",
     "embedding_params",
diff --git a/frontend/src/metabase/parameters/utils/cards.ts b/frontend/src/metabase/parameters/utils/cards.ts
index 265bed63523f7755ab9d63e4a3de5703615761f1..2bfbf5acc27974b4fe8253ce76279f0477770f17 100644
--- a/frontend/src/metabase/parameters/utils/cards.ts
+++ b/frontend/src/metabase/parameters/utils/cards.ts
@@ -30,7 +30,24 @@ function getTemplateTagType(tag: TemplateTag) {
   }
 }
 
+export function getTemplateTagParameter(tag: TemplateTag): ParameterWithTarget {
+  const target: ParameterTarget =
+    tag.type === "dimension"
+      ? ["dimension", ["template-tag", tag.name]]
+      : ["variable", ["template-tag", tag.name]];
+
+  return {
+    id: tag.id,
+    type: tag["widget-type"] || getTemplateTagType(tag),
+    target,
+    name: tag["display-name"],
+    slug: tag.name,
+    default: tag.default,
+  };
+}
+
 // NOTE: this should mirror `template-tag-parameters` in src/metabase/api/embed.clj
+
 export function getTemplateTagParameters(
   tags: TemplateTag[],
 ): ParameterWithTarget[] {
@@ -39,21 +56,7 @@ export function getTemplateTagParameters(
       tag =>
         tag.type != null && (tag["widget-type"] || tag.type !== "dimension"),
     )
-    .map(tag => {
-      const target: ParameterTarget =
-        tag.type === "dimension"
-          ? ["dimension", ["template-tag", tag.name]]
-          : ["variable", ["template-tag", tag.name]];
-
-      return {
-        id: tag.id,
-        type: tag["widget-type"] || getTemplateTagType(tag),
-        target,
-        name: tag["display-name"],
-        slug: tag.name,
-        default: tag.default,
-      };
-    });
+    .map(getTemplateTagParameter);
 }
 
 export function getTemplateTagsForParameters(card: Card) {
@@ -74,7 +77,11 @@ export function getTemplateTagsForParameters(card: Card) {
 export function getParametersFromCard(
   card: Card,
 ): Parameter[] | ParameterWithTarget[] {
-  if (card && card.parameters) {
+  if (!card) {
+    return [];
+  }
+
+  if (card.parameters && !_.isEmpty(card.parameters)) {
     return card.parameters;
   }
 
@@ -86,12 +93,12 @@ export function getValueAndFieldIdPopulatedParametersFromCard(
   card: Card,
   metadata: Metadata,
   parameterValues: { [key: string]: any },
+  parameters = getParametersFromCard(card),
 ) {
   if (!card) {
     return [];
   }
 
-  const parameters = getParametersFromCard(card);
   const valuePopulatedParameters: (Parameter[] | ParameterWithTarget[]) & {
     value?: any;
   } = getValuePopulatedParameters(parameters, parameterValues);
diff --git a/frontend/src/metabase/parameters/utils/dashboards.ts b/frontend/src/metabase/parameters/utils/dashboards.ts
index 1667118647ffd9417b86765d44a816601705ec3c..2f8b4ab783e71167407e1ae53fa3ad7e7c546924 100644
--- a/frontend/src/metabase/parameters/utils/dashboards.ts
+++ b/frontend/src/metabase/parameters/utils/dashboards.ts
@@ -2,6 +2,7 @@ import _ from "underscore";
 import { setIn } from "icepick";
 
 import Question from "metabase-lib/lib/Question";
+import { generateParameterId } from "metabase/parameters/utils/parameter-id";
 import { getParameterTargetField } from "metabase/parameters/utils/targets";
 import { slugify } from "metabase/lib/formatting";
 import {
@@ -59,7 +60,7 @@ export function createParameter(
   const parameter = {
     name: "",
     slug: "",
-    id: Math.floor(Math.random() * Math.pow(2, 32)).toString(16),
+    id: generateParameterId(),
     type: option.type,
     sectionId: option.sectionId,
   };
diff --git a/frontend/src/metabase/parameters/utils/parameter-id.js b/frontend/src/metabase/parameters/utils/parameter-id.js
new file mode 100644
index 0000000000000000000000000000000000000000..2071d30a566d4f07b097c4e6474448c9f7a37154
--- /dev/null
+++ b/frontend/src/metabase/parameters/utils/parameter-id.js
@@ -0,0 +1,4 @@
+export function generateParameterId() {
+  const num = Math.floor(Math.random() * Math.pow(2, 32));
+  return num.toString(16);
+}
diff --git a/frontend/src/metabase/parameters/utils/parameter-id.unit.spec.js b/frontend/src/metabase/parameters/utils/parameter-id.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1298d4aadff4da943330614f7f62c0e84e98ede4
--- /dev/null
+++ b/frontend/src/metabase/parameters/utils/parameter-id.unit.spec.js
@@ -0,0 +1,7 @@
+import { generateParameterId } from "./parameter-id";
+
+describe("parameters/utils/parameter-id", () => {
+  it("should generate a random parameter id", () => {
+    expect(generateParameterId().length).toBeGreaterThan(1);
+  });
+});
diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx
index 77a6ef252a478c27b75942d36e46df4c525fd0d9..129bdaac12ee3cf5fb5741b74400501dbafc559c 100644
--- a/frontend/src/metabase/public/containers/PublicQuestion.jsx
+++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx
@@ -16,8 +16,8 @@ import {
 } from "metabase/parameters/utils/parameter-values";
 import { applyParameters } from "metabase/meta/Card";
 import {
-  getParametersFromCard,
   getValueAndFieldIdPopulatedParametersFromCard,
+  getParametersFromCard,
 } from "metabase/parameters/utils/cards";
 
 import {
@@ -91,6 +91,8 @@ class PublicQuestion extends Component {
       const parameters = getValueAndFieldIdPopulatedParametersFromCard(
         card,
         metadata,
+        {},
+        card.parameters || undefined,
       );
       const parameterValuesById = getParameterValuesByIdFromQueryParams(
         parameters,
@@ -134,7 +136,7 @@ class PublicQuestion extends Component {
       return;
     }
 
-    const parameters = getParametersFromCard(card);
+    const parameters = card.parameters || getParametersFromCard(card);
 
     try {
       this.setState({ result: null });
@@ -187,7 +189,13 @@ class PublicQuestion extends Component {
     );
 
     const parameters =
-      card && getValueAndFieldIdPopulatedParametersFromCard(card, metadata);
+      card &&
+      getValueAndFieldIdPopulatedParametersFromCard(
+        card,
+        metadata,
+        {},
+        card.parameters || undefined,
+      );
 
     return (
       <EmbedFrame
diff --git a/frontend/src/metabase/query_builder/actions/native.js b/frontend/src/metabase/query_builder/actions/native.js
index 823eedf1ad238013d74a5e3ab14c817baa4ca542..ab456bdd6ee62ba2d2146dfcb5fa98845b2d226b 100644
--- a/frontend/src/metabase/query_builder/actions/native.js
+++ b/frontend/src/metabase/query_builder/actions/native.js
@@ -1,5 +1,5 @@
 import _ from "underscore";
-import { updateIn } from "icepick";
+import { assoc, updateIn } from "icepick";
 
 import { createAction } from "redux-actions";
 
@@ -17,6 +17,12 @@ import {
 import { updateQuestion } from "./core";
 import { SET_UI_CONTROLS } from "./ui";
 
+import {
+  getTemplateTagsForParameters,
+  getTemplateTagParameters,
+  getTemplateTagParameter,
+} from "metabase/parameters/utils/cards";
+
 export const TOGGLE_DATA_REFERENCE = "metabase/qb/TOGGLE_DATA_REFERENCE";
 export const toggleDataReference = createAction(TOGGLE_DATA_REFERENCE, () => {
   MetabaseAnalytics.trackStructEvent("QueryBuilder", "Toggle Data Reference");
@@ -124,7 +130,7 @@ export const setTemplateTag = createThunkAction(
       }
 
       // we need to preserve the order of the keys to avoid UI jumps
-      return updateIn(
+      const updatedTagsCard = updateIn(
         updatedCard,
         ["dataset_query", "native", "template-tags"],
         tags => {
@@ -137,6 +143,26 @@ export const setTemplateTag = createThunkAction(
           return { ...tags, [name]: newTag };
         },
       );
+
+      const { parameters } = updatedTagsCard;
+      if (parameters && Array.isArray(parameters)) {
+        if (parameters.length === 0) {
+          // reconstruct from the existing template tags
+          const tags = getTemplateTagsForParameters(updatedTagsCard);
+          const newParameters = getTemplateTagParameters(tags);
+          return assoc(updatedTagsCard, "parameters", newParameters);
+        } else {
+          // update an existing parameter
+          const index = parameters.findIndex(p => p.id === templateTag.id);
+          if (index < 0) {
+            console.warn(`Can't find parameter with id=${templateTag.id}!`);
+          } else {
+            parameters[index] = getTemplateTagParameter(templateTag);
+          }
+        }
+      }
+
+      return updatedTagsCard;
     };
   },
 );
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index d25efa4df8cf47fe34117d9227797c8da5310c8c..d16360ece2baf91d99269921fdbc000115aa96d9 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -11791,6 +11791,61 @@ databaseChangeLog:
                   nullable: true
                   unique: true
             tableName: report_dashboardcard
+  - changeSet:
+      id: v44.00-023
+      author: qnkhuat
+      comment: Added 0.44.0 - Add parameters to report_card
+      changes:
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: parameters
+                  type: ${text.type}
+                  remarks: List of parameter associated to a card
+                  constraints:
+                    nullable: true
+                    deferrable: false
+                    initiallyDeferred: false
+  - changeSet:
+      id: v44.00-024
+      author: qnkhuat
+      comment: Added 0.44.0 - Add parameters to report_card
+      changes:
+        - addNotNullConstraint:
+            columnDataType: ${text.type}
+            columnName: parameters
+            defaultNullValue: '[]'
+            tableName: report_card
+
+  - changeSet:
+      id: v44.00-025
+      author: qnkhuat
+      comment: Added 0.44.0 - Add parameter_mappings to report_card
+      changes:
+        - addColumn:
+            tableName: report_card
+            columns:
+              - column:
+                  name: parameter_mappings
+                  type: ${text.type}
+                  remarks: List of parameter associated to a card
+                  constraints:
+                    nullable: true
+                    deferrable: false
+                    initiallyDeferred: false
+  - changeSet:
+      id: v44.00-026
+      author: qnkhuat
+      comment: Added 0.44.0 - Add parameter_mappings to report_card
+      changes:
+        - addNotNullConstraint:
+            columnDataType: ${text.type}
+            columnName: parameter_mappings
+            defaultNullValue: '[]'
+            tableName: report_card
+
+
 
 # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
 
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index d8a788437483be6786e9d6ce12fc6d156fb0e46a..608ec898ccdb4fbf0d877e51f8a04c5068323a13 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -270,14 +270,16 @@
 (defn- create-card-async!
   "Create a new Card asynchronously. Returns a channel for fetching the newly created Card, or an Exception if one was
   thrown. Closing this channel before it finishes will cancel the Card creation."
-  [{:keys [dataset_query result_metadata dataset], :as card-data}]
+  [{:keys [dataset_query result_metadata dataset parameters parameter_mappings], :as card-data}]
   ;; `zipmap` instead of `select-keys` because we want to get `nil` values for keys that aren't present. Required by
   ;; `api/maybe-reconcile-collection-position!`
-  (let [data-keys            [:dataset_query :description :display :name
-                              :visualization_settings :collection_id :collection_position :cache_ttl]
+  (let [data-keys            [:dataset_query :description :display :name :visualization_settings
+                              :parameters :parameter_mappings :collection_id :collection_position :cache_ttl]
         card-data            (assoc (zipmap data-keys (map card-data data-keys))
                                     :creator_id api/*current-user-id*
-                                    :dataset (boolean (:dataset card-data)))
+                                    :dataset (boolean (:dataset card-data))
+                                    :parameters (or parameters [])
+                                    :parameter_mappings (or parameter_mappings []))
         result-metadata-chan (result-metadata-async {:query dataset_query
                                                      :metadata result_metadata
                                                      :dataset? dataset})
@@ -298,8 +300,10 @@
 (api/defendpoint ^:returns-chan POST "/"
   "Create a new `Card`."
   [:as {{:keys [collection_id collection_position dataset_query description display name
-                result_metadata visualization_settings cache_ttl], :as body} :body}]
+                parameters parameter_mappings result_metadata visualization_settings cache_ttl], :as body} :body}]
   {name                   su/NonBlankString
+   parameters             (s/maybe [su/Parameter])
+   parameter_mappings     (s/maybe [su/ParameterMapping])
    description            (s/maybe su/NonBlankString)
    display                su/NonBlankString
    visualization_settings su/Map
@@ -502,7 +506,7 @@
         (u/select-keys-when card-updates
           :present #{:collection_id :collection_position :description :cache_ttl :dataset}
           :non-nil #{:dataset_query :display :name :visualization_settings :archived :enable_embedding
-                     :embedding_params :result_metadata})))
+                     :parameters :parameter_mappings :embedding_params :result_metadata})))
     ;; Fetch the updated Card from the DB
     (let [card (Card id)]
       (delete-alerts-if-needed! card-before-update card)
@@ -521,10 +525,11 @@
 (api/defendpoint ^:returns-chan PUT "/:id"
   "Update a `Card`."
   [id :as {{:keys [dataset_query description display name visualization_settings archived collection_id
-                   collection_position enable_embedding embedding_params result_metadata
+                   collection_position enable_embedding embedding_params result_metadata parameters
                    cache_ttl dataset]
             :as   card-updates} :body}]
   {name                   (s/maybe su/NonBlankString)
+   parameters             (s/maybe [su/Parameter])
    dataset_query          (s/maybe su/Map)
    dataset                (s/maybe s/Bool)
    display                (s/maybe su/NonBlankString)
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index 4497e465e4fde7f79c9d338bf05b9dc4261c9459..d557b3c93af6da8386492f05d1cc16075e5664a0 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -69,7 +69,7 @@
   "Create a new Dashboard."
   [:as {{:keys [name description parameters cache_ttl collection_id collection_position], :as _dashboard} :body}]
   {name                su/NonBlankString
-   parameters          [su/Map]
+   parameters          (s/maybe [su/Parameter])
    description         (s/maybe s/Str)
    cache_ttl           (s/maybe su/IntGreaterThanZero)
    collection_id       (s/maybe su/IntGreaterThanZero)
@@ -275,7 +275,7 @@
    show_in_getting_started (s/maybe s/Bool)
    enable_embedding        (s/maybe s/Bool)
    embedding_params        (s/maybe su/EmbeddingParams)
-   parameters              (s/maybe [su/Map])
+   parameters              (s/maybe [su/Parameter])
    position                (s/maybe su/IntGreaterThanZero)
    archived                (s/maybe s/Bool)
    collection_id           (s/maybe su/IntGreaterThanZero)
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index 2a9b7924668a70792cacbe3fd4665429fe80f70f..40c57d6a88a53894f319ccdcbac282ada4e12671 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -210,11 +210,17 @@
 
 ;; TODO -- consider whether we should validate the Card query when you save/update it??
 (defn- pre-insert [card]
-  (u/prog1 card
-    ;; make sure this Card doesn't have circular source query references
-    (check-for-circular-source-query-references card)
-    (check-field-filter-fields-are-from-correct-database card)
-    (collection/check-collection-namespace Card (:collection_id card))))
+  (let [defaults {:parameters         []
+                  :parameter_mappings []}
+        card     (merge defaults card)]
+   (u/prog1 card
+     ;; make sure this Card doesn't have circular source query references
+     (check-for-circular-source-query-references card)
+     (check-field-filter-fields-are-from-correct-database card)
+     ;; TODO: add a check to see if all id in :parameter_mappings are in :parameters
+     (params/assert-valid-parameters card)
+     (params/assert-valid-parameter-mappings card)
+     (collection/check-collection-namespace Card (:collection_id card)))))
 
 (defn- post-insert [card]
   ;; if this Card has any native template tag parameters we need to update FieldValues for any Fields that are
@@ -231,7 +237,8 @@
   For the OSS edition, there is no implementation for this function -- it is a no-op. For Metabase Enterprise Edition,
   the implementation of this function is
   [[metabase-enterprise.sandbox.models.group-table-access-policy/update-card-check-gtaps]] and is installed by that
-  namespace."} pre-update-check-sandbox-constraints
+  namespace."}
+  pre-update-check-sandbox-constraints
   (atom identity))
 
 (defn- pre-update [{archived? :archived, id :id, :as changes}]
@@ -261,6 +268,8 @@
     (check-field-filter-fields-are-from-correct-database changes)
     ;; Make sure the Collection is in the default Collection namespace (e.g. as opposed to the Snippets Collection namespace)
     (collection/check-collection-namespace Card (:collection_id changes))
+    (params/assert-valid-parameters changes)
+    (params/assert-valid-parameter-mappings changes)
     ;; additional checks (Enterprise Edition only)
     (@pre-update-check-sandbox-constraints changes)))
 
@@ -289,7 +298,9 @@
                                        :embedding_params       :json
                                        :query_type             :keyword
                                        :result_metadata        ::result-metadata
-                                       :visualization_settings :visualization-settings})
+                                       :visualization_settings :visualization-settings
+                                       :parameters             :parameters-list
+                                       :parameter_mappings     :parameters-list})
           :properties     (constantly {:timestamped? true
                                        :entity_id    true})
           ;; Make sure we normalize the query before calling `pre-update` or `pre-insert` because some of the
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index 0580b3c7a3b636f8c45e229815a379ea14d472b1..20cbfd7537fea33472570af60c42dea117569936 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -64,11 +64,6 @@
 (models/defmodel Dashboard :report_dashboard)
 ;;; ----------------------------------------------- Entity & Lifecycle -----------------------------------------------
 
-(defn- assert-valid-parameters [{:keys [parameters]}]
-  (when (s/check (s/maybe [{:id su/NonBlankString, s/Keyword s/Any}]) parameters)
-    (throw (ex-info (tru ":parameters must be a sequence of maps with String :id keys")
-                    {:parameters parameters}))))
-
 (defn- pre-delete [dashboard]
   (db/delete! 'Revision :model "Dashboard" :model_id (u/the-id dashboard)))
 
@@ -76,12 +71,12 @@
   (let [defaults  {:parameters []}
         dashboard (merge defaults dashboard)]
     (u/prog1 dashboard
-      (assert-valid-parameters dashboard)
+      (params/assert-valid-parameters dashboard)
       (collection/check-collection-namespace Dashboard (:collection_id dashboard)))))
 
 (defn- pre-update [dashboard]
   (u/prog1 dashboard
-    (assert-valid-parameters dashboard)
+    (params/assert-valid-parameters dashboard)
     (collection/check-collection-namespace Dashboard (:collection_id dashboard))))
 
 (defn- update-dashboard-subscription-pulses!
diff --git a/src/metabase/models/params.clj b/src/metabase/models/params.clj
index fb303a8ab209ab03875bea0ef623bf5af285f0cb..a23c504852c1b4e2a77d7d4f8024070db3c8ee01 100644
--- a/src/metabase/models/params.clj
+++ b/src/metabase/models/params.clj
@@ -17,6 +17,20 @@
 ;;; |                                                     SHARED                                                     |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
+(defn assert-valid-parameters
+  "Receive a Paremeterized Object and check if its parameters is valid."
+  [{:keys [parameters]}]
+  (when (s/check (s/maybe [su/Parameter]) parameters)
+    (throw (ex-info (tru ":parameters must be a sequence of maps with String :id key")
+                    {:parameters parameters}))))
+
+(defn assert-valid-parameter-mappings
+  "Receive a Paremeterized Object and check if its parameters is valid."
+  [{:keys [parameter_mappings]}]
+  (when (s/check (s/maybe [su/ParameterMapping]) parameter_mappings)
+    (throw (ex-info (tru ":parameter_mappings must be a sequence of maps with String :parameter_id key")
+                    {:parameter_mappings parameter_mappings}))))
+
 (s/defn unwrap-field-clause :- mbql.s/field
   "Unwrap something that contains a `:field` clause, such as a template tag, Also handles unwrapped integers for
   legacy compatibility.
diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj
index 0b2d709b94781a099a457ed7cea79f4e6a1ce071..6ad80d196abbd1f89f3ef9baabff279cf0fd66ee 100644
--- a/src/metabase/util/schema.clj
+++ b/src/metabase/util/schema.clj
@@ -327,6 +327,18 @@
                                                     false)))
     (deferred-tru "value must be a valid JSON string.")))
 
+(def Parameter
+  "Schema for a valid Parameter."
+  (with-api-error-message {:id       NonBlankString
+                           s/Keyword s/Any}
+    (deferred-tru "parameter must be a map with String :id key")))
+
+(def ParameterMapping
+  "Schema for a valid Parameter Mapping"
+  (with-api-error-message {:parameter_id       NonBlankString
+                           s/Keyword s/Any}
+    (deferred-tru "parameter mapping must be a String :parameter_id key")))
+
 (def EmbeddingParams
   "Schema for a valid map of embedding params."
   (with-api-error-message (s/maybe {s/Keyword (s/enum "disabled" "enabled" "locked")})
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 2bb567d2e7e95481f27cbf5a8188084e334b8d71..2f515fb4e217d584133959e3461785a6b7097903 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -43,6 +43,7 @@
   (-> (mt/run-mbql-query venues {:aggregation [[:count]]}) :data :cols first :base_type))
 
 (def card-defaults
+  "The default card params."
   {:archived            false
    :collection_id       nil
    :collection_position nil
@@ -54,6 +55,8 @@
    :entity_id           nil
    :embedding_params    nil
    :made_public_by_id   nil
+   :parameters          []
+   :parameter_mappings  []
    :moderation_reviews  ()
    :public_uuid         nil
    :query_type          nil
@@ -307,13 +310,19 @@
           (mt/with-model-cleanup [Card]
             (let [card (assoc (card-with-name-and-query (mt/random-name)
                                                         (mbql-count-query (mt/id) (mt/id :venues)))
-                              :collection_id (u/the-id collection))]
+                              :collection_id      (u/the-id collection)
+                              :parameters         [{:id "abc123", :name "test", :type "date"}]
+                              :parameter_mappings [{:parameter_id "abc123", :card_id "10",
+                                                    :target [:dimension [:template-tags "category"]]}])]
               (is (= (merge
                       card-defaults
                       {:name                   (:name card)
                        :collection_id          true
                        :collection             true
                        :creator_id             (mt/user->id :rasta)
+                       :parameters             [{:id "abc123", :name "test", :type "date"}]
+                       :parameter_mappings     [{:parameter_id "abc123", :card_id "10",
+                                                 :target ["dimension" ["template-tags" "category"]]}]
                        :dataset_query          true
                        :query_type             "query"
                        :visualization_settings {:global {:title nil}}
@@ -347,6 +356,16 @@
                                                        (update :id boolean)
                                                        (update :timestamp boolean))))))))))))))
 
+(deftest create-card-validation-test
+  (testing "POST /api/card"
+   (is (= {:errors {:visualization_settings "value must be a map."}}
+          (mt/user-http-request :crowberto :post 400 "card" {:visualization_settings "ABC"})))
+
+   (is (= {:errors {:parameters (str "value may be nil, or if non-nil, value must be an array. "
+                                     "Each parameter must be a map with String :id key")}}
+          (mt/user-http-request :crowberto :post 400 "card" {:visualization_settings {:global {:title nil}}
+                                                             :parameters             "abc"})))))
+
 (deftest save-empty-card-test
   (testing "POST /api/card"
     (testing "Should be able to save an empty Card"
@@ -757,6 +776,50 @@
       (is (= ""
              (db/select-one-field :description Card :id (u/the-id card)))))))
 
+(deftest update-card-parameters-test
+  (testing "PUT /api/card/:id"
+    (mt/with-temp Card [card]
+      (testing "successfully update with valid parameters"
+        (is (partial= {:parameters [{:id   "random-id"
+                                     :type "number"}]}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameters [{:id   "random-id"
+                                                           :type "number"}]})))))
+
+    (mt/with-temp Card [card {:parameters [{:id   "random-id"
+                                            :type "number"}]}]
+      (testing "nil parameters will no-op"
+        (is (partial= {:parameters [{:id   "random-id"
+                                     :type "number"}]}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameters nil}))))
+      (testing "an empty list will remove parameters"
+        (is (partial= {:parameters []}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameters []})))))))
+
+(deftest update-card-parameter-mappings-test
+  (testing "PUT /api/card/:id"
+    (mt/with-temp Card [card]
+      (testing "successfully update with valid parameter_mappings"
+        (is (partial= {:parameter_mappings [{:parameter_id "abc123", :card_id "10",
+                                             :target ["dimension" ["template-tags" "category"]]}]}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameter_mappings [{:parameter_id "abc123", :card_id "10",
+                                                                   :target ["dimension" ["template-tags" "category"]]}]})))))
+
+    (mt/with-temp Card [card {:parameter_mappings [{:parameter_id "abc123", :card_id "10",
+                                                    :target ["dimension" ["template-tags" "category"]]}]}]
+      (testing "nil parameters will no-op"
+        (is (partial= {:parameter_mappings [{:parameter_id "abc123", :card_id "10",
+                                             :target ["dimension" ["template-tags" "category"]]}]}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameters nil}))))
+      (testing "an empty list will remove parameter_mappings"
+        (is (partial= {:parameter_mappings []}
+                      (mt/user-http-request :rasta :put 202 (str "card/" (u/the-id card))
+                                            {:parameter_mappings []})))))))
+
 (deftest update-embedding-params-test
   (testing "PUT /api/card/:id"
     (mt/with-temp Card [card]
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 282068be20ef5390fd24cba566e40b529f828385..b3f1e362d6ad8ce3bd2acfa69a0628d7e8d95671 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -113,7 +113,8 @@
     (is (= {:errors {:name "value must be a non-blank string."}}
            (mt/user-http-request :rasta :post 400 "dashboard" {})))
 
-    (is (= {:errors {:parameters "value must be an array. Each value must be a map."}}
+    (is (= {:errors {:parameters (str "value may be nil, or if non-nil, value must be an array. "
+                                      "Each parameter must be a map with String :id key")}}
            (mt/user-http-request :crowberto :post 400 "dashboard" {:name       "Test"
                                                                    :parameters "abc"})))))
 
diff --git a/test/metabase/db/schema_migrations_test.clj b/test/metabase/db/schema_migrations_test.clj
index 8d977584ca80248363d1f88a3777333dd11a0ff0..159c7b59c3ae00488a0cb6fa61a06e11c5d339d8 100644
--- a/test/metabase/db/schema_migrations_test.clj
+++ b/test/metabase/db/schema_migrations_test.clj
@@ -555,7 +555,7 @@
                         :left-join [[PermissionsGroup :pg] [:= :p.group_id :pg.id]]
                         :where     [:= :pg.name perms-group/all-users-group-name]}))))))
 
-(deftest grant-subscription-permission-tests
+(deftest grant-subscription-permission-test
   (testing "Migration v43.00-047: Grant the 'All Users' Group permissions to create/edit subscriptions and alerts"
     (impl/test-migrations ["v43.00-047" "v43.00-048"] [migrate!]
         (migrate!)
@@ -565,7 +565,7 @@
                                           :left-join [[PermissionsGroup :pg] [:= :p.group_id :pg.id]]
                                           :where     [:= :p.object "/general/subscription/"]}))))))))
 
-(deftest rename-general-permissions-to-application-tests
+(deftest rename-general-permissions-to-application-test
   (testing "Migration v43.00-057: Rename general permissions to application permissions"
     (impl/test-migrations ["v43.00-057" "v43.00-058"] [migrate!]
       (letfn [(get-perms [object] (set (map :name (db/query {:select    [:pg.name]
@@ -575,3 +575,50 @@
         (is (= #{"All Users"} (get-perms "/general/subscription/")))
         (migrate!)
         (is (= #{"All Users"} (get-perms "/application/subscription/")))))))
+
+(deftest add-parameter-to-cards-test
+  (testing "Migration v44.00-022: Add parameters to report_card"
+    (impl/test-migrations ["v44.00-022" "v44.00-024"] [migrate!]
+      (let [user-id
+            (db/simple-insert! User {:first_name  "Howard"
+                                     :last_name   "Hughes"
+                                     :email       "howard@aircraft.com"
+                                     :password    "superstrong"
+                                     :date_joined :%now})
+            database-id (db/simple-insert! Database {:name "DB", :engine "h2", :created_at :%now, :updated_at :%now})
+            card-id (db/simple-insert! Card {:name                   "My Saved Question"
+                                             :created_at             :%now
+                                             :updated_at             :%now
+                                             :creator_id             user-id
+                                             :display                "table"
+                                             :dataset_query          "{}"
+                                             :visualization_settings "{}"
+                                             :database_id            database-id
+                                             :collection_id          nil})]
+       (migrate!)
+       (is (= [] (:parameters (first (db/simple-select Card {:where [:= :id card-id]})))))))))
+
+(deftest add-parameter-mappings-to-cards-test
+  (testing "Migration v44.00-024: Add parameter_mappings to cards"
+    (impl/test-migrations ["v44.00-024" "v44.00-026"] [migrate!]
+      (let [user-id
+            (db/simple-insert! User {:first_name  "Howard"
+                                     :last_name   "Hughes"
+                                     :email       "howard@aircraft.com"
+                                     :password    "superstrong"
+                                     :date_joined :%now})
+            database-id
+            (db/simple-insert! Database {:name "DB", :engine "h2", :created_at :%now, :updated_at :%now})
+            card-id
+            (db/simple-insert! Card {:name                   "My Saved Question"
+                                     :created_at             :%now
+                                     :updated_at             :%now
+                                     :creator_id             user-id
+                                     :display                "table"
+                                     :dataset_query          "{}"
+                                     :visualization_settings "{}"
+                                     :database_id            database-id
+                                     :collection_id          nil})]
+        (migrate!)
+        (is (= []
+               (:parameter_mappings (first (db/simple-select Card {:where [:= :id card-id]})))))))))
diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj
index 21c16fdb23ef3a956fa8edab682374297c5f1db8..40085e50e2b213c3254c1020d0e2f2eb3252ec74 100644
--- a/test/metabase/events/revision_test.clj
+++ b/test/metabase/events/revision_test.clj
@@ -32,6 +32,8 @@
    :id                     (u/the-id card)
    :made_public_by_id      nil
    :name                   (:name card)
+   :parameters             []
+   :parameter_mappings     []
    :public_uuid            nil
    :cache_ttl              nil
    :query_type             :query
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index c61c192a44016a23518504becea5a1b8490b97a9..6bd68d3484a79203b08e22f817fa36d31e643b6c 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -291,3 +291,72 @@
                clojure.lang.ExceptionInfo
                #"Invalid Field Filter: Field \d+ \"VENUES\"\.\"NAME\" belongs to Database \d+ \"test-data\", but the query is against Database \d+ \"sample-dataset\""
                (db/update! Card card-id bad-card-data))))))))
+
+
+;;; ------------------------------------------ Parameters tests ------------------------------------------
+
+(deftest validate-parameters-test
+  (testing "Should validate Card :parameters when"
+    (testing "creating"
+      (is (thrown-with-msg?
+           clojure.lang.ExceptionInfo
+           #":parameters must be a sequence of maps with String :id key"
+           (mt/with-temp Card [_ {:parameters {:a :b}}])))
+
+     (mt/with-temp Card [card {:parameters [{:id "valid-id"}]}]
+       (is (some? card))))
+
+    (testing "updating"
+      (mt/with-temp Card [{:keys [id]} {:parameters []}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #":parameters must be a sequence of maps with String :id key"
+             (db/update! Card id :parameters [{:id 100}])))
+        (is (some? (db/update! Card id :parameters [{:id "new-valid-id"}])))))))
+
+(deftest normalize-parameters-test
+  (testing ":parameters should get normalized when coming out of the DB"
+    (doseq [[target expected] {[:dimension [:field-id 1000]] [:dimension [:field 1000 nil]]
+                               [:field-id 1000]              [:field 1000 nil]}]
+      (testing (format "target = %s" (pr-str target))
+        (mt/with-temp Card [{card-id :id} {:parameters [{:name   "Category Name"
+                                                         :slug   "category_name"
+                                                         :id     "_CATEGORY_NAME_"
+                                                         :type   "category"
+                                                         :target target}]}]
+          (is (= [{:name   "Category Name"
+                   :slug   "category_name"
+                   :id     "_CATEGORY_NAME_"
+                   :type   :category
+                   :target expected}]
+                 (db/select-one-field :parameters Card :id card-id))))))))
+
+(deftest validate-parameter-mappings-test
+  (testing "Should validate Card :parameter_mappings when"
+    (testing "creating"
+      (is (thrown-with-msg?
+           clojure.lang.ExceptionInfo
+           #":parameter_mappings must be a sequence of maps with String :parameter_id key"
+           (mt/with-temp Card [_ {:parameter_mappings {:a :b}}])))
+
+     (mt/with-temp Card [card {:parameter_mappings [{:parameter_id "valid-id"}]}]
+       (is (some? card))))
+
+    (testing "updating"
+      (mt/with-temp Card [{:keys [id]} {:parameter_mappings []}]
+        (is (thrown-with-msg?
+             clojure.lang.ExceptionInfo
+             #":parameter_mappings must be a sequence of maps with String :parameter_id key"
+             (db/update! Card id :parameter_mappings [{:parameter_id 100}])))
+
+        (is (some? (db/update! Card id :parameter_mappings [{:parameter_id "new-valid-id"}])))))))
+
+(deftest normalize-parameter-mappings-test
+  (testing ":parameter_mappings should get normalized when coming out of the DB"
+    (mt/with-temp Card [{card-id :id} {:parameter_mappings [{:parameter_id "22486e00"
+                                                             :card_id      1
+                                                             :target       [:dimension [:field-id 1]]}]}]
+      (is (= [{:parameter_id "22486e00",
+               :card_id      1,
+               :target       [:dimension [:field 1 nil]]}]
+             (db/select-one-field :parameter_mappings Card :id card-id))))))
diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj
index 5599ee5ee999576d044dfa1f64ee8e069a96ac6c..73495dede6d5c9601342d72f01078b8ecb51be5a 100644
--- a/test/metabase/models/dashboard_test.clj
+++ b/test/metabase/models/dashboard_test.clj
@@ -282,13 +282,13 @@
     (testing "creating"
       (is (thrown-with-msg?
            clojure.lang.ExceptionInfo
-           #":parameters must be a sequence of maps with String :id keys"
+           #":parameters must be a sequence of maps with String :id key"
            (mt/with-temp Dashboard [_ {:parameters {:a :b}}]))))
     (testing "updating"
       (mt/with-temp Dashboard [{:keys [id]} {:parameters []}]
         (is (thrown-with-msg?
              clojure.lang.ExceptionInfo
-             #":parameters must be a sequence of maps with String :id keys"
+             #":parameters must be a sequence of maps with String :id key"
              (db/update! Dashboard id :parameters [{:id 100}])))))))
 
 (deftest normalize-parameters-test