diff --git a/frontend/src/metabase-types/api/actions.ts b/frontend/src/metabase-types/api/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bdf3fc13f56c2e936407326fb9f30137889e0897
--- /dev/null
+++ b/frontend/src/metabase-types/api/actions.ts
@@ -0,0 +1,163 @@
+import type { ParameterTarget } from "metabase-types/types/Parameter";
+import type { Parameter, ParameterId } from "./parameters";
+import type { NativeDatasetQuery } from "./query";
+
+export interface WritebackParameter extends Parameter {
+  target: ParameterTarget;
+}
+
+export type WritebackActionType = "http" | "query" | "implicit";
+
+export type WritebackActionId = number;
+
+export interface WritebackActionBase {
+  id: WritebackActionId;
+  model_id: number;
+  name: string;
+  description: string | null;
+  parameters: WritebackParameter[];
+  visualization_settings?: ActionFormSettings;
+  updated_at: string;
+  created_at: string;
+}
+
+export interface QueryAction {
+  type: "query";
+  dataset_query: NativeDatasetQuery;
+}
+
+export interface ImplicitQueryAction {
+  type: "implicit";
+  kind: "row/create" | "row/update" | "row/delete";
+}
+
+export interface HttpAction {
+  type: "http";
+  template: HttpActionTemplate;
+  response_handle: string | null;
+  error_handle: string | null;
+}
+
+export type HttpActionResponseHandle = any;
+export type HttpActionErrorHandle = any;
+
+export interface HttpActionTemplate {
+  method: string;
+  url: string;
+  body: string;
+  headers: string;
+  parameters: Record<ParameterId, Parameter>;
+  parameter_mappings: Record<ParameterId, ParameterTarget>;
+}
+
+export type WritebackQueryAction = WritebackActionBase & QueryAction;
+export type WritebackImplicitQueryAction = WritebackActionBase &
+  ImplicitQueryAction;
+export type WritebackHttpAction = WritebackActionBase & HttpAction;
+export type WritebackAction = WritebackActionBase &
+  (QueryAction | ImplicitQueryAction | HttpAction);
+
+export type ParameterMappings = Record<ParameterId, ParameterTarget>;
+
+export type ParametersForActionExecution = {
+  [id: ParameterId]: string | number | null;
+};
+
+export type ActionFormInitialValues = ParametersForActionExecution;
+
+export interface ActionFormSubmitResult {
+  success: boolean;
+  message?: string;
+  error?: string;
+}
+
+export type OnSubmitActionForm = (
+  parameters: ParametersForActionExecution,
+) => Promise<ActionFormSubmitResult>;
+
+// Action Forms
+
+export type ActionDisplayType = "form" | "button";
+export type FieldType = "string" | "number" | "date" | "category";
+
+export type DateInputType = "date" | "time" | "datetime";
+
+// these types are saved in visualization_settings
+export type InputSettingType =
+  | DateInputType
+  | "string"
+  | "text"
+  | "number"
+  | "select"
+  | "radio"
+  | "boolean"
+  | "category";
+
+// these types get passed to the input components
+export type InputComponentType =
+  | "text"
+  | "textarea"
+  | "number"
+  | "boolean"
+  | "select"
+  | "radio"
+  | "date"
+  | "time"
+  | "datetime-local"
+  | "category";
+
+export type Size = "small" | "medium" | "large";
+
+export type DateRange = [string, string];
+export type NumberRange = [number, number];
+
+export interface FieldSettings {
+  id: string;
+  name: string;
+  title: string;
+  order: number;
+  description?: string | null;
+  placeholder?: string;
+  fieldType: FieldType;
+  inputType: InputSettingType;
+  required: boolean;
+  defaultValue?: string | number;
+  hidden: boolean;
+  range?: DateRange | NumberRange;
+  valueOptions?: (string | number)[];
+  width?: Size;
+  height?: number;
+  hasSearch?: boolean;
+}
+
+export type FieldSettingsMap = Record<ParameterId, FieldSettings>;
+export interface ActionFormSettings {
+  name?: string;
+  type: ActionDisplayType;
+  description?: string;
+  fields: FieldSettingsMap;
+  submitButtonLabel?: string;
+  submitButtonColor?: string;
+  confirmMessage?: string;
+  successMessage?: string;
+  errorMessage?: string;
+}
+
+export type ActionFormOption = {
+  name: string | number;
+  value: string | number;
+};
+
+export type ActionFormFieldProps = {
+  name: string;
+  title: string;
+  description?: string;
+  placeholder?: string;
+  type: InputComponentType;
+  optional?: boolean;
+  options?: ActionFormOption[];
+};
+
+export type ActionFormProps = {
+  fields: ActionFormFieldProps[];
+};
diff --git a/frontend/src/metabase-types/api/card.ts b/frontend/src/metabase-types/api/card.ts
index 65c87ac2a47953d7cb22066f61f14599206545c4..5fede8011f09517cbbf5b97be86633d98ec10eec 100644
--- a/frontend/src/metabase-types/api/card.ts
+++ b/frontend/src/metabase-types/api/card.ts
@@ -1,3 +1,4 @@
+import type { DatabaseId } from "./database";
 import type { Field } from "./field";
 import type { DatasetQuery } from "./query";
 
