diff --git a/backend/src/Queue.ts b/backend/src/Queue.ts
index cb8179bd..38ab2560 100644
--- a/backend/src/Queue.ts
+++ b/backend/src/Queue.ts
@@ -41,4 +41,8 @@ export class Queue {
       setTimeout(resolve, this.timeout);
     }).then(() => this.next());
   }
+
+  public clear() {
+    this.queue.splice(0, this.queue.length);
+  }
 }
diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts
index 6e776961..86fbba80 100644
--- a/backend/src/pluginUtils.ts
+++ b/backend/src/pluginUtils.ts
@@ -3,7 +3,7 @@
  */
 
 import { Member } from "eris";
-import { configUtils, helpers, PluginData, PluginOptions } from "knub";
+import { configUtils, helpers, PluginBlueprint, PluginData, PluginOptions } from "knub";
 import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils";
 import { deepKeyIntersect, errorMessage, successMessage } from "./utils";
 import { ZeppelinPluginBlueprint } from "./plugins/ZeppelinPluginBlueprint";
@@ -27,8 +27,15 @@ export function hasPermission(pluginData: PluginData<any>, permission: string, m
   return helpers.hasPermission(config, permission);
 }
 
-export function getPluginConfigPreprocessor(blueprint: ZeppelinPluginBlueprint) {
-  return (options: PluginOptions<any>) => {
+export function getPluginConfigPreprocessor(
+  blueprint: ZeppelinPluginBlueprint,
+  customPreprocessor?: PluginBlueprint<any>["configPreprocessor"],
+) {
+  return async (options: PluginOptions<any>) => {
+    if (customPreprocessor) {
+      options = await customPreprocessor(options);
+    }
+
     const decodedConfig = blueprint.configSchema
       ? decodeAndValidateStrict(blueprint.configSchema, options.config)
       : options.config;
diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts
new file mode 100644
index 00000000..9c5ce269
--- /dev/null
+++ b/backend/src/plugins/Automod/AutomodPlugin.ts
@@ -0,0 +1,148 @@
+import { zeppelinPlugin } from "../ZeppelinPluginBlueprint";
+import { AutomodPluginType, ConfigSchema } from "./types";
+import { RunAutomodOnJoinEvt } from "./events/RunAutomodOnJoinEvt";
+import { GuildLogs } from "../../data/GuildLogs";
+import { GuildSavedMessages } from "../../data/GuildSavedMessages";
+import { runAutomodOnMessage } from "./events/runAutomodOnMessage";
+import { Queue } from "../../Queue";
+import { configUtils } from "knub";
+import { availableTriggers } from "./triggers/availableTriggers";
+import { StrictValidationError } from "../../validatorUtils";
+import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
+import { availableActions } from "./actions/availableActions";
+import { clearOldRecentActions } from "./functions/clearOldRecentActions";
+import { MINUTES, SECONDS } from "../../utils";
+import { clearOldRecentSpam } from "./functions/clearOldRecentSpam";
+import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
+import { GuildArchives } from "../../data/GuildArchives";
+
+const defaultOptions = {
+  config: {
+    rules: {},
+    antiraid_levels: ["low", "medium", "high"],
+    can_set_antiraid: false,
+    can_view_antiraid: false,
+  },
+  overrides: [
+    {
+      level: ">=50",
+      config: {
+        can_view_antiraid: true,
+      },
+    },
+    {
+      level: ">=100",
+      config: {
+        can_set_antiraid: true,
+      },
+    },
+  ],
+};
+
+/**
+ * Config preprocessor to set default values for triggers
+ */
+const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = options => {
+  if (options.config?.rules) {
+    // Loop through each rule
+    for (const [name, rule] of Object.entries(options.config.rules)) {
+      rule["name"] = name;
+
+      // If the rule doesn't have an explicitly set "enabled" property, set it to true
+      if (rule["enabled"] == null) {
+        rule["enabled"] = true;
+      }
+
+      if (rule["affects_bots"] == null) {
+        rule["affects_bots"] = false;
+      }
+
+      // Loop through the rule's triggers
+      if (rule["triggers"]) {
+        for (const triggerObj of rule["triggers"]) {
+          for (const triggerName in triggerObj) {
+            if (!availableTriggers[triggerName]) {
+              throw new StrictValidationError([`Unknown trigger '${triggerName}' in rule '${rule.name}'`]);
+            }
+
+            const triggerBlueprint = availableTriggers[triggerName];
+            triggerObj[triggerName] = configUtils.mergeConfig(triggerBlueprint.defaultConfig, triggerObj[triggerName]);
+
+            if (triggerObj[triggerName].match_attachment_type) {
+              const white = triggerObj[triggerName].match_attachment_type.whitelist_enabled;
+              const black = triggerObj[triggerName].match_attachment_type.blacklist_enabled;
+
+              if (white && black) {
+                throw new StrictValidationError([
+                  `Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_attachment_type>`,
+                ]);
+              } else if (!white && !black) {
+                throw new StrictValidationError([
+                  `Must have either blacklist or whitelist enabled at rule <${rule.name}/match_attachment_type>`,
+                ]);
+              }
+            }
+          }
+        }
+      }
+
+      // Enable logging of automod actions by default
+      if (rule["actions"]) {
+        for (const actionName in rule.actions) {
+          if (!availableActions[actionName]) {
+            throw new StrictValidationError([`Unknown action '${actionName}' in rule '${rule.name}'`]);
+          }
+        }
+
+        // if (rule["actions"]["log"] == null) {
+        //   rule["actions"]["log"] = true;
+        // }
+      }
+    }
+  }
+
+  return options;
+};
+
+export const AutomodPlugin = zeppelinPlugin<AutomodPluginType>()("automod", {
+  configSchema: ConfigSchema,
+  defaultOptions,
+  configPreprocessor,
+
+  events: [
+    RunAutomodOnJoinEvt,
+    // Messages use message events from SavedMessages, see onLoad below
+  ],
+
+  onLoad(pluginData) {
+    pluginData.state.queue = new Queue();
+
+    pluginData.state.recentActions = [];
+    pluginData.state.clearRecentActionsInterval = setInterval(() => clearOldRecentActions(pluginData), 1 * MINUTES);
+
+    pluginData.state.recentSpam = [];
+    pluginData.state.clearRecentSpamInterval = setInterval(() => clearOldRecentSpam(pluginData), 1 * SECONDS);
+
+    pluginData.state.logs = new GuildLogs(pluginData.guild.id);
+    pluginData.state.savedMessages = GuildSavedMessages.getGuildInstance(pluginData.guild.id);
+    pluginData.state.antiraidLevels = GuildAntiraidLevels.getGuildInstance(pluginData.guild.id);
+    pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
+
+    pluginData.state.onMessageCreateFn = message => runAutomodOnMessage(pluginData, message, false);
+    pluginData.state.savedMessages.events.on("create", pluginData.state.onMessageCreateFn);
+
+    pluginData.state.onMessageUpdateFn = message => runAutomodOnMessage(pluginData, message, true);
+    pluginData.state.savedMessages.events.on("update", pluginData.state.onMessageUpdateFn);
+  },
+
+  onUnload(pluginData) {
+    pluginData.state.queue.clear();
+
+    clearInterval(pluginData.state.clearRecentActionsInterval);
+
+    clearInterval(pluginData.state.clearRecentSpamInterval);
+
+    pluginData.state.savedMessages.events.off("create", pluginData.state.onMessageCreateFn);
+    pluginData.state.savedMessages.events.off("update", pluginData.state.onMessageUpdateFn);
+  },
+});
diff --git a/backend/src/plugins/Automod/actions/availableActions.ts b/backend/src/plugins/Automod/actions/availableActions.ts
new file mode 100644
index 00000000..24ec1542
--- /dev/null
+++ b/backend/src/plugins/Automod/actions/availableActions.ts
@@ -0,0 +1,11 @@
+import * as t from "io-ts";
+import { CleanAction } from "./clean";
+import { AutomodActionBlueprint } from "../helpers";
+
+export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
+  clean: CleanAction,
+};
+
+export const AvailableActions = t.type({
+  clean: CleanAction.configType,
+});
diff --git a/backend/src/plugins/Automod/actions/clean.ts b/backend/src/plugins/Automod/actions/clean.ts
new file mode 100644
index 00000000..67fbf0b1
--- /dev/null
+++ b/backend/src/plugins/Automod/actions/clean.ts
@@ -0,0 +1,28 @@
+import * as t from "io-ts";
+import { automodAction } from "../helpers";
+import { LogType } from "../../../data/LogType";
+
+export const CleanAction = automodAction({
+  configType: t.boolean,
+
+  async apply({ pluginData, contexts }) {
+    const messageIdsToDeleteByChannelId: Map<string, string[]> = new Map();
+    for (const context of contexts) {
+      if (context.message) {
+        if (!messageIdsToDeleteByChannelId.has(context.message.channel_id)) {
+          messageIdsToDeleteByChannelId.set(context.message.channel_id, []);
+        }
+
+        messageIdsToDeleteByChannelId.get(context.message.channel_id).push(context.message.id);
+      }
+    }
+
+    for (const [channelId, messageIds] of messageIdsToDeleteByChannelId.entries()) {
+      for (const id of messageIds) {
+        pluginData.state.logs.ignoreLog(LogType.MESSAGE_DELETE, id);
+      }
+
+      await pluginData.client.deleteMessages(channelId, messageIds);
+    }
+  },
+});
diff --git a/backend/src/plugins/Automod/constants.ts b/backend/src/plugins/Automod/constants.ts
new file mode 100644
index 00000000..3ebc278c
--- /dev/null
+++ b/backend/src/plugins/Automod/constants.ts
@@ -0,0 +1,16 @@
+import { MINUTES, SECONDS } from "../../utils";
+
+export const RECENT_SPAM_EXPIRY_TIME = 10 * SECONDS;
+export const RECENT_ACTION_EXPIRY_TIME = 5 * MINUTES;
+
+export enum RecentActionType {
+  Message = 1,
+  Mention,
+  Link,
+  Attachment,
+  Emoji,
+  Line,
+  Character,
+  VoiceChannelMove,
+  MemberJoin,
+}
diff --git a/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts b/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts
new file mode 100644
index 00000000..83786f12
--- /dev/null
+++ b/backend/src/plugins/Automod/events/RunAutomodOnJoinEvt.ts
@@ -0,0 +1,16 @@
+import { SavedMessage } from "../../../data/entities/SavedMessage";
+import { eventListener, PluginData } from "knub";
+import { AutomodContext, AutomodPluginType } from "../types";
+import { runAutomod } from "../functions/runAutomod";
+
+export const RunAutomodOnJoinEvt = eventListener<AutomodPluginType>()(
+  "guildMemberAdd",
+  ({ pluginData, args: { member } }) => {
+    const context: AutomodContext = {
+      timestamp: Date.now(),
+      user: member.user,
+    };
+
+    pluginData.state.queue.add(() => runAutomod(pluginData, context));
+  },
+);
diff --git a/backend/src/plugins/Automod/events/runAutomodOnMessage.ts b/backend/src/plugins/Automod/events/runAutomodOnMessage.ts
new file mode 100644
index 00000000..d90872b4
--- /dev/null
+++ b/backend/src/plugins/Automod/events/runAutomodOnMessage.ts
@@ -0,0 +1,18 @@
+import { SavedMessage } from "../../../data/entities/SavedMessage";
+import { PluginData } from "knub";
+import { AutomodContext, AutomodPluginType } from "../types";
+import { runAutomod } from "../functions/runAutomod";
+import { addRecentActionsFromMessage } from "../functions/addRecentActionsFromMessage";
+import moment from "moment-timezone";
+
+export function runAutomodOnMessage(pluginData: PluginData<AutomodPluginType>, message: SavedMessage, isEdit: boolean) {
+  const context: AutomodContext = {
+    timestamp: moment.utc(message.posted_at).valueOf(),
+    message,
+  };
+
+  pluginData.state.queue.add(async () => {
+    addRecentActionsFromMessage(pluginData, context);
+    await runAutomod(pluginData, context);
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts
new file mode 100644
index 00000000..ad88d00d
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/addRecentActionsFromMessage.ts
@@ -0,0 +1,130 @@
+import moment from "moment-timezone";
+import { AutomodContext, AutomodPluginType } from "../types";
+import { PluginData } from "knub";
+import { RECENT_ACTION_EXPIRY_TIME, RecentActionType } from "../constants";
+import { getEmojiInString, getRoleMentions, getUrlsInString, getUserMentions } from "../../../utils";
+
+export function addRecentActionsFromMessage(pluginData: PluginData<AutomodPluginType>, context: AutomodContext) {
+  const globalIdentifier = context.message.user_id;
+  const perChannelIdentifier = `${context.message.channel_id}-${context.message.user_id}`;
+  const expiresAt = Date.now() + RECENT_ACTION_EXPIRY_TIME;
+
+  pluginData.state.recentActions.push({
+    context,
+    type: RecentActionType.Message,
+    identifier: globalIdentifier,
+    count: 1,
+  });
+
+  pluginData.state.recentActions.push({
+    context,
+    type: RecentActionType.Message,
+    identifier: perChannelIdentifier,
+    count: 1,
+  });
+
+  const mentionCount =
+    getUserMentions(context.message.data.content || "").length +
+    getRoleMentions(context.message.data.content || "").length;
+  if (mentionCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Mention,
+      identifier: globalIdentifier,
+      count: mentionCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Mention,
+      identifier: perChannelIdentifier,
+      count: mentionCount,
+    });
+  }
+
+  const linkCount = getUrlsInString(context.message.data.content || "").length;
+  if (linkCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Link,
+      identifier: globalIdentifier,
+      count: linkCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Link,
+      identifier: perChannelIdentifier,
+      count: linkCount,
+    });
+  }
+
+  const attachmentCount = context.message.data.attachments && context.message.data.attachments.length;
+  if (attachmentCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Attachment,
+      identifier: globalIdentifier,
+      count: attachmentCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Attachment,
+      identifier: perChannelIdentifier,
+      count: attachmentCount,
+    });
+  }
+
+  const emojiCount = getEmojiInString(context.message.data.content || "").length;
+  if (emojiCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Emoji,
+      identifier: globalIdentifier,
+      count: emojiCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Emoji,
+      identifier: perChannelIdentifier,
+      count: emojiCount,
+    });
+  }
+
+  // + 1 is for the first line of the message (which doesn't have a line break)
+  const lineCount = context.message.data.content ? (context.message.data.content.match(/\n/g) || []).length + 1 : 0;
+  if (lineCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Line,
+      identifier: globalIdentifier,
+      count: lineCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Line,
+      identifier: perChannelIdentifier,
+      count: lineCount,
+    });
+  }
+
+  const characterCount = [...(context.message.data.content || "")].length;
+  if (characterCount) {
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Character,
+      identifier: globalIdentifier,
+      count: characterCount,
+    });
+
+    pluginData.state.recentActions.push({
+      context,
+      type: RecentActionType.Character,
+      identifier: perChannelIdentifier,
+      count: characterCount,
+    });
+  }
+}
diff --git a/backend/src/plugins/Automod/functions/clearOldRecentActions.ts b/backend/src/plugins/Automod/functions/clearOldRecentActions.ts
new file mode 100644
index 00000000..7f933be9
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/clearOldRecentActions.ts
@@ -0,0 +1,10 @@
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+import { RECENT_ACTION_EXPIRY_TIME } from "../constants";
+
+export function clearOldRecentActions(pluginData: PluginData<AutomodPluginType>) {
+  const now = Date.now();
+  pluginData.state.recentActions = pluginData.state.recentActions.filter(info => {
+    return info.context.timestamp + RECENT_ACTION_EXPIRY_TIME > now;
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/clearOldRecentSpam.ts b/backend/src/plugins/Automod/functions/clearOldRecentSpam.ts
new file mode 100644
index 00000000..a05c699d
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/clearOldRecentSpam.ts
@@ -0,0 +1,10 @@
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+import { RECENT_SPAM_EXPIRY_TIME } from "../constants";
+
+export function clearOldRecentSpam(pluginData: PluginData<AutomodPluginType>) {
+  const now = Date.now();
+  pluginData.state.recentSpam = pluginData.state.recentSpam.filter(spam => {
+    return spam.timestamp + RECENT_SPAM_EXPIRY_TIME > now;
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts
new file mode 100644
index 00000000..7c5ca32e
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/createMessageSpamTrigger.ts
@@ -0,0 +1,84 @@
+import { RecentActionType } from "../constants";
+import { automodTrigger } from "../helpers";
+import { getBaseUrl } from "../../../pluginUtils";
+import { convertDelayStringToMS, tDelayString, tNullable } from "../../../utils";
+import { humanizeDurationShort } from "../../../humanizeDurationShort";
+import { findRecentSpam } from "./findRecentSpam";
+import { getMatchingMessageRecentActions } from "./getMatchingMessageRecentActions";
+import * as t from "io-ts";
+
+const MessageSpamTriggerConfig = t.type({
+  amount: t.number,
+  within: tDelayString,
+  per_channel: tNullable(t.boolean),
+});
+type TMessageSpamTriggerConfig = t.TypeOf<typeof MessageSpamTriggerConfig>;
+
+const MessageSpamMatchResultType = t.type({
+  archiveId: t.string,
+});
+type TMessageSpamMatchResultType = t.TypeOf<typeof MessageSpamMatchResultType>;
+
+export function createMessageSpamTrigger(spamType: RecentActionType, prettyName: string) {
+  return automodTrigger({
+    configType: MessageSpamTriggerConfig,
+    defaultConfig: {},
+
+    matchResultType: MessageSpamMatchResultType,
+
+    async match({ pluginData, context, triggerConfig }) {
+      if (!context.message) {
+        return;
+      }
+
+      const recentSpam = findRecentSpam(pluginData, spamType, context.message.user_id);
+      if (recentSpam) {
+        // TODO: Combine with old archive
+        return {
+          silentClean: true,
+        };
+      }
+
+      const within = convertDelayStringToMS(triggerConfig.within);
+      const matchedSpam = getMatchingMessageRecentActions(
+        pluginData,
+        context.message,
+        spamType,
+        triggerConfig.amount,
+        within,
+        triggerConfig.per_channel,
+      );
+
+      if (matchedSpam) {
+        // TODO: Generate archive link
+        const archiveId = "TODO";
+
+        pluginData.state.recentSpam.push({
+          type: spamType,
+          userId: context.message.user_id,
+          archiveId,
+          timestamp: Date.now(),
+        });
+
+        return {
+          extraContexts: matchedSpam.recentActions
+            .map(action => action.context)
+            .filter(_context => _context !== context),
+
+          extra: {
+            archiveId,
+          },
+        };
+      }
+    },
+
+    renderMatchInformation({ pluginData, matchResult, triggerConfig }) {
+      const baseUrl = getBaseUrl(pluginData);
+      const archiveUrl = pluginData.state.archives.getUrl(baseUrl, matchResult.extra.archiveId);
+      const withinMs = convertDelayStringToMS(triggerConfig.within);
+      const withinStr = humanizeDurationShort(withinMs);
+
+      return `Matched ${prettyName} spam (${triggerConfig.amount} in ${withinStr}): ${archiveUrl}`;
+    },
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/findRecentSpam.ts b/backend/src/plugins/Automod/functions/findRecentSpam.ts
new file mode 100644
index 00000000..a22b91b5
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/findRecentSpam.ts
@@ -0,0 +1,9 @@
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+import { RecentActionType } from "../constants";
+
+export function findRecentSpam(pluginData: PluginData<AutomodPluginType>, type: RecentActionType, userId: string) {
+  return pluginData.state.recentSpam.find(spam => {
+    return spam.type === type && spam.userId === userId;
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/getMatchingMessageRecentActions.ts b/backend/src/plugins/Automod/functions/getMatchingMessageRecentActions.ts
new file mode 100644
index 00000000..410aeaf2
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/getMatchingMessageRecentActions.ts
@@ -0,0 +1,28 @@
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+import { SavedMessage } from "../../../data/entities/SavedMessage";
+import moment from "moment-timezone";
+import { getMatchingRecentActions } from "./getMatchingRecentActions";
+import { RecentActionType } from "../constants";
+
+export function getMatchingMessageRecentActions(
+  pluginData: PluginData<AutomodPluginType>,
+  message: SavedMessage,
+  type: RecentActionType,
+  count: number,
+  within: number,
+  perChannel: boolean,
+) {
+  const since = moment.utc(message.posted_at).valueOf() - within;
+  const to = moment.utc(message.posted_at).valueOf();
+  const identifier = perChannel ? `${message.channel_id}-${message.user_id}` : message.user_id;
+  const recentActions = getMatchingRecentActions(pluginData, type, identifier, since, to);
+  const totalCount = recentActions.reduce((total, action) => total + action.count, 0);
+
+  if (totalCount >= count) {
+    return {
+      identifier,
+      recentActions,
+    };
+  }
+}
diff --git a/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts
new file mode 100644
index 00000000..9197e24d
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/getMatchingRecentActions.ts
@@ -0,0 +1,20 @@
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+import { RecentActionType } from "../constants";
+
+export function getMatchingRecentActions(
+  pluginData: PluginData<AutomodPluginType>,
+  type: RecentActionType,
+  identifier: string | null,
+  since: number,
+  to: number,
+) {
+  return pluginData.state.recentActions.filter(action => {
+    return (
+      action.type === type &&
+      (!identifier || action.identifier === identifier) &&
+      action.context.timestamp >= since &&
+      action.context.timestamp <= to
+    );
+  });
+}
diff --git a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts
new file mode 100644
index 00000000..176bfa23
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts
@@ -0,0 +1,68 @@
+import * as t from "io-ts";
+import { SavedMessage } from "../../../data/entities/SavedMessage";
+import { resolveMember } from "../../../utils";
+import { PluginData } from "knub";
+import { AutomodPluginType } from "../types";
+
+type TextTriggerWithMultipleMatchTypes = {
+  match_messages: boolean;
+  match_embeds: boolean;
+  match_visible_names: boolean;
+  match_usernames: boolean;
+  match_nicknames: boolean;
+  match_custom_status: boolean;
+};
+
+export const MatchableTextType = t.union([
+  t.literal("message"),
+  t.literal("embed"),
+  t.literal("visiblename"),
+  t.literal("username"),
+  t.literal("nickname"),
+  t.literal("customstatus"),
+]);
+
+export type TMatchableTextType = t.TypeOf<typeof MatchableTextType>;
+
+type YieldedContent = [TMatchableTextType, string];
+
+/**
+ * Generator function that allows iterating through matchable pieces of text of a SavedMessage
+ */
+export async function* matchMultipleTextTypesOnMessage(
+  pluginData: PluginData<AutomodPluginType>,
+  trigger: TextTriggerWithMultipleMatchTypes,
+  msg: SavedMessage,
+): AsyncIterableIterator<YieldedContent> {
+  const member = await resolveMember(pluginData.client, pluginData.guild, msg.user_id);
+  if (!member) return;
+
+  if (trigger.match_messages && msg.data.content) {
+    yield ["message", msg.data.content];
+  }
+
+  if (trigger.match_embeds && msg.data.embeds && msg.data.embeds.length) {
+    const copiedEmbed = JSON.parse(JSON.stringify(msg.data.embeds[0]));
+    if (copiedEmbed.type === "video") {
+      copiedEmbed.description = ""; // The description is not rendered, hence it doesn't need to be matched
+    }
+    yield ["embed", JSON.stringify(copiedEmbed)];
+  }
+
+  if (trigger.match_visible_names) {
+    yield ["visiblename", member.nick || msg.data.author.username];
+  }
+
+  if (trigger.match_usernames) {
+    yield ["username", `${msg.data.author.username}#${msg.data.author.discriminator}`];
+  }
+
+  if (trigger.match_nicknames && member.nick) {
+    yield ["nickname", member.nick];
+  }
+
+  // type 4 = custom status
+  if (trigger.match_custom_status && member.game?.type === 4 && member.game?.state) {
+    yield ["customstatus", member.game.state];
+  }
+}
diff --git a/backend/src/plugins/Automod/functions/runAutomod.ts b/backend/src/plugins/Automod/functions/runAutomod.ts
new file mode 100644
index 00000000..f4951160
--- /dev/null
+++ b/backend/src/plugins/Automod/functions/runAutomod.ts
@@ -0,0 +1,83 @@
+import { PluginData } from "knub";
+import { AutomodContext, AutomodPluginType, TRule } from "../types";
+import { availableTriggers } from "../triggers/availableTriggers";
+import { availableActions } from "../actions/availableActions";
+import { AutomodTriggerMatchResult } from "../helpers";
+import { CleanAction } from "../actions/clean";
+
+export async function runAutomod(pluginData: PluginData<AutomodPluginType>, context: AutomodContext) {
+  const userId = context.user?.id || context.message?.user_id;
+  const member = userId && pluginData.guild.members.get(userId);
+  const channelId = context.message?.channel_id;
+  const channel = channelId && pluginData.guild.channels.get(channelId);
+  const categoryId = channel?.parentID;
+
+  const config = pluginData.config.getMatchingConfig({
+    channelId,
+    categoryId,
+    userId,
+    member,
+  });
+
+  for (const [ruleName, rule] of Object.entries(config.rules)) {
+    if (rule.enabled === false) continue;
+
+    let matchResult: AutomodTriggerMatchResult<any>;
+    let matchSummary: string;
+    let contexts: AutomodContext[];
+
+    triggerLoop: for (const triggerItem of rule.triggers) {
+      for (const [triggerName, triggerConfig] of Object.entries(triggerItem)) {
+        const trigger = availableTriggers[triggerName];
+        matchResult = await trigger.match({
+          ruleName,
+          pluginData,
+          context,
+          triggerConfig,
+        });
+
+        if (matchResult) {
+          contexts = [context, ...(matchResult.extraContexts || [])];
+
+          for (const _context of contexts) {
+            _context.actioned = true;
+          }
+
+          if (matchResult.silentClean) {
+            await CleanAction.apply({
+              ruleName,
+              pluginData,
+              contexts,
+              actionConfig: true,
+            });
+            return;
+          }
+
+          matchSummary = await trigger.renderMatchInformation({
+            ruleName,
+            pluginData,
+            contexts,
+            matchResult,
+            triggerConfig,
+          });
+
+          break triggerLoop;
+        }
+      }
+    }
+
+    if (matchResult) {
+      for (const [actionName, actionConfig] of Object.entries(rule.actions)) {
+        const action = availableActions[actionName];
+        action.apply({
+          ruleName,
+          pluginData,
+          contexts,
+          actionConfig,
+        });
+      }
+
+      break;
+    }
+  }
+}
diff --git a/backend/src/plugins/Automod/helpers.ts b/backend/src/plugins/Automod/helpers.ts
new file mode 100644
index 00000000..b2c020d5
--- /dev/null
+++ b/backend/src/plugins/Automod/helpers.ts
@@ -0,0 +1,60 @@
+import { PluginData } from "knub";
+import { Awaitable } from "knub/dist/utils";
+import * as t from "io-ts";
+import { AutomodContext, AutomodPluginType } from "./types";
+
+export interface AutomodTriggerMatchResult<TExtra extends any = unknown> {
+  extraContexts?: AutomodContext[];
+  extra?: TExtra;
+
+  silentClean?: boolean;
+}
+
+type AutomodTriggerMatchFn<TConfigType, TMatchResultExtra> = (meta: {
+  ruleName: string;
+  pluginData: PluginData<AutomodPluginType>;
+  context: AutomodContext;
+  triggerConfig: TConfigType;
+}) => Awaitable<null | AutomodTriggerMatchResult<TMatchResultExtra>>;
+
+type AutomodTriggerRenderMatchInformationFn<TConfigType, TMatchResultExtra> = (meta: {
+  ruleName: string;
+  pluginData: PluginData<AutomodPluginType>;
+  contexts: AutomodContext[];
+  triggerConfig: TConfigType;
+  matchResult: AutomodTriggerMatchResult<TMatchResultExtra>;
+}) => Awaitable<string>;
+
+export interface AutomodTriggerBlueprint<TConfigType extends t.Any, TMatchResultExtra extends t.Any> {
+  configType: TConfigType;
+  defaultConfig: Partial<t.TypeOf<TConfigType>>;
+
+  matchResultType: TMatchResultExtra;
+
+  match: AutomodTriggerMatchFn<t.TypeOf<TConfigType>, t.TypeOf<TMatchResultExtra>>;
+  renderMatchInformation: AutomodTriggerRenderMatchInformationFn<t.TypeOf<TConfigType>, t.TypeOf<TMatchResultExtra>>;
+}
+
+export function automodTrigger<TConfigType extends t.Any, TMatchResultExtra extends t.Any>(
+  blueprint: AutomodTriggerBlueprint<TConfigType, TMatchResultExtra>,
+): AutomodTriggerBlueprint<TConfigType, TMatchResultExtra> {
+  return blueprint;
+}
+
+type AutomodActionApplyFn<TConfigType> = (meta: {
+  ruleName: string;
+  pluginData: PluginData<AutomodPluginType>;
+  contexts: AutomodContext[];
+  actionConfig: TConfigType;
+}) => Awaitable<void>;
+
+export interface AutomodActionBlueprint<TConfigType extends t.Any> {
+  configType: TConfigType;
+  apply: AutomodActionApplyFn<t.TypeOf<TConfigType>>;
+}
+
+export function automodAction<TConfigType extends t.Any>(
+  blueprint: AutomodActionBlueprint<TConfigType>,
+): AutomodActionBlueprint<TConfigType> {
+  return blueprint;
+}
diff --git a/backend/src/plugins/Automod/triggers/attachmentSpam.ts b/backend/src/plugins/Automod/triggers/attachmentSpam.ts
new file mode 100644
index 00000000..79c46d84
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/attachmentSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const AttachmentSpamTrigger = createMessageSpamTrigger(RecentActionType.Attachment, "attachment");
diff --git a/backend/src/plugins/Automod/triggers/availableTriggers.ts b/backend/src/plugins/Automod/triggers/availableTriggers.ts
new file mode 100644
index 00000000..31c2ef83
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/availableTriggers.ts
@@ -0,0 +1,37 @@
+import * as t from "io-ts";
+import { MatchWordsTrigger } from "./matchWords";
+import { AutomodTriggerBlueprint } from "../helpers";
+import { MessageSpamTrigger } from "./messageSpam";
+import { MentionSpamTrigger } from "./mentionSpam";
+import { LinkSpamTrigger } from "./linkSpam";
+import { AttachmentSpamTrigger } from "./attachmentSpam";
+import { EmojiSpamTrigger } from "./emojiSpam";
+import { LineSpamTrigger } from "./lineSpam";
+import { CharacterSpamTrigger } from "./characterSpam";
+import { MatchRegexTrigger } from "./matchRegex";
+
+export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>> = {
+  match_words: MatchWordsTrigger,
+  match_regex: MatchRegexTrigger,
+
+  message_spam: MessageSpamTrigger,
+  mention_spam: MentionSpamTrigger,
+  link_spam: LinkSpamTrigger,
+  attachment_spam: AttachmentSpamTrigger,
+  emoji_spam: EmojiSpamTrigger,
+  line_spam: LineSpamTrigger,
+  character_spam: CharacterSpamTrigger,
+};
+
+export const AvailableTriggers = t.type({
+  match_words: MatchWordsTrigger.configType,
+  match_regex: MatchRegexTrigger.configType,
+
+  message_spam: MessageSpamTrigger.configType,
+  mention_spam: MentionSpamTrigger.configType,
+  link_spam: LinkSpamTrigger.configType,
+  attachment_spam: AttachmentSpamTrigger.configType,
+  emoji_spam: EmojiSpamTrigger.configType,
+  line_spam: LineSpamTrigger.configType,
+  character_spam: CharacterSpamTrigger.configType,
+});
diff --git a/backend/src/plugins/Automod/triggers/characterSpam.ts b/backend/src/plugins/Automod/triggers/characterSpam.ts
new file mode 100644
index 00000000..5412ffb2
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/characterSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const CharacterSpamTrigger = createMessageSpamTrigger(RecentActionType.Character, "character");
diff --git a/backend/src/plugins/Automod/triggers/emojiSpam.ts b/backend/src/plugins/Automod/triggers/emojiSpam.ts
new file mode 100644
index 00000000..018cb891
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/emojiSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const EmojiSpamTrigger = createMessageSpamTrigger(RecentActionType.Emoji, "emoji");
diff --git a/backend/src/plugins/Automod/triggers/exampleTrigger.ts b/backend/src/plugins/Automod/triggers/exampleTrigger.ts
new file mode 100644
index 00000000..56770f72
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/exampleTrigger.ts
@@ -0,0 +1,27 @@
+import * as t from "io-ts";
+import { automodTrigger } from "../helpers";
+
+export const ExampleTrigger = automodTrigger({
+  configType: t.type({
+    some: t.number,
+    value: t.string,
+  }),
+
+  defaultConfig: {},
+
+  matchResultType: t.type({
+    thing: t.string,
+  }),
+
+  async match() {
+    return {
+      extra: {
+        thing: "hi",
+      },
+    };
+  },
+
+  renderMatchInformation() {
+    return "";
+  },
+});
diff --git a/backend/src/plugins/Automod/triggers/lineSpam.ts b/backend/src/plugins/Automod/triggers/lineSpam.ts
new file mode 100644
index 00000000..2a54f1b4
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/lineSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const LineSpamTrigger = createMessageSpamTrigger(RecentActionType.Line, "line");
diff --git a/backend/src/plugins/Automod/triggers/linkSpam.ts b/backend/src/plugins/Automod/triggers/linkSpam.ts
new file mode 100644
index 00000000..0278d8d5
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/linkSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const LinkSpamTrigger = createMessageSpamTrigger(RecentActionType.Link, "link");
diff --git a/backend/src/plugins/Automod/triggers/matchRegex.ts b/backend/src/plugins/Automod/triggers/matchRegex.ts
new file mode 100644
index 00000000..20dc7f60
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/matchRegex.ts
@@ -0,0 +1,72 @@
+import * as t from "io-ts";
+import { transliterate } from "transliteration";
+import escapeStringRegexp from "escape-string-regexp";
+import { automodTrigger } from "../helpers";
+import { disableInlineCode, verboseChannelMention } from "../../../utils";
+import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
+
+export const MatchRegexTrigger = automodTrigger({
+  configType: t.type({
+    patterns: t.array(t.string),
+    case_sensitive: t.boolean,
+    normalize: t.boolean,
+    match_messages: t.boolean,
+    match_embeds: t.boolean,
+    match_visible_names: t.boolean,
+    match_usernames: t.boolean,
+    match_nicknames: t.boolean,
+    match_custom_status: t.boolean,
+  }),
+
+  defaultConfig: {
+    case_sensitive: false,
+    normalize: false,
+    match_messages: true,
+    match_embeds: true,
+    match_visible_names: false,
+    match_usernames: false,
+    match_nicknames: false,
+    match_custom_status: false,
+  },
+
+  matchResultType: t.type({
+    pattern: t.string,
+    type: MatchableTextType,
+  }),
+
+  async match({ pluginData, context, triggerConfig: trigger }) {
+    if (!context.message) {
+      return;
+    }
+
+    for await (let [type, str] of matchMultipleTextTypesOnMessage(pluginData, trigger, context.message)) {
+      if (trigger.normalize) {
+        str = transliterate(str);
+      }
+
+      for (const pattern of trigger.patterns) {
+        const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
+        const test = regex.test(str);
+        if (test) {
+          return {
+            extra: {
+              pattern,
+              type,
+            },
+          };
+        }
+      }
+    }
+
+    return null;
+  },
+
+  renderMatchInformation({ pluginData, contexts, matchResult }) {
+    const channel = pluginData.guild.channels.get(contexts[0].message.channel_id);
+    const prettyChannel = verboseChannelMention(channel);
+
+    return `Matched regex \`${disableInlineCode(matchResult.extra.pattern)}\` in message (\`${
+      contexts[0].message.id
+    }\`) in ${prettyChannel}:`;
+  },
+});
diff --git a/backend/src/plugins/Automod/triggers/matchWords.ts b/backend/src/plugins/Automod/triggers/matchWords.ts
new file mode 100644
index 00000000..97390544
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/matchWords.ts
@@ -0,0 +1,90 @@
+import * as t from "io-ts";
+import { transliterate } from "transliteration";
+import escapeStringRegexp from "escape-string-regexp";
+import { automodTrigger } from "../helpers";
+import { disableInlineCode, verboseChannelMention } from "../../../utils";
+import { MatchableTextType, matchMultipleTextTypesOnMessage } from "../functions/matchMultipleTextTypesOnMessage";
+
+export const MatchWordsTrigger = automodTrigger({
+  configType: t.type({
+    words: t.array(t.string),
+    case_sensitive: t.boolean,
+    only_full_words: t.boolean,
+    normalize: t.boolean,
+    loose_matching: t.boolean,
+    loose_matching_threshold: t.number,
+    match_messages: t.boolean,
+    match_embeds: t.boolean,
+    match_visible_names: t.boolean,
+    match_usernames: t.boolean,
+    match_nicknames: t.boolean,
+    match_custom_status: t.boolean,
+  }),
+
+  defaultConfig: {
+    case_sensitive: false,
+    only_full_words: true,
+    normalize: false,
+    loose_matching: false,
+    loose_matching_threshold: 4,
+    match_messages: true,
+    match_embeds: true,
+    match_visible_names: false,
+    match_usernames: false,
+    match_nicknames: false,
+    match_custom_status: false,
+  },
+
+  matchResultType: t.type({
+    word: t.string,
+    type: MatchableTextType,
+  }),
+
+  async match({ pluginData, context, triggerConfig: trigger }) {
+    if (!context.message) {
+      return;
+    }
+
+    for await (let [type, str] of matchMultipleTextTypesOnMessage(pluginData, trigger, context.message)) {
+      if (trigger.normalize) {
+        str = transliterate(str);
+      }
+
+      const looseMatchingThreshold = Math.min(Math.max(trigger.loose_matching_threshold, 1), 64);
+
+      for (const word of trigger.words) {
+        // When performing loose matching, allow any amount of whitespace or up to looseMatchingThreshold number of other
+        // characters between the matched characters. E.g. if we're matching banana, a loose match could also match b a n a n a
+        let pattern = trigger.loose_matching
+          ? [...word].map(c => escapeStringRegexp(c)).join(`(?:\\s*|.{0,${looseMatchingThreshold})`)
+          : escapeStringRegexp(word);
+
+        if (trigger.only_full_words) {
+          pattern = `\\b${pattern}\\b`;
+        }
+
+        const regex = new RegExp(pattern, trigger.case_sensitive ? "" : "i");
+        const test = regex.test(str);
+        if (test) {
+          return {
+            extra: {
+              word,
+              type,
+            },
+          };
+        }
+      }
+    }
+
+    return null;
+  },
+
+  renderMatchInformation({ pluginData, contexts, matchResult }) {
+    const channel = pluginData.guild.channels.get(contexts[0].message.channel_id);
+    const prettyChannel = verboseChannelMention(channel);
+
+    return `Matched word \`${disableInlineCode(matchResult.extra.word)}\` in message (\`${
+      contexts[0].message.id
+    }\`) in ${prettyChannel}:`;
+  },
+});
diff --git a/backend/src/plugins/Automod/triggers/mentionSpam.ts b/backend/src/plugins/Automod/triggers/mentionSpam.ts
new file mode 100644
index 00000000..fdcd8f46
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/mentionSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const MentionSpamTrigger = createMessageSpamTrigger(RecentActionType.Mention, "mention");
diff --git a/backend/src/plugins/Automod/triggers/messageSpam.ts b/backend/src/plugins/Automod/triggers/messageSpam.ts
new file mode 100644
index 00000000..91b4f63b
--- /dev/null
+++ b/backend/src/plugins/Automod/triggers/messageSpam.ts
@@ -0,0 +1,4 @@
+import { RecentActionType } from "../constants";
+import { createMessageSpamTrigger } from "../functions/createMessageSpamTrigger";
+
+export const MessageSpamTrigger = createMessageSpamTrigger(RecentActionType.Message, "message");
diff --git a/backend/src/plugins/Automod/types.ts b/backend/src/plugins/Automod/types.ts
new file mode 100644
index 00000000..e8b51ad2
--- /dev/null
+++ b/backend/src/plugins/Automod/types.ts
@@ -0,0 +1,88 @@
+import * as t from "io-ts";
+import { tNullable, UnknownUser } from "../../utils";
+import { BasePluginType } from "knub";
+import { GuildSavedMessages } from "../../data/GuildSavedMessages";
+import { GuildLogs } from "../../data/GuildLogs";
+import { SavedMessage } from "../../data/entities/SavedMessage";
+import { User } from "eris";
+import { AvailableTriggers } from "./triggers/availableTriggers";
+import { AvailableActions } from "./actions/availableActions";
+import { Queue } from "../../Queue";
+import { GuildAntiraidLevels } from "../../data/GuildAntiraidLevels";
+import { GuildArchives } from "../../data/GuildArchives";
+import { RecentActionType } from "./constants";
+import Timeout = NodeJS.Timeout;
+
+export const Rule = t.type({
+  enabled: t.boolean,
+  name: t.string,
+  presets: tNullable(t.array(t.string)),
+  affects_bots: t.boolean,
+  triggers: t.array(t.partial(AvailableTriggers.props)),
+  actions: t.partial(AvailableActions.props),
+  cooldown: tNullable(t.string),
+});
+export type TRule = t.TypeOf<typeof Rule>;
+
+export const ConfigSchema = t.type({
+  rules: t.record(t.string, Rule),
+  antiraid_levels: t.array(t.string),
+  can_set_antiraid: t.boolean,
+  can_view_antiraid: t.boolean,
+});
+export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
+
+export interface AutomodPluginType extends BasePluginType {
+  config: TConfigSchema;
+  state: {
+    /**
+     * Automod checks/actions are handled in a queue so we don't get overlap on the same user
+     */
+    queue: Queue;
+
+    /**
+     * Recent actions are used for spam triggers
+     */
+    recentActions: RecentAction[];
+    clearRecentActionsInterval: Timeout;
+
+    /**
+     * After a spam trigger is tripped and the rule's action carried out, a unique identifier is placed here so further
+     * spam (either messages that were sent before the bot managed to mute the user or, with global spam, other users
+     * continuing to spam) is "included" in the same match and doesn't generate duplicate cases or logs.
+     * Key: rule_name-match_identifier
+     */
+    recentSpam: RecentSpam[];
+    clearRecentSpamInterval: Timeout;
+
+    savedMessages: GuildSavedMessages;
+    logs: GuildLogs;
+    antiraidLevels: GuildAntiraidLevels;
+    archives: GuildArchives;
+
+    onMessageCreateFn: any;
+    onMessageUpdateFn: any;
+  };
+}
+
+export interface AutomodContext {
+  timestamp: number;
+  actioned?: boolean;
+
+  user?: User | UnknownUser;
+  message?: SavedMessage;
+}
+
+export interface RecentAction {
+  type: RecentActionType;
+  identifier: string;
+  count: number;
+  context: AutomodContext;
+}
+
+export interface RecentSpam {
+  archiveId: string;
+  type: RecentActionType;
+  userId: string;
+  timestamp: number;
+}
diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts
index 5e05b313..671ada10 100644
--- a/backend/src/plugins/ZeppelinPluginBlueprint.ts
+++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts
@@ -26,12 +26,12 @@ export function zeppelinPlugin<TPluginType extends BasePluginType>(): <
 >(
   name: string,
   blueprint: TPartialBlueprint,
-) => TPartialBlueprint & { name: string };
+) => TPartialBlueprint & { name: string; configPreprocessor: PluginBlueprint<TPluginType>["configPreprocessor"] };
 
 export function zeppelinPlugin(...args) {
   if (args.length) {
     const blueprint: ZeppelinPluginBlueprint = plugin(...(args as Parameters<typeof plugin>));
-    blueprint.configPreprocessor = getPluginConfigPreprocessor(blueprint);
+    blueprint.configPreprocessor = getPluginConfigPreprocessor(blueprint, blueprint.configPreprocessor);
     return blueprint;
   } else {
     return zeppelinPlugin;
diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts
index 6416517e..ea38e264 100644
--- a/backend/src/plugins/availablePlugins.ts
+++ b/backend/src/plugins/availablePlugins.ts
@@ -22,6 +22,7 @@ import { RolesPlugin } from "./Roles/RolesPlugin";
 import { SlowmodePlugin } from "./Slowmode/SlowmodePlugin";
 import { StarboardPlugin } from "./Starboard/StarboardPlugin";
 import { ChannelArchiverPlugin } from "./ChannelArchiver/ChannelArchiverPlugin";
+import { AutomodPlugin } from "./Automod/AutomodPlugin";
 
 // prettier-ignore
 export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
@@ -47,6 +48,7 @@ export const guildPlugins: Array<ZeppelinPluginBlueprint<any>> = [
   WelcomeMessagePlugin,
   CasesPlugin,
   MutesPlugin,
+  AutomodPlugin,
 ];
 
 // prettier-ignore