diff --git a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
index 9495a4b6b826716ab7364ffa57689153fd1d61a0..fbf43f06500f13c899ac5f4698ebb0a599bc26c5 100644
--- a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
@@ -66,6 +66,7 @@ export default ({ question, clicked }) => {
           openActionParametersModal({
             emitterId: emitterId,
             props: {
+              description: settings["user_input_modal.description"],
               missingParameters,
               onSubmit: filledMissingParameters =>
                 executeRowAction({
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..33b0b21297b17005f13002122aa56f3ce788db5e
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.styled.tsx
@@ -0,0 +1,20 @@
+import styled from "@emotion/styled";
+import { color } from "metabase/lib/colors";
+
+export const TextArea = styled.textarea`
+  min-width: 100%;
+  max-width: 100%;
+  min-height: 9rem;
+  padding: 8px;
+
+  border: 1px solid ${color("border")};
+  border-radius: 6px;
+
+  transition: border 0.3s;
+  outline: none;
+
+  &:hover,
+  &:focus {
+    border-color: ${color("brand")};
+  }
+`;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a0e136ebe74e4b40f9ca102273e540d7a6d515e1
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/ChartSettingsTextArea.tsx
@@ -0,0 +1,20 @@
+import React, { useCallback } from "react";
+import { TextArea } from "./ChartSettingsTextArea.styled";
+
+interface Props {
+  value: string;
+  onChange: (value: string) => void;
+}
+
+function ChartSettingsTextArea({ value, onChange }: Props) {
+  const handleChange = useCallback(
+    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+      onChange(e.target.value);
+    },
+    [onChange],
+  );
+
+  return <TextArea value={value} onChange={handleChange} />;
+}
+
+export default ChartSettingsTextArea;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..45138a1668d0dbd178a4018590422a9a7a90d179
--- /dev/null
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTextArea/index.ts
@@ -0,0 +1 @@
+export { default } from "./ChartSettingsTextArea";
diff --git a/frontend/src/metabase/visualizations/lib/settings.js b/frontend/src/metabase/visualizations/lib/settings.js
index 405cfc00d633dc56427a1cdf8a9cd462e577a94f..65b0fef1089757f8d7319107276f73d243f90b23 100644
--- a/frontend/src/metabase/visualizations/lib/settings.js
+++ b/frontend/src/metabase/visualizations/lib/settings.js
@@ -13,12 +13,14 @@ import ChartSettingFieldsPicker from "metabase/visualizations/components/setting
 import ChartSettingFieldsPartition from "metabase/visualizations/components/settings/ChartSettingFieldsPartition";
 import ChartSettingColorPicker from "metabase/visualizations/components/settings/ChartSettingColorPicker";
 import ChartSettingColorsPicker from "metabase/visualizations/components/settings/ChartSettingColorsPicker";
+import ChartSettingsTextArea from "metabase/visualizations/components/settings/ChartSettingsTextArea";
 
 import * as MetabaseAnalytics from "metabase/lib/analytics";
 
 const WIDGETS = {
   input: ChartSettingInput,
   inputGroup: ChartSettingInputGroup,
+  text: ChartSettingsTextArea,
   number: ChartSettingInputNumeric,
   radio: ChartSettingRadio,
   select: ChartSettingSelect,
diff --git a/frontend/src/metabase/writeback/components/ActionButtonViz.tsx b/frontend/src/metabase/writeback/components/ActionButtonViz.tsx
index 52eccb9ec7e673e5916ff66124cf2937a4bafdd8..a5c80eb780c88bbf628d30aff36a6faa253cb75f 100644
--- a/frontend/src/metabase/writeback/components/ActionButtonViz.tsx
+++ b/frontend/src/metabase/writeback/components/ActionButtonViz.tsx
@@ -1,9 +1,11 @@
 import React, { useCallback, useMemo } from "react";
 import { t } from "ttag";
-import cx from "classnames";
+import _ from "underscore";
 
 import Button from "metabase/core/components/Button";
 
+import { useConfirmation } from "metabase/hooks/use-confirmation";
+
 import { DashboardWithCards } from "metabase-types/types/Dashboard";
 import { VisualizationProps } from "metabase-types/types/Visualization";
 
@@ -50,6 +52,49 @@ const ACTIONS_VIZ_DEFINITION = {
         ],
       },
     },
+    "confirmation_modal.is_required": {
+      section: t`Confirmation`,
+      title: t`Require confirmation`,
+      widget: "toggle",
+      default: false,
+    },
+    "confirmation_modal.title": {
+      section: t`Confirmation`,
+      title: t`Confirmation modal title`,
+      widget: "input",
+      default: t`Are you sure?`,
+      getHidden: (_: any, settings: any) =>
+        !settings["confirmation_modal.is_required"],
+    },
+    "confirmation_modal.description": {
+      section: t`Confirmation`,
+      title: t`Confirmation modal description`,
+      widget: "input",
+      default: t`This cannot be undone`,
+      getHidden: (_: any, settings: any) =>
+        !settings["confirmation_modal.is_required"],
+    },
+    "confirmation_modal.submit.title": {
+      section: t`Confirmation`,
+      title: t`Submit button title`,
+      widget: "input",
+      default: t`Confirm`,
+      getHidden: (_: any, settings: any) =>
+        !settings["confirmation_modal.is_required"],
+    },
+    "confirmation_modal.cancel.title": {
+      section: t`Confirmation`,
+      title: t`Cancel button title`,
+      widget: "input",
+      default: t`Cancel`,
+      getHidden: (_: any, settings: any) =>
+        !settings["confirmation_modal.is_required"],
+    },
+    "user_input_modal.description": {
+      section: t`User input modal`,
+      title: t`Description`,
+      widget: "text",
+    },
   },
 };
 
@@ -58,14 +103,29 @@ interface ActionButtonVizProps extends VisualizationProps {
 }
 
 function ActionButtonViz({
-  isSettings,
   settings,
   getExtraDataForClick,
   onVisualizationClick,
 }: ActionButtonVizProps) {
+  const { modalContent: confirmationModal, show: requestConfirmation } =
+    useConfirmation();
+
   const label = settings["button.label"];
   const variant = settings["button.variant"];
 
+  const confirmationModalSettings = useMemo(() => {
+    const result: Record<string, any> = {};
+
+    Object.keys(settings).forEach(key => {
+      if (key.startsWith("confirmation_modal.")) {
+        const shortKey = key.replace("confirmation_modal.", "");
+        result[shortKey] = settings[key];
+      }
+    });
+
+    return result;
+  }, [settings]);
+
   const variantProps: any = {};
   if (variant !== "default") {
     variantProps[variant] = true;
@@ -83,9 +143,9 @@ function ActionButtonViz({
     [clicked, getExtraDataForClick],
   );
 
-  const onClick = useCallback(
+  const handleTriggerAction = useCallback(
     (e: React.MouseEvent) => {
-      onVisualizationClick({
+      return onVisualizationClick({
         ...clicked,
         extraData,
         element: e.currentTarget as HTMLElement,
@@ -94,16 +154,43 @@ function ActionButtonViz({
     [clicked, extraData, onVisualizationClick],
   );
 
+  const handleActionRequiringConfirmation = useCallback(
+    (e: React.MouseEvent) => {
+      requestConfirmation({
+        title: confirmationModalSettings.title,
+        message: confirmationModalSettings.description,
+        confirmButtonText: confirmationModalSettings["submit.title"],
+        cancelButtonText: confirmationModalSettings["cancel.title"],
+        onConfirm: async () => handleTriggerAction(e),
+      });
+    },
+    [confirmationModalSettings, requestConfirmation, handleTriggerAction],
+  );
+
+  const onClick = useCallback(
+    (e: React.MouseEvent) => {
+      if (confirmationModalSettings.is_required) {
+        handleActionRequiringConfirmation(e);
+      } else {
+        handleTriggerAction(e);
+      }
+    },
+    [
+      confirmationModalSettings,
+      handleTriggerAction,
+      handleActionRequiringConfirmation,
+    ],
+  );
+
   return (
-    <Button
-      className={cx({
-        "full-height": !isSettings,
-      })}
-      onClick={onClick}
-      {...variantProps}
-    >
-      {label}
-    </Button>
+    <>
+      <div className="flex full-height full-width layout-centered px1">
+        <Button onClick={onClick} {...variantProps} fullWidth>
+          {label}
+        </Button>
+      </div>
+      {confirmationModal}
+    </>
   );
 }
 
diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fd5e545bc2bb9df8485fee8cad6befae30140622
--- /dev/null
+++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.styled.tsx
@@ -0,0 +1,7 @@
+import styled from "@emotion/styled";
+
+import Markdown from "metabase/core/components/Markdown";
+
+export const FormDescription = styled(Markdown)`
+  margin-bottom: 1rem;
+`;
diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx
similarity index 81%
rename from frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx
rename to frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx
index 1f9a2eaab54807c856f4968edef42eb01dd5bb10..7bb58745ba9295f1b04d16c740c3228a2ec1a9a8 100644
--- a/frontend/src/metabase/writeback/containers/ActionParametersInputForm.tsx
+++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/ActionParametersInputForm.tsx
@@ -2,15 +2,18 @@ import React, { useCallback, useMemo } from "react";
 import { connect } from "react-redux";
 import { t } from "ttag";
 
+import { useDataAppContext } from "metabase/writeback/containers/DataAppContext";
 import {
   getActionTemplateTagType,
   getActionParameterType,
 } from "metabase/writeback/utils";
 
-import Form from "metabase/containers/Form";
+import RootForm from "metabase/containers/Form";
 import { TemplateTag } from "metabase-types/types/Query";
 import { Parameter, ParameterId } from "metabase-types/types/Parameter";
 
+import { FormDescription } from "./ActionParametersInputForm.styled";
+
 type MappedParameters = Record<
   string,
   { type: string; value: string | number }
@@ -18,10 +21,12 @@ type MappedParameters = Record<
 
 interface Props {
   missingParameters: TemplateTag[] | Parameter[];
+  description?: string;
   onSubmit: (parameters: MappedParameters) => { type: string; payload: any };
   onSubmitSuccess: () => void;
   dispatch: (action: any) => void;
 }
+
 function isTemplateTag(
   tagOrParameter: TemplateTag | Parameter,
 ): tagOrParameter is TemplateTag {
@@ -103,11 +108,14 @@ function formatParametersBeforeSubmit(
 }
 
 function ActionParametersInputForm({
+  description,
   missingParameters,
   dispatch,
   onSubmit,
   onSubmitSuccess,
 }: Props) {
+  const dataAppContext = useDataAppContext();
+
   const form = useMemo(() => {
     return {
       fields: missingParameters.map(tagOrParameter => {
@@ -137,7 +145,23 @@ function ActionParametersInputForm({
     [missingParameters, onSubmit, onSubmitSuccess, dispatch],
   );
 
-  return <Form form={form} onSubmit={handleSubmit} submitTitle={t`Execute`} />;
+  return (
+    <RootForm form={form} onSubmit={handleSubmit} submitTitle={t`Execute`}>
+      {({ Form, FormField, FormFooter, formFields }: any) => (
+        <Form>
+          {description && (
+            <FormDescription>
+              {dataAppContext.format(description)}
+            </FormDescription>
+          )}
+          {formFields.map((field: any) => (
+            <FormField key={field.name} name={field.name} />
+          ))}
+          <FormFooter />
+        </Form>
+      )}
+    </RootForm>
+  );
 }
 
 export default connect()(ActionParametersInputForm);
diff --git a/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..943e8c4a1cbeb3043d2de58793477a80bc55fd28
--- /dev/null
+++ b/frontend/src/metabase/writeback/containers/ActionParametersInputForm/index.ts
@@ -0,0 +1 @@
+export { default } from "./ActionParametersInputForm";