@@ -7,6 +8,7 @@ export interface Card extends UnsavedCard {
   name: string;
   description: string | null;
   dataset: boolean;
+  database_id?: DatabaseId;
   can_write: boolean;
   cache_ttl: number | null;
   query_average_duration?: number | null;
diff --git a/frontend/src/metabase-types/api/index.ts b/frontend/src/metabase-types/api/index.ts
index ca5b38d3b8af693b24989f4410abaf050bcfc79b..db49f2a142f63bcd5cfdf447dd1288795b45d6a8 100644
--- a/frontend/src/metabase-types/api/index.ts
+++ b/frontend/src/metabase-types/api/index.ts
@@ -1,3 +1,4 @@
+export * from "./actions";
 export * from "./activity";
 export * from "./automagic-dashboards";
 export * from "./bookmark";
diff --git a/frontend/src/metabase-types/api/mocks/actions.ts b/frontend/src/metabase-types/api/mocks/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca1def78ae9332d1f8da101b61eb5e94d790ca89
--- /dev/null
+++ b/frontend/src/metabase-types/api/mocks/actions.ts
@@ -0,0 +1,54 @@
+import {
+  WritebackParameter,
+  WritebackQueryAction,
+  WritebackImplicitQueryAction,
+} from "metabase-types/api";
+import { createMockNativeDatasetQuery } from "./query";
+import { createMockParameter } from "./parameters";
+
+export const createMockActionParameter = (
+  opts?: Partial<WritebackParameter>,
+): WritebackParameter => ({
+  target: opts?.target || ["variable", ["template-tag", "id"]],
+  ...createMockParameter({
+    id: "id",
+    name: "ID",
+    type: "type/Integer",
+    slug: "id",
+    ...opts,
+  }),
+});
+
+export const createMockQueryAction = ({
+  dataset_query = createMockNativeDatasetQuery(),
+  ...opts
+}: Partial<WritebackQueryAction> = {}): WritebackQueryAction => {
+  return {
+    id: 1,
+    dataset_query,
+    name: "Query Action Mock",
+    description: null,
+    model_id: 1,
+    parameters: [],
+    created_at: new Date().toISOString(),
+    updated_at: new Date().toISOString(),
+    ...opts,
+    type: "query",
+  };
+};
+
+export const createMockImplicitQueryAction = (
+  options: Partial<WritebackImplicitQueryAction>,
+): WritebackImplicitQueryAction => ({
+  id: 1,
+  kind: "row/create",
+  name: "",
+  description: "",
+  model_id: 1,
+  parameters: [],
+  visualization_settings: undefined,
+  created_at: new Date().toISOString(),
+  updated_at: new Date().toISOString(),
+  ...options,
+  type: "implicit",
+});
diff --git a/frontend/src/metabase-types/api/mocks/index.ts b/frontend/src/metabase-types/api/mocks/index.ts
index 65286226c84a54c8156161a8fa05f4481c585e50..8384fb7853aac9a1485881fdc37f5737fd7c9fda 100644
--- a/frontend/src/metabase-types/api/mocks/index.ts
+++ b/frontend/src/metabase-types/api/mocks/index.ts
@@ -1,3 +1,4 @@
+export * from "./actions";
 export * from "./activity";
 export * from "./automagic-dashboards";
 export * from "./card";
diff --git a/frontend/src/metabase-types/api/parameters.ts b/frontend/src/metabase-types/api/parameters.ts
index 618b9e4783474efa0f249b62c23c0fc72726e757..9c457b35f48c83125b61e2abed4c5a91f8dd1db8 100644
--- a/frontend/src/metabase-types/api/parameters.ts
+++ b/frontend/src/metabase-types/api/parameters.ts
@@ -35,10 +35,12 @@ export type ActionParameterValue = string | number;
 export interface Parameter {
   id: ParameterId;
   name: string;
+  "display-name"?: string;
   type: string;
   slug: string;
   sectionId?: string;
   default?: any;
+  required?: boolean;
   filteringParameters?: ParameterId[];
   isMultiSelect?: boolean;
   value?: any;
diff --git a/frontend/src/metabase-types/store/entities.ts b/frontend/src/metabase-types/store/entities.ts
index 5726a5f5b8f81543bcaab581262f059ec6cb4cc4..3c0c4b7b6f096201c782dbf14dec826e7841665c 100644
--- a/frontend/src/metabase-types/store/entities.ts
+++ b/frontend/src/metabase-types/store/entities.ts
@@ -9,9 +9,12 @@ import {
   Table,
   User,
   UserId,
+  WritebackAction,
+  WritebackActionId,
 } from "metabase-types/api";
 
 export interface EntitiesState {
+  actions?: Record<WritebackActionId, WritebackAction>;
   collections?: Record<CollectionId, Collection>;
   databases?: Record<number, Database>;
   fields?: Record<FieldId, Field>;
diff --git a/frontend/src/metabase/actions/utils.ts b/frontend/src/metabase/actions/utils.ts
index b052e29f9fc6225a34e60c4858f3afbda9b1ed02..1ddf786c0f08631bc52f21929f27f4097900c2b1 100644
--- a/frontend/src/metabase/actions/utils.ts
+++ b/frontend/src/metabase/actions/utils.ts
@@ -1,7 +1,39 @@
-import type { Database } from "metabase-types/api";
+import type {
+  ActionFormSettings,
+  Database,
+  FieldSettings,
+} from "metabase-types/api";
 
 export const checkDatabaseSupportsActions = (database: Database) =>
   database.features.includes("actions");
 
 export const checkDatabaseActionsEnabled = (database: Database) =>
   !!database.settings?.["database-enable-actions"];
+
+export const getDefaultFormSettings = (
+  overrides: Partial<ActionFormSettings> = {},
+): ActionFormSettings => ({
+  name: "",
+  type: "button",
+  description: "",
+  fields: {},
+  confirmMessage: "",
+  ...overrides,
+});
+
+export const getDefaultFieldSettings = (
+  overrides: Partial<FieldSettings> = {},
+): FieldSettings => ({
+  id: "",
+  name: "",
+  title: "",
+  description: "",
+  placeholder: "",
+  order: 999,
+  fieldType: "string",
+  inputType: "string",
+  required: true,
+  hidden: false,
+  width: "medium",
+  ...overrides,
+});
diff --git a/frontend/src/metabase/entities/actions/actions.ts b/frontend/src/metabase/entities/actions/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..041bbfa548d7d03df2066a0082c962fee8e50aa9
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/actions.ts
@@ -0,0 +1,211 @@
+import { t } from "ttag";
+import { createEntity } from "metabase/lib/entities";
+
+import type {
+  ActionFormSettings,
+  ImplicitQueryAction,
+  WritebackActionBase,
+  WritebackAction,
+} from "metabase-types/api";
+import type { Dispatch } from "metabase-types/store";
+
+import { ActionsApi } from "metabase/services";
+
+import {
+  removeOrphanSettings,
+  addMissingSettings,
+  setParameterTypesFromFieldSettings,
+  setTemplateTagTypesFromFieldSettings,
+} from "metabase/entities/actions/utils";
+import type Question from "metabase-lib/Question";
+import { saveForm, updateForm } from "./forms";
+
+export type ActionParams = {
+  id?: WritebackAction["id"];
+  name: WritebackAction["name"];
+  type?: WritebackAction["type"];
+  kind?: ImplicitQueryAction["kind"];
+  description?: WritebackAction["description"];
+  model_id: WritebackAction["model_id"];
+  question?: Question;
+  formSettings?: ActionFormSettings;
+};
+
+interface BaseCreateActionParams {
+  model_id: WritebackActionBase["model_id"];
+  name: WritebackActionBase["name"];
+  description: WritebackActionBase["description"];
+  parameters: WritebackActionBase["parameters"];
+}
+
+interface UpdateActionParams {
+  id: WritebackActionBase["id"];
+}
+
+export interface CreateQueryActionOptions extends BaseCreateActionParams {
+  question: Question;
+  formSettings: ActionFormSettings;
+}
+
+export type UpdateQueryActionOptions = CreateQueryActionOptions &
+  UpdateActionParams;
+
+export interface CreateImplicitActionOptions extends BaseCreateActionParams {
+  kind: ImplicitQueryAction["kind"];
+}
+
+export type UpdateImplicitActionOptions = CreateImplicitActionOptions &
+  UpdateActionParams;
+
+function cleanUpQueryAction(
+  question: Question,
+  formSettings: ActionFormSettings,
+) {
+  question = setTemplateTagTypesFromFieldSettings(formSettings, question);
+
+  const parameters = setParameterTypesFromFieldSettings(
+    formSettings,
+    question.parameters(),
+  );
+
+  const visualization_settings = removeOrphanSettings(
+    addMissingSettings(formSettings, parameters),
+    parameters,
+  );
+
+  return {
+    dataset_query: question.datasetQuery(),
+    parameters,
+    visualization_settings,
+  };
+}
+
+function createQueryAction({
+  question,
+  formSettings,
+  ...action
+}: CreateQueryActionOptions) {
+  const { dataset_query, parameters, visualization_settings } =
+    cleanUpQueryAction(question, formSettings);
+
+  return ActionsApi.create({
+    ...action,
+    type: "query",
+    dataset_query,
+    database_id: dataset_query.database,
+    parameters,
+    visualization_settings,
+  });
+}
+
+function updateQueryAction({
+  question,
+  formSettings,
+  ...action
+}: UpdateQueryActionOptions) {
+  const { dataset_query, parameters, visualization_settings } =
+    cleanUpQueryAction(question, formSettings);
+
+  return ActionsApi.update({
+    ...action,
+    dataset_query,
+    parameters,
+    visualization_settings,
+  });
+}
+
+function createImplicitAction(action: CreateImplicitActionOptions) {
+  return Actions.actions.create({
+    ...action,
+    type: "implicit",
+  });
+}
+
+function updateImplicitAction(action: UpdateImplicitActionOptions) {
+  return Actions.actions.update({
+    ...action,
+    type: "implicit",
+  });
+}
+
+const defaultImplicitActionCreateOptions = {
+  insert: true,
+  update: true,
+  delete: true,
+};
+
+const enableImplicitActionsForModel =
+  async (modelId: number, options = defaultImplicitActionCreateOptions) =>
+  async (dispatch: Dispatch) => {
+    const requests = [];
+
+    if (options.insert) {
+      requests.push(
+        ActionsApi.create({
+          name: t`Create`,
+          type: "implicit",
+          kind: "row/create",
+          model_id: modelId,
+        }),
+      );
+    }
+
+    if (options.update) {
+      requests.push(
+        ActionsApi.create({
+          name: t`Update`,
+          type: "implicit",
+          kind: "row/update",
+          model_id: modelId,
+        }),
+      );
+    }
+
+    if (options.delete) {
+      requests.push(
+        ActionsApi.create({
+          name: t`Delete`,
+          type: "implicit",
+          kind: "row/delete",
+          model_id: modelId,
+        }),
+      );
+    }
+
+    await Promise.all(requests);
+
+    dispatch(Actions.actions.invalidateLists());
+  };
+
+const Actions = createEntity({
+  name: "actions",
+  nameOne: "action",
+  path: "/api/action",
+  api: {
+    create: (
+      params: CreateQueryActionOptions | CreateImplicitActionOptions,
+    ) => {
+      if ("question" in params) {
+        return createQueryAction(params);
+      }
+      return createImplicitAction(params);
+    },
+    update: (
+      params: UpdateQueryActionOptions | UpdateImplicitActionOptions,
+    ) => {
+      if ("question" in params) {
+        return updateQueryAction(params);
+      }
+      return updateImplicitAction(params);
+    },
+  },
+  actions: {
+    enableImplicitActionsForModel,
+  },
+  forms: {
+    saveForm,
+    updateForm,
+  },
+});
+
+export default Actions;
diff --git a/frontend/src/metabase/entities/actions/constants.ts b/frontend/src/metabase/entities/actions/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5eea11262dfa6a8df8c44546ee0bf1194b6047a9
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/constants.ts
@@ -0,0 +1,30 @@
+import type { ParameterType } from "metabase-types/api";
+import type { TemplateTagType } from "metabase-types/types/Query";
+
+interface FieldTypeMap {
+  [key: string]: ParameterType;
+}
+
+interface TagTypeMap {
+  [key: string]: TemplateTagType;
+}
+
+export const fieldTypeToParameterTypeMap: FieldTypeMap = {
+  string: "string/=",
+  category: "string/=",
+  number: "number/=",
+};
+
+export const dateTypeToParameterTypeMap: FieldTypeMap = {
+  date: "date/single",
+  datetime: "date/single",
+  monthyear: "date/month-year",
+  quarteryear: "date/quarter-year",
+};
+
+export const fieldTypeToTagTypeMap: TagTypeMap = {
+  string: "text",
+  category: "text",
+  number: "number",
+  date: "date",
+};
diff --git a/frontend/src/metabase/entities/actions/forms.ts b/frontend/src/metabase/entities/actions/forms.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bab7fceb86db9deb7a73544c1695b13f624e6885
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/forms.ts
@@ -0,0 +1,41 @@
+import { t } from "ttag";
+
+const getFormFields = (formAction: "create" | "update") => [
+  {
+    name: "name",
+    title: t`Name`,
+    placeholder: t`My new fantastic action`,
+    autoFocus: true,
+    validate: (name: string) =>
+      (!name && t`Name is required`) ||
+      (name && name.length > 100 && t`Name must be 100 characters or less`),
+  },
+  {
+    name: "description",
+    title: t`Description`,
+    type: "text",
+    placeholder: t`It's optional but oh, so helpful`,
+    normalize: (description: string) => description || null, // expected to be nil or non-empty string
+  },
+  {
+    name: "model_id",
+    title: t`Model it's saved in`,
+    type: formAction === "create" ? "model" : "hidden",
+  },
+  {
+    name: "question",
+    type: "hidden",
+  },
+  {
+    name: "formSettings",
+    type: "hidden",
+  },
+];
+
+export const saveForm = {
+  fields: getFormFields("create"),
+};
+
+export const updateForm = {
+  fields: getFormFields("update"),
+};
diff --git a/frontend/src/metabase/entities/actions/index.ts b/frontend/src/metabase/entities/actions/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b6d262f57b87efc344fab769a520242d6ec197cc
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/index.ts
@@ -0,0 +1,2 @@
+export { default } from "./actions";
+export * from "./actions";
diff --git a/frontend/src/metabase/entities/actions/utils.ts b/frontend/src/metabase/entities/actions/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e705dfcddaecfb17f4d28076c46dd91be1ceaadb
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/utils.ts
@@ -0,0 +1,107 @@
+import _ from "underscore";
+
+import { getDefaultFieldSettings } from "metabase/actions/utils";
+
+import type {
+  ActionFormSettings,
+  FieldType,
+  InputSettingType,
+  Parameter,
+  ParameterType,
+} from "metabase-types/api";
+import type { TemplateTag, TemplateTagType } from "metabase-types/types/Query";
+import type NativeQuery from "metabase-lib/queries/NativeQuery";
+import type Question from "metabase-lib/Question";
+
+import {
+  fieldTypeToParameterTypeMap,
+  dateTypeToParameterTypeMap,
+  fieldTypeToTagTypeMap,
+} from "./constants";
+
+export const removeOrphanSettings = (
+  settings: ActionFormSettings,
+  parameters: Parameter[],
+): ActionFormSettings => {
+  const parameterIds = parameters.map(p => p.id);
+  return {
+    ...settings,
+    fields: _.pick(settings.fields, parameterIds),
+  };
+};
+
+export const addMissingSettings = (
+  settings: ActionFormSettings,
+  parameters: Parameter[],
+): ActionFormSettings => {
+  const parameterIds = parameters.map(p => p.id);
+  const fieldIds = Object.keys(settings.fields);
+  const missingIds = _.difference(parameterIds, fieldIds);
+
+  if (!missingIds.length) {
+    return settings;
+  }
+
+  return {
+    ...settings,
+    fields: {
+      ...settings.fields,
+      ...Object.fromEntries(
+        missingIds.map(id => [id, getDefaultFieldSettings({ id })]),
+      ),
+    },
+  };
+};
+
+const getParameterTypeFromFieldSettings = (
+  fieldType: FieldType,
+  inputType: InputSettingType,
+): ParameterType => {
+  if (fieldType === "date") {
+    return dateTypeToParameterTypeMap[inputType] ?? "date/single";
+  }
+
+  return fieldTypeToParameterTypeMap[fieldType] ?? "string/=";
+};
+
+const getTagTypeFromFieldSettings = (fieldType: FieldType): TemplateTagType => {
+  return fieldTypeToTagTypeMap[fieldType] ?? "text";
+};
+
+export const setParameterTypesFromFieldSettings = (
+  settings: ActionFormSettings,
+  parameters: Parameter[],
+): Parameter[] => {
+  const fields = settings.fields;
+  return parameters.map(parameter => {
+    const field = fields[parameter.id];
+    return {
+      ...parameter,
+      type: field
+        ? getParameterTypeFromFieldSettings(field.fieldType, field.inputType)
+        : "string/=",
+    };
+  });
+};
+
+export const setTemplateTagTypesFromFieldSettings = (
+  settings: ActionFormSettings,
+  question: Question,
+): Question => {
+  const fields = settings.fields;
+
+  (question.query() as NativeQuery)
+    .templateTagsWithoutSnippets()
+    .forEach((tag: TemplateTag) => {
+      question = question.setQuery(
+        (question.query() as NativeQuery).setTemplateTag(tag.name, {
+          ...tag,
+          type: getTagTypeFromFieldSettings(
+            fields[tag.id]?.fieldType ?? "string",
+          ),
+        }),
+      );
+    });
+
+  return question;
+};
diff --git a/frontend/src/metabase/entities/actions/utils.unit.spec.ts b/frontend/src/metabase/entities/actions/utils.unit.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eda385c1f80e827fce9535f7de23a75cfb77a711
--- /dev/null
+++ b/frontend/src/metabase/entities/actions/utils.unit.spec.ts
@@ -0,0 +1,214 @@
+import {
+  getDefaultFormSettings,
+  getDefaultFieldSettings,
+} from "metabase/actions/utils";
+
+import type { Parameter } from "metabase-types/api";
+import type { NativeDatasetQuery } from "metabase-types/types/Card";
+import type { TemplateTagType } from "metabase-types/types/Query";
+import { getUnsavedNativeQuestion } from "metabase-lib/mocks";
+
+import {
+  removeOrphanSettings,
+  setParameterTypesFromFieldSettings,
+  setTemplateTagTypesFromFieldSettings,
+} from "./utils";
+
+const createQuestionWithTemplateTags = (tagType: TemplateTagType) =>
+  getUnsavedNativeQuestion({
+    dataset_query: {
+      type: "native",
+      database: 1,
+      native: {
+        query:
+          "INSERT INTO products (name, price) VALUES ({{name}}, {{price}});",
+        "template-tags": {
+          name: {
+            id: "aaa",
+            name: "name",
+            "display-name": "Name",
+            type: tagType,
+          },
+          price: {
+            id: "bbb",
+            name: "price",
+            "display-name": "Price",
+            type: tagType,
+          },
+        },
+      },
+    },
+  });
+
+describe("entities > actions > utils", () => {
+  describe("removeOrphanSettings", () => {
+    it("should remove orphan settings", () => {
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings(),
+          bbb: getDefaultFieldSettings(),
+          ccc: getDefaultFieldSettings(),
+        },
+      });
+
+      const parameters = [
+        { id: "aaa", name: "foo" },
+        { id: "ccc", name: "bar" },
+      ] as Parameter[];
+
+      const result = removeOrphanSettings(formSettings, parameters);
+
+      expect(result.name).toEqual("test form");
+      expect(result.fields).toHaveProperty("aaa");
+      expect(result.fields).toHaveProperty("ccc");
+      expect(result.fields).not.toHaveProperty("bbb");
+    });
+
+    it("should leave non-orphan settings intact", () => {
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings(),
+          bbb: getDefaultFieldSettings(),
+          ccc: getDefaultFieldSettings(),
+        },
+      });
+
+      const parameters = [
+        { id: "aaa", name: "foo" },
+        { id: "bbb", name: "foo" },
+        { id: "ccc", name: "bar" },
+      ] as Parameter[];
+
+      const result = removeOrphanSettings(formSettings, parameters);
+
+      expect(result.name).toEqual("test form");
+      expect(result.fields).toHaveProperty("aaa");
+      expect(result.fields).toHaveProperty("bbb");
+      expect(result.fields).toHaveProperty("ccc");
+    });
+  });
+
+  describe("setParameterTypesFromFieldSettings", () => {
+    it("should set string parameter types", () => {
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings({ fieldType: "string" }),
+          bbb: getDefaultFieldSettings({ fieldType: "string" }),
+          ccc: getDefaultFieldSettings({ fieldType: "string" }),
+        },
+      });
+
+      const parameters = [
+        { id: "aaa", name: "foo", type: "number/=" },
+        { id: "bbb", name: "foo", type: "number/=" },
+        { id: "ccc", name: "bar", type: "number/=" },
+      ] as Parameter[];
+
+      const newParams = setParameterTypesFromFieldSettings(
+        formSettings,
+        parameters,
+      );
+
+      newParams.forEach(param => expect(param.type).toEqual("string/="));
+    });
+
+    it("should set number parameter types", () => {
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings({ fieldType: "number" }),
+          bbb: getDefaultFieldSettings({ fieldType: "number" }),
+          ccc: getDefaultFieldSettings({ fieldType: "number" }),
+        },
+      });
+
+      const parameters = [
+        { id: "aaa", name: "foo", type: "string/=" },
+        { id: "bbb", name: "foo", type: "string/=" },
+        { id: "ccc", name: "bar", type: "string/=" },
+      ] as Parameter[];
+
+      const newParams = setParameterTypesFromFieldSettings(
+        formSettings,
+        parameters,
+      );
+
+      newParams.forEach(param => expect(param.type).toEqual("number/="));
+    });
+
+    it("should set date parameter types", () => {
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings({ fieldType: "date" }),
+          bbb: getDefaultFieldSettings({ fieldType: "date" }),
+          ccc: getDefaultFieldSettings({ fieldType: "date" }),
+        },
+      });
+
+      const parameters = [
+        { id: "aaa", name: "foo", type: "string/=" },
+        { id: "bbb", name: "foo", type: "string/=" },
+        { id: "ccc", name: "bar", type: "string/=" },
+      ] as Parameter[];
+
+      const newParams = setParameterTypesFromFieldSettings(
+        formSettings,
+        parameters,
+      );
+
+      newParams.forEach(param => expect(param.type).toEqual("date/single"));
+    });
+  });
+
+  describe("setTemplateTagTypesFromFieldSettings", () => {
+    it("should set text and number template tag types", () => {
+      const question = createQuestionWithTemplateTags("date");
+
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings({ fieldType: "string" }),
+          bbb: getDefaultFieldSettings({ fieldType: "number" }),
+        },
+      });
+
+      const newQuestion = setTemplateTagTypesFromFieldSettings(
+        formSettings,
+        question,
+      );
+
+      const tags = (newQuestion.card().dataset_query as NativeDatasetQuery)
+        .native["template-tags"];
+
+      expect(tags.name.type).toEqual("text");
+      expect(tags.price.type).toEqual("number");
+    });
+
+    it("should set date template tag types", () => {
+      const question = createQuestionWithTemplateTags("number");
+
+      const formSettings = getDefaultFormSettings({
+        name: "test form",
+        fields: {
+          aaa: getDefaultFieldSettings({ fieldType: "date" }),
+          bbb: getDefaultFieldSettings({ fieldType: "date" }),
+        },
+      });
+
+      const newQuestion = setTemplateTagTypesFromFieldSettings(
+        formSettings,
+        question,
+      );
+
+      const tags = (newQuestion.card().dataset_query as NativeDatasetQuery)
+        .native["template-tags"];
+
+      expect(tags.name.type).toEqual("date");
+      expect(tags.price.type).toEqual("date");
+    });
+  });
+});
diff --git a/frontend/src/metabase/entities/index.js b/frontend/src/metabase/entities/index.js
index d536e5ce7b65df2e6ac4e3babce3c82780337146..710b3b14cbb0df26cb842ed4e0c2d6cfab396a82 100644
--- a/frontend/src/metabase/entities/index.js
+++ b/frontend/src/metabase/entities/index.js
@@ -1,3 +1,4 @@
+export { default as actions } from "./actions";
 export { default as alerts } from "./alerts";
 export { default as collections } from "./collections";
 export { default as snippetCollections } from "./snippet-collections";
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index cb25bf004e91c44f1da63e7a58cc4673d6d28b32..7d20d3788f4452bdd09c53c3cceba300543e4493 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -465,3 +465,14 @@ function setParamsEndpoints(prefix) {
     prefix + "/dashboard/:dashId/params/:paramId/search/:query",
   );
 }
+
+export const ActionsApi = {
+  list: GET("/api/action"),
+  get: GET("/api/action/:id"),
+  create: POST("/api/action"),
+  update: PUT("/api/action/:id"),
+  prefetchValues: GET(
+    "/api/dashboard/:dashboardId/dashcard/:dashcardId/execute",
+  ),
+  execute: POST("/api/dashboard/:dashboardId/dashcard/:dashcardId/execute"),
+};