diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java
index 89c320bc0db..81ee8bbb37d 100644
--- a/src/main/java/ch/njol/skript/Skript.java
+++ b/src/main/java/ch/njol/skript/Skript.java
@@ -77,6 +77,7 @@
 import org.skriptlang.skript.bukkit.loottables.LootTableModule;
 import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys;
 import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos;
+import org.skriptlang.skript.bukkit.recipes.RecipeModule;
 import org.skriptlang.skript.lang.comparator.Comparator;
 import org.skriptlang.skript.lang.comparator.Comparators;
 import org.skriptlang.skript.lang.converter.Converter;
@@ -555,6 +556,7 @@ public void onEnable() {
 			TagModule.load();
 			FurnaceModule.load();
 			LootTableModule.load();
+			RecipeModule.load();
 		} catch (final Exception e) {
 			exception(e, "Could not load required .class files: " + e.getLocalizedMessage());
 			setEnabled(false);
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/CreateRecipeEvent.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/CreateRecipeEvent.java
new file mode 100644
index 00000000000..e4383bd3270
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/CreateRecipeEvent.java
@@ -0,0 +1,172 @@
+package org.skriptlang.skript.bukkit.recipes;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.*;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableBlastingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableCampfireRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableFurnaceRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableSmokingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe.MutableShapedRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe.MutableShapelessRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTransformRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTrimRecipe;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+
+/**
+ * Event class used with ExprSecCreateRecipe to allow the creation of MutableRecipe
+ * Allows elements used to determine if the recipe being created is of the required type
+ */
+public class CreateRecipeEvent extends Event {
+	private boolean errorInSection = false;
+	private final RecipeType recipeType;
+	private final MutableRecipe mutableRecipe;
+
+	public CreateRecipeEvent(NamespacedKey key, RecipeType recipeType) {
+		this.recipeType = recipeType;
+		this.mutableRecipe = recipeType.createMutableRecipe(key);
+	}
+
+	public void setErrorInSection() {
+		this.errorInSection = true;
+	}
+
+	public boolean getErrorInSection() {
+		return errorInSection;
+	}
+
+	public RecipeType getRecipeType() {
+		return recipeType;
+	}
+
+	public MutableRecipe getMutableRecipe() {
+		return mutableRecipe;
+	}
+
+	/**
+	 * Event correlating to creating a {@link MutableCraftingRecipe}
+	 */
+	public static class CraftingRecipeEvent extends CreateRecipeEvent {
+
+		public CraftingRecipeEvent(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		};
+
+		/**
+		 * Event correlating to creating a {@link MutableShapedRecipe}
+		 */
+		public static class ShapedRecipeEvent extends CraftingRecipeEvent {
+			public ShapedRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.SHAPED);
+			};
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableShapelessRecipe}
+		 */
+		public static class ShapelessRecipeEvent extends CraftingRecipeEvent {
+			public ShapelessRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.SHAPELESS);
+			};
+		}
+	}
+
+	/**
+	 * Event correlating to creating a {@link MutableCookingRecipe}
+	 */
+	public static class CookingRecipeEvent extends CreateRecipeEvent {
+
+		public CookingRecipeEvent(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		};
+
+		/**
+		 * Event correlating to creating a {@link MutableBlastingRecipe}
+		 */
+		public static class BlastingRecipeEvent extends CookingRecipeEvent {
+			public BlastingRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.BLASTING);
+			}
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableCampfireRecipe}
+		 */
+		public static class CampfireRecipeEvent extends CookingRecipeEvent {
+			public CampfireRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.CAMPFIRE);
+			}
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableFurnaceRecipe}
+		 */
+		public static class FurnaceRecipeEvent extends CookingRecipeEvent {
+			public FurnaceRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.FURNACE);
+			}
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableSmokingRecipe}
+		 */
+		public static class SmokingRecipeEvent extends CookingRecipeEvent {
+			public SmokingRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.SMOKING);
+			}
+		}
+	}
+
+	/**
+	 * Event correlating to creating a {@link MutableSmithingRecipe}
+	 */
+	public static class SmithingRecipeEvent extends CreateRecipeEvent {
+		public SmithingRecipeEvent(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableSmithingTransformRecipe}
+		 */
+		public static class SmithingTransformRecipeEvent extends SmithingRecipeEvent {
+			public SmithingTransformRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.SMITHING_TRANSFORM);
+			}
+		}
+
+		/**
+		 * Event correlating to creating a {@link MutableSmithingTrimRecipe}
+		 */
+		public static class SmithingTrimRecipeEvent extends SmithingRecipeEvent {
+			public SmithingTrimRecipeEvent(NamespacedKey key) {
+				super(key, RecipeType.SMITHING_TRIM);
+			}
+		}
+	}
+
+	/**
+	 * Event correlating to creating a {@link MutableStonecuttingRecipe}
+	 */
+	public static class StonecuttingRecipeEvent extends CreateRecipeEvent {
+		public StonecuttingRecipeEvent(NamespacedKey key) {
+			super(key, RecipeType.STONECUTTING);
+		}
+	}
+
+	/**
+	 * Event correlating to creating a {@link MutableTransmuteRecipe}
+	 */
+	public static class TransmuteRecipeEvent extends CreateRecipeEvent {
+		public TransmuteRecipeEvent(NamespacedKey key) {
+			super(key, RecipeType.TRANSMUTE);
+		}
+	}
+
+	@Override
+	public @NotNull HandlerList getHandlers() {
+		throw new IllegalStateException();
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/MutableRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/MutableRecipe.java
new file mode 100644
index 00000000000..1ce451a6353
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/MutableRecipe.java
@@ -0,0 +1,496 @@
+package org.skriptlang.skript.bukkit.recipes;
+
+import ch.njol.skript.Skript;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.*;
+import org.bukkit.inventory.recipe.CookingBookCategory;
+import org.bukkit.inventory.recipe.CraftingBookCategory;
+import org.jetbrains.annotations.NotNull;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Mutable Recipe class allowing data to be set before creation of a recipe.
+ */
+public abstract class MutableRecipe implements Recipe {
+
+	private final RecipeType recipeType;
+	private final NamespacedKey key;
+	private ItemStack result = new ItemStack(Material.AIR);
+	private final List<String> errors = new ArrayList<>();
+
+	public MutableRecipe(NamespacedKey key, RecipeType recipeType) {
+		this.key = key;
+		this.recipeType = recipeType;
+	}
+
+	public void setResult(ItemStack result) {
+		this.result = result;
+	}
+
+	@Override
+	public @NotNull ItemStack getResult() {
+		return result;
+	}
+
+	public NamespacedKey getKey() {
+		return this.key;
+	}
+
+	public RecipeType getRecipeType() {
+		return recipeType;
+	}
+
+	public void addError(String error) {
+		errors.add(error);
+	}
+
+	public List<String> getErrors() {
+		return errors;
+	}
+
+	public abstract Recipe create();
+
+	public abstract static class MutableCraftingRecipe extends MutableRecipe implements MutableGroupRecipe {
+		private final RecipeChoice[] ingredients = new RecipeChoice[9];
+		private String group;
+		private CraftingBookCategory category;
+
+		public MutableCraftingRecipe(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		}
+
+		@Override
+		public abstract Recipe create();
+
+		public void setIngredients(int placement, RecipeChoice item) {
+			ingredients[placement] = item;
+		}
+
+		public void setGroup(String group) {
+			this.group = group;
+		}
+
+		public void setCategory(CraftingBookCategory category) {
+			this.category = category;
+		}
+
+		public RecipeChoice[] getIngredients() {
+			return ingredients;
+		}
+
+		public String getGroup() {
+			return this.group;
+		}
+
+		public CraftingBookCategory getCategory() {
+			return this.category;
+		}
+
+		public static class MutableShapedRecipe extends MutableCraftingRecipe {
+
+			public MutableShapedRecipe(NamespacedKey key) {
+				super(key, RecipeType.SHAPED);
+			}
+
+			private static final char[] characters = new char[]{'a','b','c','d','e','f','g','h','i'};
+
+			@Override
+			public ShapedRecipe create() {
+				if (getResult().getType() == Material.AIR) {
+					addError("You must provide a result item when creating a recipe");
+					return null;
+				}
+				RecipeChoice[] ingredients = getIngredients();
+				if (ingredients.length == 0) {
+					addError("You must have at least 1 ingredient when creating a '" + getRecipeType() + "' recipe.");
+					return null;
+				}
+				ShapedRecipe shapedRecipe = new ShapedRecipe(getKey(), getResult());
+				shapedRecipe.shape("abc","def","ghi");
+				for (int i = 0; i < ingredients.length; i++) {
+					RecipeChoice thisChoice = ingredients[i];
+					if (thisChoice != null)
+						shapedRecipe.setIngredient(characters[i], thisChoice);
+				}
+				if (getGroup() != null)
+					shapedRecipe.setGroup(getGroup());
+				if (getCategory() != null)
+					shapedRecipe.setCategory(getCategory());
+				return shapedRecipe;
+			}
+
+		}
+
+		public static class MutableShapelessRecipe extends MutableCraftingRecipe {
+
+			public MutableShapelessRecipe(NamespacedKey key) {
+				super(key, RecipeType.SHAPELESS);
+			}
+
+			@Override
+			public ShapelessRecipe create() {
+				if (getResult().getType() == Material.AIR) {
+					addError("You must provide a result item when creating a recipe");
+					return null;
+				}
+				RecipeChoice[] ingredients = getIngredients();
+				if (ingredients.length == 0) {
+					addError("You must have at least 1 ingredient when creating a '" + getRecipeType() + "' recipe.");
+					return null;
+				}
+				ShapelessRecipe shapelessRecipe = new ShapelessRecipe(getKey(), getResult());
+				for (RecipeChoice thisChoice : ingredients) {
+					if (thisChoice != null)
+						shapelessRecipe.addIngredient(thisChoice);
+				}
+				if (getGroup() != null)
+					shapelessRecipe.setGroup(getGroup());
+				if (getCategory() != null)
+					shapelessRecipe.setCategory(getCategory());
+				return shapelessRecipe;
+			}
+		}
+	}
+
+	public static class MutableCookingRecipe extends MutableRecipe implements MutableGroupRecipe {
+		private RecipeChoice input;
+		private String group;
+		private CookingBookCategory category;
+		private int cookingTime = 10;
+		private float experience = 0;
+
+		public MutableCookingRecipe(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		}
+
+		public void setInput(RecipeChoice input) {
+			this.input = input;
+		}
+
+		public void setGroup(String group) {
+			this.group = group;
+		}
+
+		public void setCategory(CookingBookCategory category) {
+			this.category = category;
+		}
+
+		public void setCookingTime(int cookingTime) {
+			this.cookingTime = cookingTime;
+		}
+
+		public void setExperience(float experience) {
+			this.experience = experience;
+		}
+
+		public RecipeChoice getInput() {
+			return input;
+		}
+
+		public String getGroup() {
+			return group;
+		}
+
+		public CookingBookCategory getCategory() {
+			return category;
+		}
+
+		public int getCookingTime() {
+			return cookingTime;
+		}
+
+		public float getExperience() {
+			return experience;
+		}
+
+		@Override
+		public CookingRecipe<?> create() {
+			if (getResult().getType() == Material.AIR) {
+				addError("You must provide a result item when creating a recipe");
+				return null;
+			}
+			if (getInput() == null) {
+				addError("You must provide an input item when creating a " + getRecipeType() + " recipe.");
+				return null;
+			}
+			var recipe = switch (getRecipeType()) {
+				case BLASTING -> new BlastingRecipe(getKey(), getResult(), getInput(), getExperience(), getCookingTime());
+				case FURNACE -> new FurnaceRecipe(getKey(), getResult(), getInput(), getExperience(), getCookingTime());
+				case SMOKING -> new SmokingRecipe(getKey(), getResult(), getInput(), getExperience(), getCookingTime());
+				case CAMPFIRE -> new CampfireRecipe(getKey(), getResult(), getInput(), getExperience(), getCookingTime());
+				default -> throw new IllegalStateException("Unexpected value: " + getRecipeType());
+			};
+			if (getGroup() != null)
+				recipe.setGroup(getGroup());
+			if (getCategory() != null)
+				recipe.setCategory(getCategory());
+			return recipe;
+		}
+
+		public static class MutableBlastingRecipe extends MutableCookingRecipe {
+			public MutableBlastingRecipe(NamespacedKey key) {
+				super(key, RecipeType.BLASTING);
+			}
+
+			@Override
+			public BlastingRecipe create() {
+				return (BlastingRecipe) super.create();
+			}
+		}
+
+		public static class MutableFurnaceRecipe extends MutableCookingRecipe {
+			public MutableFurnaceRecipe(NamespacedKey key) {
+				super(key, RecipeType.FURNACE);
+			}
+
+			@Override
+			public FurnaceRecipe create() {
+				return (FurnaceRecipe) super.create();
+			}
+		}
+
+		public static class MutableSmokingRecipe extends MutableCookingRecipe {
+			public MutableSmokingRecipe(NamespacedKey key) {
+				super(key, RecipeType.SMOKING);
+			}
+
+			@Override
+			public SmokingRecipe create() {
+				return (SmokingRecipe) super.create();
+			}
+		}
+
+		public static class MutableCampfireRecipe extends MutableCookingRecipe {
+			public MutableCampfireRecipe(NamespacedKey key) {
+				super(key, RecipeType.CAMPFIRE);
+			}
+
+			@Override
+			public CampfireRecipe create() {
+				return (CampfireRecipe) super.create();
+			}
+		}
+
+	}
+
+	public static class MutableSmithingRecipe extends MutableRecipe {
+
+		private static final boolean SUPPORTS_COPY_NBT = Skript.methodExists(SmithingRecipe.class, "willCopyNbt");
+		private static final boolean SUPPORTS_COPY_DATA = Skript.methodExists(SmithingRecipe.class, "willCopyDataComponents");
+		private static final boolean RUNNING_1_20_0 = Skript.isRunningMinecraft(1, 20, 0);
+
+		private RecipeChoice base;
+		private RecipeChoice template;
+		private RecipeChoice addition;
+		private Boolean copyData = null;
+
+		public MutableSmithingRecipe(NamespacedKey key, RecipeType recipeType) {
+			super(key, recipeType);
+		}
+
+		public void setBase(RecipeChoice base) {
+			this.base = base;
+		}
+
+		public void setTemplate(RecipeChoice template)  {
+			this.template = template;
+		}
+
+		public void setAddition(RecipeChoice addition) {
+			this.addition = addition;
+		}
+
+		public void setCopyData(Boolean copyData) {
+			this.copyData = copyData;
+		}
+
+		public RecipeChoice getBase() {
+			return base;
+		}
+
+		public RecipeChoice getTemplate() {
+			return template;
+		}
+
+		public RecipeChoice getAddition() {
+			return addition;
+		}
+
+
+		public Boolean willCopyData() {
+			return copyData;
+		}
+
+		@Override
+		public SmithingRecipe create() {
+			if (getResult().getType() == Material.AIR) {
+				addError("You must provide a result item when creating a recipe");
+				return null;
+			}
+			RecipeChoice base = getBase(), addition = getAddition();
+			if (base == null) {
+				addError("You must provide a base item when creating a smithing recipe.");
+				return null;
+			}
+			if (addition == null) {
+				addError("You must provide an additional item when creating a smithing recipe.");
+				return null;
+			}
+			if ((SUPPORTS_COPY_NBT || SUPPORTS_COPY_DATA) && copyData != null)
+				return new SmithingRecipe(getKey(), getResult(), getBase(), getAddition(), copyData);
+			return new SmithingRecipe(getKey(), getResult(), getBase(), getAddition());
+		}
+
+		public static class MutableSmithingTransformRecipe extends MutableSmithingRecipe {
+			public MutableSmithingTransformRecipe(NamespacedKey key) {
+				super(key, RecipeType.SMITHING_TRANSFORM);
+			}
+
+			@Override
+			public SmithingTransformRecipe create() {
+				if (getResult().getType() == Material.AIR) {
+					addError("You must provide a result item when creating a recipe");
+					return null;
+				}
+				RecipeChoice base = getBase(), template = getTemplate(), addition = getAddition();
+				if (base == null) {
+					addError("You must provide a base item when creating a smithing recipe.");
+					return null;
+				}
+				if (addition == null) {
+					addError("You must provide an additional item when creating a smithing recipe.");
+					return null;
+				}
+				if (template == null) {
+					addError("You must provide a template item when creating a smithing recipe.");
+					return null;
+				}
+				if (RUNNING_1_20_0 && (SUPPORTS_COPY_NBT || SUPPORTS_COPY_DATA) && willCopyData() != null)
+					return new SmithingTransformRecipe(getKey(), getResult(), getTemplate(), getBase(), getAddition(), willCopyData());
+				return new SmithingTransformRecipe(getKey(), getResult(), getTemplate(), getBase(), getAddition());
+			}
+		}
+
+		public static class MutableSmithingTrimRecipe extends MutableSmithingRecipe {
+			public MutableSmithingTrimRecipe(NamespacedKey key) {
+				super(key, RecipeType.SMITHING_TRIM);
+			}
+
+			@Override
+			public SmithingTrimRecipe create() {
+				RecipeChoice base = getBase(), template = getTemplate(), addition = getAddition();
+				if (base == null) {
+					addError("You must provide a base item when creating a smithing recipe.");
+					return null;
+				}
+				if (addition == null) {
+					addError("You must provide an additional item when creating a smithing recipe.");
+					return null;
+				}
+				if (template == null) {
+					addError("You must provide a template item when creating a smithing recipe.");
+					return null;
+				}
+				if (RUNNING_1_20_0 && (SUPPORTS_COPY_NBT || SUPPORTS_COPY_DATA) && willCopyData() != null)
+					return new SmithingTrimRecipe(getKey(), getTemplate(), getBase(), getAddition(), willCopyData());
+				return new SmithingTrimRecipe(getKey(), getTemplate(), getBase(), getAddition());
+			}
+		}
+	}
+
+	public static class MutableStonecuttingRecipe extends MutableRecipe implements MutableGroupRecipe {
+		private RecipeChoice input;
+		private String group;
+
+		public MutableStonecuttingRecipe(NamespacedKey key) {
+			super(key, RecipeType.STONECUTTING);
+		}
+
+		public void setInput(RecipeChoice input) {
+			this.input = input;
+		}
+
+		public RecipeChoice getInput() {
+			return input;
+		}
+
+		public void setGroup(String group) {
+			this.group =  group;
+		}
+
+		public String getGroup() {
+			return group;
+		}
+
+		@Override
+		public StonecuttingRecipe create() {
+			if (getResult().getType() == Material.AIR) {
+				addError("You must provide a result item when creating a recipe");
+				return null;
+			}
+			if (getInput() == null) {
+				addError("You must provide an input item when creating a stonecutting recipe.");
+				return null;
+			}
+			StonecuttingRecipe recipe = new StonecuttingRecipe(getKey(), getResult(), input);
+			if (group != null && !group.isEmpty())
+				recipe.setGroup(group);
+			return recipe;
+		}
+	}
+
+	public static class MutableTransmuteRecipe extends MutableRecipe {
+
+		private RecipeChoice input, material;
+
+		public MutableTransmuteRecipe(NamespacedKey key) {
+			super(key, RecipeType.TRANSMUTE);
+		}
+
+		public void setInput(RecipeChoice input) {
+			this.input = input;
+		}
+
+		public void setMaterial(RecipeChoice material) {
+			this.material = material;
+		}
+
+		public RecipeChoice getInput() {
+			return input;
+		}
+
+		public RecipeChoice getMaterial() {
+			return material;
+		}
+
+		@Override
+		public TransmuteRecipe create() {
+			if (getResult().getType() == Material.AIR) {
+				addError("You must provide a result item when creating a recipe");
+				return null;
+			}
+			if (getInput() == null) {
+				addError("You must provide an input item when creating a transmute recipe.");
+				return null;
+			}
+			if (getMaterial() == null) {
+				addError("You must provide a transmute item when creating a transmute recipe.");
+				return null;
+			}
+			return new TransmuteRecipe(getKey(), getResult().getType(), input, material);
+		}
+	}
+
+	/**
+	 * Interface used to have mutable recipes, that are able to have groups, to be combined
+	 */
+	public interface MutableGroupRecipe {
+		String getGroup();
+		void setGroup(String object);
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeCategory.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeCategory.java
new file mode 100644
index 00000000000..139655c3e07
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeCategory.java
@@ -0,0 +1,38 @@
+package org.skriptlang.skript.bukkit.recipes;
+
+import org.bukkit.inventory.recipe.CookingBookCategory;
+import org.bukkit.inventory.recipe.CraftingBookCategory;
+
+/**
+ * Enum for converting Bukkit recipe categories into one usable by Skript.
+ */
+public enum RecipeCategory {
+
+	CRAFTING_BUILDING(CraftingBookCategory.BUILDING),
+	CRAFTING_EQUIPMENT(CraftingBookCategory.EQUIPMENT),
+	CRAFTING_MISC(CraftingBookCategory.MISC),
+	CRAFTING_REDSTONE(CraftingBookCategory.REDSTONE),
+
+	COOKING_BLOCKS(CookingBookCategory.BLOCKS),
+	COOKING_FOOD(CookingBookCategory.FOOD),
+	COOKING_MISC(CookingBookCategory.MISC);
+
+	private final Enum<?> category;
+
+	RecipeCategory(Enum<?> category) {
+		this.category = category;
+	}
+
+	public Enum<?> getCategory() {
+		return category;
+	}
+
+	public static RecipeCategory convertBukkitToSkript(Enum<?> category) {
+		for (RecipeCategory recipeCategory : RecipeCategory.values()) {
+			if (recipeCategory.category.equals(category))
+				return recipeCategory;
+		}
+		return null;
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeModule.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeModule.java
new file mode 100644
index 00000000000..652dec41d1b
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeModule.java
@@ -0,0 +1,72 @@
+package org.skriptlang.skript.bukkit.recipes;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.ClassInfo;
+import ch.njol.skript.classes.EnumClassInfo;
+import ch.njol.skript.expressions.base.EventValueExpression;
+import ch.njol.skript.registrations.Classes;
+import ch.njol.skript.registrations.EventValues;
+import org.bukkit.Bukkit;
+import org.bukkit.event.inventory.CraftItemEvent;
+import org.bukkit.event.inventory.PrepareItemCraftEvent;
+import org.bukkit.event.player.PlayerRecipeDiscoverEvent;
+import org.bukkit.inventory.Recipe;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+import org.skriptlang.skript.lang.comparator.Comparators;
+import org.skriptlang.skript.lang.comparator.Relation;
+
+import java.io.IOException;
+
+public class RecipeModule {
+
+	public static void load() throws IOException {
+		Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.recipes", "elements");
+
+		// --- CLASSES --- //
+
+		Classes.registerClass(new ClassInfo<>(Recipe.class, "recipe")
+			.user("recipes?")
+			.name("Recipe")
+			.description("Represents a recipe.")
+			.usage("recipes")
+			.since("INSERT VERSION")
+			.defaultExpression(new EventValueExpression<>(Recipe.class)));
+
+		Classes.registerClass(new EnumClassInfo<>(RecipeCategory.class, "recipecategory", "recipe categories")
+			.user("recipe ?categor(y|ies)")
+			.name("Recipe Category")
+			.description("Represents the different categories of recipes.")
+			.since("INSERT VERSION")
+		);
+
+		Classes.registerClass(new EnumClassInfo<>(RecipeType.class, "recipetype", "recipe types", false)
+			.user("recipe ?types?")
+			.name("Recipe Type")
+			.description("Represents the type of a recipe.")
+			.since("INSERT VERSION")
+		);
+
+		// --- COMPARATORS --- //
+
+		Comparators.registerComparator(RecipeType.class, RecipeType.class, (type1, type2) -> {
+			if (type1.getRecipeClass() != null && type2.getRecipeClass() != null)
+				return Relation.get(type2.getRecipeClass().isAssignableFrom(type1.getRecipeClass()));
+			return Relation.NOT_EQUAL;
+		});
+
+		// --- EVENT VALUES --- //
+
+		//PrepareItemCraftEvent
+		EventValues.registerEventValue(PrepareItemCraftEvent.class, Recipe.class, PrepareItemCraftEvent::getRecipe);
+
+		//CraftItemEvent
+		EventValues.registerEventValue(CraftItemEvent.class, Recipe.class, CraftItemEvent::getRecipe);
+
+		// PlayerRecipeDiscoverEvent
+		EventValues.registerEventValue(PlayerRecipeDiscoverEvent.class, Recipe.class,
+			event -> Bukkit.getRecipe(event.getRecipe())
+		);
+
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeUtils.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeUtils.java
new file mode 100644
index 00000000000..29157a917db
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/RecipeUtils.java
@@ -0,0 +1,305 @@
+package org.skriptlang.skript.bukkit.recipes;
+
+import ch.njol.skript.Skript;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.*;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.*;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CookingRecipeEvent.BlastingRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CookingRecipeEvent.CampfireRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CookingRecipeEvent.FurnaceRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CookingRecipeEvent.SmokingRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CraftingRecipeEvent.ShapedRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CraftingRecipeEvent.ShapelessRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTransformRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTrimRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableBlastingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableCampfireRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableFurnaceRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe.MutableSmokingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe.MutableShapedRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe.MutableShapelessRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTransformRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTrimRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableStonecuttingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableTransmuteRecipe;
+import org.skriptlang.skript.bukkit.recipes.elements.ExprSecCreateRecipe;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utils used for getting data from {@link RecipeType}
+ */
+public class RecipeUtils {
+
+	/**
+	 * Enum for storing all types of Recipes (until Bukkit makes an enum or registry)
+	 */
+	public enum RecipeType {
+		SHAPED(ShapedRecipe.class, ShapedRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new ShapedRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableShapedRecipe(key);
+			}
+
+		},
+		SHAPELESS(ShapelessRecipe.class, ShapelessRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new ShapelessRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableShapelessRecipe(key);
+			}
+
+		},
+		// TODO: Remove method and apply class directly when MC version is raised to 1.21.2+
+		TRANSMUTE(getTransmuteRecipeClass(), TransmuteRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new TransmuteRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableTransmuteRecipe(key);
+			}
+
+		},
+		// TODO: Remove method and apply class directly when MC version is raised to 1.20.1+
+		// Having 'CRAFTING' under the subclasses allows for proper ExprRecipeType
+		CRAFTING(getCraftingRecipeClass(), CraftingRecipeEvent.class) {
+			// A 'CraftingRecipeEvent' should never be created
+			@Override
+			public @Nullable CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return null;
+			}
+			// A 'MutableCraftingRecipe' should never be created
+			@Override
+			public @Nullable MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return null;
+			}
+
+		},
+		BLASTING(BlastingRecipe.class, BlastingRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new BlastingRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableBlastingRecipe(key);
+			}
+
+		},
+		FURNACE(FurnaceRecipe.class, FurnaceRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new FurnaceRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableFurnaceRecipe(key);
+			}
+
+		},
+		CAMPFIRE(CampfireRecipe.class, CampfireRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new CampfireRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableCampfireRecipe(key);
+			}
+
+		},
+		SMOKING(SmokingRecipe.class, SmokingRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new SmokingRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableSmokingRecipe(key);
+			}
+
+		},
+		// Having 'COOKING' under the subclasses allows for proper ExprRecipeType
+		COOKING(CookingRecipe.class, CookingRecipeEvent.class) {
+			// A 'CookingRecipeEvent' should never be created
+			@Override
+			public @Nullable CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return null;
+			}
+			// A 'MutableCookingRecipe' should never be created
+			@Override
+			public @Nullable MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return null;
+			}
+
+		},
+		SMITHING_TRANSFORM(SmithingTransformRecipe.class, SmithingTransformRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new SmithingTransformRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableSmithingTransformRecipe(key);
+			}
+
+		},
+		SMITHING_TRIM(SmithingTrimRecipe.class, SmithingTrimRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new SmithingTrimRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableSmithingTrimRecipe(key);
+			}
+
+		},
+		// Having 'SMITHING' under the subclasses allows for proper ExprRecipeType
+		SMITHING(SmithingRecipe.class, SmithingRecipeEvent.class) {
+			// TODO: return null after minimum support version is raised to 1.20+
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new SmithingRecipeEvent(key, this);
+			}
+			// TODO: return null after minimum support version is raised to 1.20+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableSmithingRecipe(key, this);
+			}
+
+		},
+		STONECUTTING(StonecuttingRecipe.class, StonecuttingRecipeEvent.class) {
+			@Override
+			public CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return new StonecuttingRecipeEvent(key);
+			}
+
+			@Override
+			public MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return new MutableStonecuttingRecipe(key);
+			}
+		},
+		COMPLEX(ComplexRecipe.class, null) {
+			// A 'CompleRecipeEvent' does not exist
+			@Override
+			public @Nullable CreateRecipeEvent createRecipeEvent(NamespacedKey key) {
+				return null;
+			}
+			// A 'MutableComplexRecipe' does not exist
+			@Override
+			public @Nullable MutableRecipe createMutableRecipe(NamespacedKey key) {
+				return null;
+			}
+		};
+
+		/**
+		 * Create a {@link CreateRecipeEvent} used for {@link ExprSecCreateRecipe}
+		 * @param key The key to create a recipe
+		 * @return
+		 */
+		public abstract @Nullable CreateRecipeEvent createRecipeEvent(NamespacedKey key);
+
+		/**
+		 * Create a {@link MutableRecipe} designed to set data before final creation.
+		 * Used for {@link ExprSecCreateRecipe}.
+		 * @param key
+		 * @return
+		 */
+		public abstract @Nullable MutableRecipe createMutableRecipe(NamespacedKey key);
+
+		private final @Nullable Class<? extends Recipe> recipeClass;
+		private final @Nullable Class<? extends Event> eventClass;
+
+		RecipeType(@Nullable Class<? extends Recipe> recipeClass, @Nullable Class<? extends Event> eventClass) {
+			this.recipeClass = recipeClass;
+			this.eventClass = eventClass;
+		}
+
+		/**
+		 * Gets the Bukkit recipe type class.
+		 * @return Bukkit recipe class
+		 */
+		public @Nullable Class<? extends Recipe> getRecipeClass() {
+			return recipeClass;
+		}
+
+		/**
+		 * Gets the custom event used when creating a new recipe.
+		 * @return Custom event class
+		 */
+		public @Nullable Class<? extends Event> getEventClass() {
+			return eventClass;
+		}
+
+		// Due to 1.19 not having 'CraftingRecipe.class'
+		private static @Nullable Class<? extends Recipe> getCraftingRecipeClass() {
+			if (Skript.classExists("org.bukkit.inventory.CraftingRecipe"))
+				return CraftingRecipe.class;
+			return null;
+		}
+
+		private static @Nullable Class<? extends Recipe> getTransmuteRecipeClass() {
+			if (Skript.classExists("org.bukkit.inventory.TransmuteRecipe"))
+				return TransmuteRecipe.class;
+			return null;
+		}
+
+	}
+
+	private static final Map<Class<? extends Recipe>, RecipeType> recipeClassConverter = new HashMap<>();
+
+	static {
+		for (RecipeType recipeType : RecipeType.values()) {
+			if (recipeType.recipeClass != null)
+				recipeClassConverter.put(recipeType.recipeClass, recipeType);
+		}
+	}
+
+	/**
+	 * Gets {@link RecipeType} from provided recipe class.
+	 *
+	 * Note: MC versions below 1.20 converts Smithing Transform and Smithing Trim Recipes to a Smithing Recipe, causing the returned RecipeType to be Smithing.
+	 * @param providedClass Bukkit recipe class
+	 * @return Recipe Type
+	 */
+	public static @Nullable RecipeType getRecipeType(@NotNull Class<? extends Recipe> providedClass) {
+		RecipeType recipeType = recipeClassConverter.get(providedClass);
+		if (recipeType == null)
+			recipeType = recipeClassConverter.get(providedClass.getSuperclass());
+		return recipeType;
+	}
+
+	/**
+	 * Gets {@link RecipeType} from provided recipe.
+	 * @param providedRecipe Recipe
+	 * @return Recipe Type
+	 */
+	public static @Nullable RecipeType getRecipeType(@NotNull Recipe providedRecipe) {
+		return getRecipeType(providedRecipe.getClass());
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondDiscoveredRecipes.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondDiscoveredRecipes.java
new file mode 100644
index 00000000000..28e6360ff6f
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondDiscoveredRecipes.java
@@ -0,0 +1,67 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import org.bukkit.Keyed;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+
+@Name("Has Discovered Recipe")
+@Description("Checks whether the players have discovered a recipe.")
+@Examples({
+	"if player has discovered recipe \"custom_recipe\":",
+		"\tgive player 1 diamond",
+	"",
+	"if all players have not discovered recipe \"custom_recipe\":",
+		"\tkill all players",
+})
+@Since("INSERT VERSION")
+public class CondDiscoveredRecipes extends Condition {
+
+	static {
+		Skript.registerCondition(CondDiscoveredRecipes.class,
+			"%players% (has|have) (discovered|unlocked) [the] [recipe[s]] %recipes%",
+			"%players% (hasn't|has not|haven't|have not) (discovered|unlocked) [the] [recipe[s]] %recipes%");
+	}
+
+	private Expression<Player> exprPlayer;
+	private Expression<? extends Recipe> exprRecipe;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		//noinspection unchecked
+		exprPlayer = (Expression<Player>) exprs[0];
+		//noinspection unchecked
+		exprRecipe = (Expression<? extends Recipe>) exprs[1];
+		setNegated(matchedPattern == 1);
+		return true;
+	}
+
+	@Override
+	public boolean check(Event event) {
+		Recipe[] recipes = exprRecipe.getArray(event);
+		return exprPlayer.check(event, player ->
+			SimpleExpression.check(recipes, recipe -> {
+				assert recipe instanceof Keyed;
+				Keyed recipeKey = (Keyed) recipe;
+				return player.hasDiscoveredRecipe(recipeKey.getKey());
+			}, isNegated(), exprRecipe.getAnd())
+		);
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return exprPlayer.toString(event, debug) + (isNegated() ? " have not" : " have") + " found recipes " + exprRecipe.toString(event, debug);
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondSmithingCopy.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondSmithingCopy.java
new file mode 100644
index 00000000000..3369cbe36c1
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/CondSmithingCopy.java
@@ -0,0 +1,89 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.*;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.bukkit.inventory.SmithingRecipe;
+import org.bukkit.inventory.SmithingTransformRecipe;
+import org.bukkit.inventory.SmithingTrimRecipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTransformRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTrimRecipe;
+
+@Name("Recipe Allows Data Copy")
+@Description({
+	"Checks whether the recipe allows copying data from the base item to the result item of the recipe.",
+	"",
+	"This condition can only be used with smithing recipes on PaperMC version 1.19",
+	"This condition can be used with smithing transform and smithing trim recipes on PaperMC version 1.20.0+",
+})
+@Examples({
+	"loop the server's recipes:",
+		"\tif all:",
+			"\t\tthe recipe type of loop-value is a smithing recipe",
+			"\t\tthe loop-value allows data copying",
+		"\tthen:",
+			"\t\tbroadcast loop-value"
+})
+@RequiredPlugins("Paper")
+@Since("INSERT VERSION")
+public class CondSmithingCopy extends Condition {
+
+	private static final boolean SUPPORTS_COPY_NBT = Skript.methodExists(SmithingRecipe.class, "willCopyNbt");
+	private static final boolean SUPPORTS_COPY_DATA = Skript.methodExists(SmithingRecipe.class, "willCopyDataComponents");
+	private static final boolean RUNNING_1_20_0 =  Skript.isRunningMinecraft(1, 20, 0);
+
+	static {
+		if (SUPPORTS_COPY_NBT || SUPPORTS_COPY_DATA) {
+			Skript.registerCondition(CondSmithingCopy.class, ConditionType.PROPERTY,
+				"[the] %recipes% allow[s] [item] data (copying|transferring)",
+				"[the] %recipes% allow[s] [the] [item] data to be (copied|transferred)",
+				"[the] %recipes% (don't|do not|doesn't|does not) allow[s] [item] data (copying|transferring)",
+				"[the] %recipes% (don't|do not|doesn't|does not) allow[s] [the] [item] data to be (copied|transferred)");
+		}
+	}
+
+	private Expression<Recipe> exprRecipe;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		//noinspection unchecked
+		exprRecipe = (Expression<Recipe>) exprs[0];
+		setNegated(matchedPattern >= 2);
+		return true;
+	}
+
+	@Override
+	public boolean check(Event event) {
+		return exprRecipe.check(event, recipe -> {
+			if (recipe instanceof MutableSmithingRecipe mutableRecipe) {
+				if (!RUNNING_1_20_0 && (mutableRecipe instanceof MutableSmithingTransformRecipe || mutableRecipe instanceof MutableSmithingTrimRecipe)) {
+					return isNegated();
+				}
+				return mutableRecipe.willCopyData();
+			} else if (recipe instanceof SmithingRecipe smithingRecipe) {
+				if (!RUNNING_1_20_0 && (smithingRecipe instanceof SmithingTransformRecipe || smithingRecipe instanceof SmithingTrimRecipe)) {
+					return isNegated();
+				}
+				if (SUPPORTS_COPY_DATA) {
+					return smithingRecipe.willCopyDataComponents();
+				} else if (SUPPORTS_COPY_NBT) {
+					return smithingRecipe.willCopyNbt();
+				}
+			}
+			return isNegated();
+		}, isNegated());
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return exprRecipe.toString(event, debug) + (isNegated() ? "does not" : "") + " allow data copying";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffDiscoverRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffDiscoverRecipe.java
new file mode 100644
index 00000000000..bc901c8e5b0
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffDiscoverRecipe.java
@@ -0,0 +1,83 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.util.Kleenean;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+
+@Name("Discover Recipe")
+@Description("Makes the specified players discover or forget the recipes.")
+@Examples({
+	"make player discover recipe \"my_recipe\"",
+	"make player undiscover recipe \"my_recipe\"",
+	"unlock recipe \"my_recipe\" for all players",
+	"lock recipe \"my_recipe\" for all players"
+})
+@Since("INSERT VERSION")
+public class EffDiscoverRecipe extends Effect {
+
+	static {
+		Skript.registerEffect(EffDiscoverRecipe.class,
+			"make %players% (discover|unlock) [the] [recipe[s]] %recipes%",
+			"make %players% forget [the] [recipe[s]] %recipes%",
+			"(discover|unlock) [the] [recipe[s]] %recipes% for %players%",
+			"(undiscover|lock) [the] [recipe[s]] %recipes% for %players%");
+	}
+
+	private Expression<Player> players;
+	private Expression<? extends Recipe> recipes;
+	private boolean isDiscover = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		isDiscover = matchedPattern == 0 || matchedPattern == 2;
+		//noinspection unchecked
+		players = (Expression<Player>) (matchedPattern <= 1 ? exprs[0] : exprs[1]);
+		//noinspection unchecked
+		recipes = (Expression<? extends Recipe>) (matchedPattern <= 1 ? exprs[1] : exprs[0]);
+		return true;
+	}
+
+	@Override
+	protected void execute(Event event) {
+		Player[] playerArray = players.getArray(event);
+		for (Object object : recipes.getArray(event)) {
+			if (object instanceof Recipe actualRecipe && actualRecipe instanceof Keyed recipeKey) {
+				NamespacedKey key = recipeKey.getKey();
+				for (Player player : playerArray) {
+					if (isDiscover) {
+						player.discoverRecipe(key);
+					} else {
+						player.undiscoverRecipe(key);
+					}
+				}
+			}
+		}
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug);
+		builder.append("make", players);
+		if (isDiscover) {
+			builder.append("discover");
+		} else {
+			builder.append("undiscover");
+		}
+		builder.append("recipes", recipes);
+		return builder.toString();
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffRemoveRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffRemoveRecipe.java
new file mode 100644
index 00000000000..1c5b8a207e5
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffRemoveRecipe.java
@@ -0,0 +1,59 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.Bukkit;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+
+@Name("Remove Recipe")
+@Description({
+	"Remove the specified recipes from the server.",
+	"This will cause all players who have discovered the recipe to forget it.",
+	"Removing a minecraft recipe is not persistent across server restart."
+})
+@Examples("remove the recipe \"my_recipe\" from the server")
+@Since("INSERT VERSION")
+public class EffRemoveRecipe extends Effect {
+
+	static {
+		Skript.registerEffect(EffRemoveRecipe.class,
+			"(remove|delete) [the] [recipe[s]] %recipes% [from [the] server]");
+	}
+
+	private Expression<? extends Recipe> recipes;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		//noinspection unchecked
+		recipes = (Expression<? extends Recipe>) exprs[0];
+		return true;
+	}
+
+	@Override
+	protected void execute(Event event) {
+		for (Recipe recipe : recipes.getArray(event)) {
+			if (recipe instanceof Keyed recipeKey) {
+				NamespacedKey key = recipeKey.getKey();
+				if (Bukkit.getRecipe(key) != null)
+					Bukkit.removeRecipe(key);
+			}
+		}
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return "remove recipes " + recipes.toString(event, debug) + " from the server";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffSmithingCopy.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffSmithingCopy.java
new file mode 100644
index 00000000000..0601efd0882
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EffSmithingCopy.java
@@ -0,0 +1,86 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.*;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.SmithingRecipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTransformRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTrimRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe;
+
+@Name("Allow Data Copy")
+@Description({
+	"Allow the recipe to copy data from the base item to the result item.",
+	"",
+	"This condition can only be used with smithing recipes on PaperMC version 1.19",
+	"This condition can be used with smithing transform and smithing trim recipes on PaperMC version 1.20.0+",
+})
+@Examples({
+	"set {_recipe} to a new smithing transform recipe:",
+		"\tset the recipe base item to a raw iron named \"Impure Iron\"",
+		"\tset the recipe additional item to an iron nugget named \"Pure Iron\"",
+		"\tset the recipe result item to an iron ingot named \"Heavenly Iron\"",
+		"\tallow the item data to be copied from the base item to the result item"
+})
+@RequiredPlugins("Paper")
+@Since("INSERT VERSION")
+public class EffSmithingCopy extends Effect {
+
+	private static final boolean SUPPORTS_COPY_NBT = Skript.methodExists(SmithingRecipe.class, "willCopyNbt");
+	private static final boolean SUPPORTS_COPY_DATA = Skript.methodExists(SmithingRecipe.class, "willCopyDataComponents");
+	private static final boolean RUNNING_1_20_0 = Skript.isRunningMinecraft(1, 20, 0);
+
+	static {
+		if (SUPPORTS_COPY_NBT || SUPPORTS_COPY_DATA) {
+			Skript.registerEffect(EffSmithingCopy.class,
+				"allow [the] [item] data to be (copied|transferred) from [the] base item to [the] result item",
+				"allow (copying|transferring) [the] [item] data from [the] base item to [the] result item",
+				"disallow [the] [item] data to be (copied|transferred) from [the] base item to [the] result item",
+				"disallow (copying|transferring) [the] [item] data from [the] base item to [the] result item");
+		}
+	}
+
+	private boolean copyData;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (!getParser().isCurrentEvent(CreateRecipeEvent.class)) {
+			Skript.error("This effect can only be used when creating a new recipe.");
+			return false;
+		} else if (!getParser().isCurrentEvent(SmithingRecipeEvent.class)) {
+			if (RUNNING_1_20_0) {
+				Skript.error("This effect can only be used when creating a new Smithing, Smithing Transform or Smithing Trim Recipe.");
+			} else {
+				Skript.error("This effect can only be used when creating a new Smithing Recipe.");
+			}
+			return false;
+		} else if (!RUNNING_1_20_0 && (getParser().isCurrentEvent(SmithingTransformRecipeEvent.class, SmithingTrimRecipeEvent.class))) {
+			Skript.error("Your Paper MC version only allows this effect for creating Smithing Recipes and not Smithing Transform or Smithing Trim Recipes.");
+			return false;
+		}
+		copyData = matchedPattern <= 1;
+		return true;
+	}
+
+	@Override
+	protected void execute(Event event) {
+		if (!(event instanceof SmithingRecipeEvent smithingEvent))
+			return;
+
+		MutableSmithingRecipe mutableRecipe = (MutableSmithingRecipe) smithingEvent.getMutableRecipe();
+		mutableRecipe.setCopyData(copyData);
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return (copyData ? "allow" : "disallow") + " copying the data from the base item to the result item";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EvtDiscoverRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EvtDiscoverRecipe.java
new file mode 100644
index 00000000000..167740a2301
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/EvtDiscoverRecipe.java
@@ -0,0 +1,64 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.Literal;
+import ch.njol.skript.lang.SkriptEvent;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.event.player.PlayerRecipeDiscoverEvent;
+import org.jetbrains.annotations.Nullable;
+
+@Name("Player Discover Recipe")
+@Description("Called when a player discovers a new recipe.")
+@Examples({
+	"on player discovered recipe:",
+		"\tbroadcast event-recipe",
+	"",
+	"on discovered recipe of \"my_recipe\":",
+		"\tbroadcast event-recipe"
+})
+@Since("INSERT VERSION")
+public class EvtDiscoverRecipe extends SkriptEvent {
+
+	static {
+		Skript.registerEvent("Player Discover Recipe", EvtDiscoverRecipe.class, PlayerRecipeDiscoverEvent.class,
+			"[player] discover[ed|ing] recipe[s] [of %-strings%]",
+			"[player] recipe discover[ed|ing] [of %-strings%]"
+		);
+	}
+
+	private @Nullable Expression<String> recipes;
+
+	@Override
+	public boolean init(Literal<?>[] exprs, int matchedPattern, ParseResult parseResult) {
+		//noinspection unchecked
+		recipes = (Expression<String>) exprs[0];
+		return true;
+	}
+
+	@Override
+	public boolean check(Event event) {
+		if (!(event instanceof PlayerRecipeDiscoverEvent discoverEvent))
+			return false;
+		if (recipes == null)
+			return true;
+		NamespacedKey eventRecipeKey = discoverEvent.getRecipe();
+		for (String string : recipes.getArray(event)) {
+			if (eventRecipeKey.equals(NamespacedKey.fromString(string, Skript.getInstance())))
+				return true;
+		}
+		return false;
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return "player discovered recipes" + (recipes == null ? "" : " of " + recipes.toString(event, debug));
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprAllRecipes.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprAllRecipes.java
new file mode 100644
index 00000000000..01fe5fa3e34
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprAllRecipes.java
@@ -0,0 +1,212 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.SyntaxStringBuilder;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import ch.njol.util.coll.iterator.CheckedIterator;
+import org.bukkit.Bukkit;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@Name("All Recipes")
+@Description({
+	"Retrieve all recipes registered in the server. Includes the options to:",
+	"<ul>",
+	"<li>Get only recipes provided by Minecraft or custom recipes</li>",
+	"<li>Specific recipe types</li>",
+	"<li>For specific items</li>",
+	"</ul>",
+	"",
+	"When resetting the server recipes, all custom recipes from any plugin will be removed, regardless of specifying additional data. "
+		+ "Only vanilla recipes will remain.",
+	"When deleting the server recipes, you are allowed to delete recipes using the options listed above.",
+})
+@Examples({
+	"set {_list::*} to all of the server's recipe of type shaped recipe for netherite ingot",
+	"set {_list::*} to the server's mc recipes of type cooking recipe for raw beef",
+	"set {_list::*} to server's custom recipes of type blasting for raw iron named \"Impure Iron\"",
+	"",
+	"reset all of the server's recipes",
+	"",
+	"delete all the server's recipes for netherite ingot",
+	"clear all of the server minecraft recipes",
+	"delete all of the server's custom shaped recipes"
+})
+@Since("INSERT VERSION")
+public class ExprAllRecipes extends SimpleExpression<Recipe> {
+
+	static {
+		Skript.registerExpression(ExprAllRecipes.class, Recipe.class, ExpressionType.SIMPLE,
+			"[all [[of] the]|the] server['s] recipes [for %-itemstacks/itemtypes%]",
+			"[all [[of] the]|the] server['s] (mc|minecraft|vanilla) recipes [for %-itemstacks/itemtypes%]",
+			"[all [[of] the]|the] server['s] custom recipes [for %-itemstacks/itemtypes%]",
+			"[all [[of] the]|the] server['s] %recipetype% [for %-itemstacks/itemtypes%]",
+			"[all [[of] the]|the] server['s] (mc|minecraft|vanilla) %recipetype% [for %-itemstacks/itemtypes%]",
+			"[all [[of] the]|the] server['s] custom %recipetype% [for %-itemstacks/itemtypes%]");
+	}
+
+	private Expression<RecipeType> recipeTypeExpr;
+	private Expression<?> itemExpr;
+	private boolean getMinecraft, getCustom;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (matchedPattern <= 2 && exprs[0] != null) {
+			itemExpr = exprs[0];
+		} else if (matchedPattern >= 3) {
+			//noinspection unchecked
+			recipeTypeExpr = (Expression<RecipeType>) exprs[0];
+			itemExpr = exprs[1];
+		}
+		getMinecraft = matchedPattern == 1 || matchedPattern == 4;
+		getCustom = matchedPattern == 2 || matchedPattern == 5;
+		return true;
+	}
+
+	@Override
+	protected Recipe @Nullable [] get(Event event) {
+		List<Recipe> recipes = new ArrayList<>();
+		CheckedIterator<Recipe> iterator = getSelectedRecipes(event);
+		if (iterator == null)
+			return null;
+		iterator.forEachRemaining(recipes::add);
+		return recipes.toArray(new Recipe[0]);
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (mode == ChangeMode.RESET || mode == ChangeMode.DELETE)
+			return CollectionUtils.array(Recipe.class);
+		else if (mode == ChangeMode.ADD)
+			return CollectionUtils.array(Recipe[].class);
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		switch (mode) {
+			case RESET -> {
+				Bukkit.resetRecipes();
+			}
+			case DELETE -> {
+				CheckedIterator<Recipe> iterator = getSelectedRecipes(event);
+				if (iterator == null)
+					return;
+				iterator.forEachRemaining(recipe -> {
+					if (recipe instanceof Keyed key)
+						Bukkit.removeRecipe(key.getKey());
+				});
+			}
+			case ADD -> {
+				Recipe[] recipes = delta != null ? (Recipe[]) delta : null;
+				if (recipes == null || recipes.length == 0)
+					return;
+				for (Recipe recipe : recipes) {
+					if (!(recipe instanceof Keyed keyed))
+						continue;
+					NamespacedKey key = keyed.getKey();
+					if (Bukkit.getRecipe(key) != null)
+						Bukkit.removeRecipe(key);
+					Bukkit.addRecipe(recipe);
+				}
+
+			}
+		}
+	}
+
+	@Override
+	public boolean isSingle() {
+		return false;
+	}
+
+	@Override
+	public Class<Recipe> getReturnType() {
+		return Recipe.class;
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug);
+		builder.append("all of the");
+		if (getMinecraft) {
+			builder.append("minecraft");
+		} else if (getCustom) {
+			builder.append("custom");
+		}
+		if (recipeTypeExpr != null)
+			builder.append(recipeTypeExpr);
+		builder.append("recipes");
+		if (itemExpr != null)
+			builder.append("for", itemExpr);
+		return builder.toString();
+	}
+
+	// Gets the recipes based on the provided input from the user
+	private CheckedIterator<Recipe> getSelectedRecipes(Event event) {
+		Iterator<Recipe> iterator = null;
+		// User is looking for a certain type of item(s)
+		if (itemExpr != null) {
+			List<Recipe> itemRecipes = new ArrayList<>();
+			for (Object object : itemExpr.getArray(event)) {
+				ItemStack stack = null;
+				if (object instanceof ItemStack itemStack) {
+					stack = itemStack;
+				} else if (object instanceof ItemType itemType) {
+					stack = itemType.getRandom();
+				}
+				if (stack != null)
+					itemRecipes.addAll(Bukkit.getRecipesFor(stack));
+			}
+			// If no recipes contain the items the user is looking for, return null
+			if (itemRecipes.isEmpty())
+				return null;
+			iterator = itemRecipes.iterator();
+		} else {
+			// Items not specified, getting all recipes
+			iterator = Bukkit.recipeIterator();
+		}
+
+		RecipeType recipeType = recipeTypeExpr != null ? recipeTypeExpr.getSingle(event) : null;
+
+		return new CheckedIterator<Recipe>(iterator, recipe -> {
+			if (recipe instanceof Keyed keyed) {
+				NamespacedKey key = keyed.getKey();
+				// If the user wants only "minecraft:" recipes
+				if (getMinecraft && !key.getNamespace().equalsIgnoreCase("minecraft"))
+					return false;
+				// If the user wants only custom recipes
+				else if (getCustom && key.getNamespace().equalsIgnoreCase("minecraft"))
+					return false;
+
+				// If the user wants a specific recipe type
+				if (recipeType != null) {
+					if (!(recipe.getClass().equals(recipeType.getRecipeClass()) || recipeType.getRecipeClass().isInstance(recipe)))
+						return false;
+				}
+				return true;
+			}
+			return false;
+		});
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprGetRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprGetRecipe.java
new file mode 100644
index 00000000000..be8407a0740
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprGetRecipe.java
@@ -0,0 +1,74 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Name("Recipe")
+@Description("Returns the recipe registered with the provided id.")
+@Examples({
+	"set {_recipe} to recipe with the key \"my_recipe\"",
+	"set {_recipes::*} to recipes from the ids \"my_recipe\" and \"custom_recipe\""
+})
+@Since("INSERT VERSION")
+public class ExprGetRecipe extends SimpleExpression<Recipe> {
+
+	static {
+		Skript.registerExpression(ExprGetRecipe.class, Recipe.class, ExpressionType.SIMPLE,
+			"[the] recipe[s] (with|from) [the] (key|id)[s] %strings%");
+	}
+
+	private Expression<String> recipeNames;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		//noinspection unchecked
+		recipeNames = (Expression<String>) exprs[0];
+		return true;
+	}
+
+	@Override
+	protected Recipe @Nullable [] get(Event event) {
+		List<Recipe> recipeList = new ArrayList<>();
+		for (String name : recipeNames.getArray(event)) {
+			NamespacedKey key = NamespacedKey.fromString(name, Skript.getInstance());
+			if (key == null)
+				continue;
+			Recipe check = Bukkit.getRecipe(key);
+			if (check != null)
+				recipeList.add(check);
+		}
+		return recipeList.toArray(new Recipe[0]);
+	}
+
+	@Override
+	public boolean isSingle() {
+		return recipeNames.isSingle();
+	}
+
+	@Override
+	public Class<Recipe> getReturnType() {
+		return Recipe.class;
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return "recipes with the keys " + recipeNames.toString(event, debug);
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCategory.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCategory.java
new file mode 100644
index 00000000000..efd872e0542
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCategory.java
@@ -0,0 +1,121 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.CookingRecipe;
+import org.bukkit.inventory.Recipe;
+import org.bukkit.inventory.ShapedRecipe;
+import org.bukkit.inventory.ShapelessRecipe;
+import org.bukkit.inventory.recipe.CookingBookCategory;
+import org.bukkit.inventory.recipe.CraftingBookCategory;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe;
+import org.skriptlang.skript.bukkit.recipes.RecipeCategory;
+
+@Name("Recipe Category")
+@Description("The recipe category of a shaped, shapeless, blasting, furnace, campfire or smoking recipe.")
+@Examples({
+	"set {_recipe} to a new shaped recipe with the key \"my_recipe\":",
+		"\tset the recipe ingredients to diamond, air, diamond, air, emerald, air, diamond, air and diamond",
+		"\tset the recipe category to misc crafting category",
+		"\tset the recipe result item to nether star",
+	"",
+	"set {_recipe} to a new blasting recipe with the id \"my_recipe\":",
+		"\tset the recipe input item to coal",
+		"\tset the recipe category to misc cooking category",
+		"\tset the recipe result item to gunpowder",
+	"",
+	"loop the server's recipes:",
+		"\tbroadcast recipe category of loop-recipe"
+})
+@Since("INSERT VERSION")
+public class ExprRecipeCategory extends SimplePropertyExpression<Recipe, RecipeCategory> {
+
+	static {
+		registerDefault(ExprRecipeCategory.class, RecipeCategory.class, "recipe category", "recipes");
+	}
+
+	private boolean isEvent = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (exprs[0].isDefault() && getParser().isCurrentEvent(CreateRecipeEvent.class))
+			isEvent = true;
+		return super.init(exprs, matchedPattern, isDelayed, parseResult);
+	}
+
+	@Override
+	public @Nullable RecipeCategory convert(Recipe recipe) {
+		Enum<?> category = null;
+		if (recipe instanceof MutableRecipe mutableRecipe) {
+			if (mutableRecipe instanceof MutableCraftingRecipe mutableCraftingRecipe) {
+				category = mutableCraftingRecipe.getCategory();
+			} else if (mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+				category = mutableCookingRecipe.getCategory();
+			}
+		} else {
+			// TODO: Combine ShapedRecipe and ShapelessRecipe into CraftingRecipe when minimum version is raised to 1.20.1 or higher.
+			if (recipe instanceof ShapedRecipe shapedRecipe) {
+				category = shapedRecipe.getCategory();
+			} else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
+				category = shapelessRecipe.getCategory();
+			} else if (recipe instanceof CookingRecipe<?> cookingRecipe) {
+				category = cookingRecipe.getCategory();
+			}
+		}
+		if (category != null)
+			return RecipeCategory.convertBukkitToSkript(category);
+		return null;
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			Skript.error("You can not set the recipe category of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(RecipeCategory.class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (!(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+		RecipeCategory recipeCategory = (RecipeCategory) (delta != null ? delta[0] : null);
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+		if (mutableRecipe instanceof MutableCraftingRecipe mutableCraftingRecipe) {
+			if (!(recipeCategory.getCategory() instanceof CraftingBookCategory category))
+				return;
+			mutableCraftingRecipe.setCategory(category);
+		} else if (mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+			if (!(recipeCategory.getCategory() instanceof CookingBookCategory category))
+				return;
+			mutableCookingRecipe.setCategory(category);
+		}
+	}
+
+	@Override
+	public Class<RecipeCategory> getReturnType() {
+		return RecipeCategory.class;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe category";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCookingTime.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCookingTime.java
new file mode 100644
index 00000000000..d2e11b88693
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeCookingTime.java
@@ -0,0 +1,90 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.util.Timespan;
+import ch.njol.skript.util.Timespan.TimePeriod;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.CookingRecipe;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe;
+
+@Name("Recipe Cooking Time")
+@Description("The cooking time of a blasting, furnace, campfire or smoking recipe.")
+@Examples({
+	"set {_recipe} to a new blasting recipe with the key \"my_recipe\":",
+		"\tset the recipe input item to raw gold named \"Impure Gold\"",
+		"\tset the recipe cooking time to 10 seconds",
+		"\tset the recipe result to gold ingot named \"Pure Gold\""
+})
+@Since("INSERT VERSION")
+public class ExprRecipeCookingTime extends SimplePropertyExpression<Recipe, Timespan> {
+
+	static {
+		registerDefault(ExprRecipeCookingTime.class, Timespan.class, "recipe cook[ing] time", "recipes");
+	}
+
+	private boolean isEvent = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (exprs[0].isDefault() && getParser().isCurrentEvent(CreateRecipeEvent.class))
+			isEvent = true;
+		return super.init(exprs, matchedPattern, isDelayed, parseResult);
+	}
+
+	@Override
+	public @Nullable Timespan convert(Recipe recipe) {
+		if (recipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+			return new Timespan(TimePeriod.TICK, mutableCookingRecipe.getCookingTime());
+		} else if (recipe instanceof CookingRecipe<?> cookingRecipe) {
+			return new Timespan(TimePeriod.TICK, cookingRecipe.getCookingTime());
+		}
+		return null;
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			Skript.error("You can not set the recipe cooking time of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(Timespan.class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (!(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+
+		Timespan timespan = (Timespan) delta[0];
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+		if (!(mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe))
+			return;
+		mutableCookingRecipe.setCookingTime((int) timespan.getAs(Timespan.TimePeriod.TICK));
+	}
+
+	@Override
+	public Class<Timespan> getReturnType() {
+		return Timespan.class;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe cooking time";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeExperience.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeExperience.java
new file mode 100644
index 00000000000..3e24249d1c7
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeExperience.java
@@ -0,0 +1,89 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.CookingRecipe;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCookingRecipe;
+
+@Name("Recipe Experience")
+@Description("The experience of a blasting, furnace, campfire, or smoking recipe.")
+@Examples({
+	"set {_recipe} to a new blasting recipe with the key \"my_recipe\":",
+		"\tset the recipe input item to a raw copper named \"Impure Copper\"",
+		"\tset the recipe experience to 20",
+		"\tset the recipe result to copper ingot named \"Pure Copper\""
+})
+@Since("INSERT VERSION")
+public class ExprRecipeExperience extends SimplePropertyExpression<Recipe, Float> {
+
+	static {
+		registerDefault(ExprRecipeExperience.class, Float.class,  "recipe [e]xp[erience]", "recipes");
+	}
+
+	private boolean isEvent = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (exprs[0].isDefault() && getParser().isCurrentEvent(CreateRecipeEvent.class))
+			isEvent = true;
+		return super.init(exprs, matchedPattern, isDelayed, parseResult);
+	}
+
+	@Override
+	public @Nullable Float convert(Recipe recipe) {
+		if (recipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+			return mutableCookingRecipe.getExperience();
+		} else if (recipe instanceof CookingRecipe<?> cookingRecipe) {
+			return cookingRecipe.getExperience();
+		}
+		return null;
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			Skript.error("You can not set the recipe experience of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(Float.class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (!(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+		if (!(mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe))
+			return;
+
+		float experience = (float) delta[0];
+		mutableCookingRecipe.setExperience(experience);
+	}
+
+	@Override
+	public Class<Float> getReturnType() {
+		return Float.class;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe experience";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeGroup.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeGroup.java
new file mode 100644
index 00000000000..d66688f58fb
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeGroup.java
@@ -0,0 +1,101 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.*;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableGroupRecipe;
+
+@Name("Recipe Group")
+@Description({
+	"The recipe group of a shaped, shapeless, blasting, furnace, campfire, smoking, or stonecutting recipe.",
+	"Groups recipes together under the provided string."
+})
+@Examples({
+	"set {_recipe} to a new shapeless recipe with the key \"my_recipe\":",
+		"\tset the recipe ingredients to 3 diamonds, 3 emeralds and 3 netherite ingots",
+		"\tset the recipe group to \"my group\"",
+		"\tset the recipe result to nether star"
+})
+@Since("INSERT VERSION")
+public class ExprRecipeGroup extends SimplePropertyExpression<Recipe, String> {
+
+	static {
+		registerDefault(ExprRecipeGroup.class, String.class, "recipe group", "recipes");
+	}
+
+	private boolean isEvent = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (exprs[0].isDefault() && getParser().isCurrentEvent(CreateRecipeEvent.class))
+			isEvent = true;
+		return super.init(exprs, matchedPattern, isDelayed, parseResult);
+	}
+
+	@Override
+	public @Nullable String convert(Recipe recipe) {
+		if (recipe instanceof MutableGroupRecipe mutableGroupRecipe)  {
+			return mutableGroupRecipe.getGroup();
+		} else {
+			// TODO: Combine ShapedRecipe and ShapelessRecipe into CraftingRecipe when minimum version is raised to 1.20.1 or higher.
+			if (recipe instanceof ShapedRecipe shapedRecipe) {
+				return shapedRecipe.getGroup();
+			} else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
+				return shapelessRecipe.getGroup();
+			} else if (recipe instanceof CookingRecipe<?> cookingRecipe) {
+				return cookingRecipe.getGroup();
+			} else if (recipe instanceof StonecuttingRecipe stonecuttingRecipe) {
+				return stonecuttingRecipe.getGroup();
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			Skript.error("You can not set the recipe group of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(String.class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (!(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+
+		String group = (String) delta[0];
+		if (group.isEmpty())
+			return;
+
+		if (mutableRecipe instanceof MutableGroupRecipe mutableGroupRecipe)
+			mutableGroupRecipe.setGroup(group);
+	}
+
+	@Override
+	public Class<String> getReturnType() {
+		return String.class;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe group";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeIngredients.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeIngredients.java
new file mode 100644
index 00000000000..e3d070190ef
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeIngredients.java
@@ -0,0 +1,476 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.config.Node;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.expressions.base.PropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.Material;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.*;
+import org.bukkit.inventory.RecipeChoice.ExactChoice;
+import org.bukkit.inventory.RecipeChoice.MaterialChoice;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.*;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableCraftingRecipe.MutableShapedRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTransformRecipe;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe.MutableSmithingRecipe.MutableSmithingTrimRecipe;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.*;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.CraftingRecipeEvent.ShapedRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTransformRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent.SmithingRecipeEvent.SmithingTrimRecipeEvent;
+import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer;
+
+import java.util.*;
+
+@Name("Recipe Ingredients")
+@Description({
+	"The ingredients of a shaped or shapeless recipe.",
+	"The ingredients of a row for a shaped recipe.",
+	"The input item of a blasting, furnace, campfire, smoking, stonecutting and transmute recipe.",
+	"The base, template, addition items of a smithing transform and smithing trim recipe.",
+	"the transmute items of a transmute recipe."
+})
+@Examples({
+	"set {_recipe} to a new shaped recipe with the key \"my_recipe\":",
+		"\tset the recipe ingredients to diamond, air, diamond, air, emerald, air, diamond, air and diamond",
+		"\t#OR",
+		"\tset the recipe ingredients of the 1st row to diamond, air and diamond",
+		"\tset the recipe ingredients of second row to air, emerald and air",
+		"\tset the recipe ingredients of the third row to diamond, air and diamond",
+		"\tset the recipe result to beacon",
+	"",
+	"set {_recipe} to a shapeless recipe with id \"my_recipe\":",
+		"\tset recipe ingredients to iron ingot, gold ingot, iron ingot, nether star, 5 obsidian, nether star, iron ingot, gold ingot and iron ingot",
+		"\tset recipe result item to beacon named \"OP Beacon\"",
+	"",
+	"set {_recipe} to new blasting recipe with the id \"my_recipe\":",
+		"\tset the recipe input item to netherite ingot named \"Impure Netherite\"",
+		"\tset the recipe result item to netherite ingot named \"Pure Netherite\"",
+	"",
+	"set {_recipe} to a new smithing transform recipe with key \"my_recipe\":",
+		"\tset the recipe base item to diamond helmet",
+		"\tset the recipe template item to paper named \"Blueprint\"",
+		"\tset the recipe addition item to netherite ingot named \"Pure Netherite\"",
+		"\tset the recipe result to netherite helmet named \"Pure Helmet\"",
+	"",
+	"set {_recipe} to a new transmute recipe with key \"my_recipe\":",
+		"\tset the recipe input item to leather helmet",
+		"\tset the recipe transmute item to nether star named \"Free Upgrade\"",
+		"\tset the recipe result to netherite helmet"
+})
+public class ExprRecipeIngredients extends PropertyExpression<Recipe, ItemStack> implements SyntaxRuntimeErrorProducer {
+
+	enum RecipePattern {
+		INGREDIENTS("[recipe] ingredients", "recipe ingredients", CraftingRecipeEvent.class,
+			"This can only be used when creating a Shaped or Shapeless Recipe."),
+		FIRSTROW("[recipe] ingredients of [the] (1st|first) row", "recipe ingredients of the first row", ShapedRecipeEvent.class,
+			"This can only be used when creating a Shaped Recipe."),
+		SECONDROW("[recipe] ingredients of [the] (2nd|second) row", "recipe ingredients of the first row", ShapedRecipeEvent.class,
+			"This can only be used when creating a Shaped Recipe."),
+		THIRDROW("[recipe] ingredients of [the] (3rd|third) row", "recipe ingredients of the first row", ShapedRecipeEvent.class,
+			"This can only be used when creating a Shaped Recipe."),
+		INPUT("recipe (input|source) [item]", "recipe input item", new Class[]{CookingRecipeEvent.class, StonecuttingRecipeEvent.class, TransmuteRecipeEvent.class},
+			"This can only be used when creating a Cooking, Blasting, Furnace, Campfire, Smoking, Stonecutting, or Transmute Recipe."),
+		BASE("[recipe] base item[s]", "recipe base items", SmithingRecipeEvent.class,
+			"This can only be used when creating a Smithing, Smithing Transform, or Smithing Trim Recipe."),
+		TEMPLATE("[recipe] template item[s]", "recipe template items", new Class[]{SmithingTransformRecipeEvent.class, SmithingTrimRecipeEvent.class},
+			"This can only be used when creating a Smithing Transform or Smithing Trim Recipe."),
+		ADDITION("[recipe] addition[al] item[s]", "recipe additional items", SmithingRecipeEvent.class,
+			"This can only be used when creating a Smithing, Smithing Transform or Smithing Trim Recipe."),
+		TRANSMUTE("[recipe] transmute item[s]", "recipe transmute items", TransmuteRecipeEvent.class,
+			"This can only be used creating a Transmute Recipe.");
+
+
+		private String pattern, pattern2, toString, error;
+		private Class<? extends Event>[] eventClasses;
+
+		RecipePattern(String pattern, String toString, Class<? extends Event> eventClass, String error) {
+			//noinspection unchecked
+			this(pattern, toString, new Class[]{eventClass}, error);
+		}
+
+		RecipePattern(String pattern, String toString, Class<? extends Event>[] eventClasses, String error) {
+			this.pattern = "[the] " + pattern + " [of %recipes%]";
+			this.pattern2 = "[the] %recipes%'[s] " + pattern;
+			this.toString = toString;
+			this.eventClasses = eventClasses;
+			this.error = error;
+		}
+
+	}
+
+	private static final RecipePattern[] recipePatterns = RecipePattern.values();
+
+	static {
+		String[] patterns = new String[recipePatterns.length * 2];
+		for (RecipePattern pattern : recipePatterns) {
+			patterns[(2 * pattern.ordinal())] = pattern.pattern;
+			patterns[(2 * pattern.ordinal()) + 1] = pattern.pattern2;
+		}
+		Skript.registerExpression(ExprRecipeIngredients.class, ItemStack.class, ExpressionType.PROPERTY, patterns);
+	}
+
+	private boolean isEvent = false;
+	private RecipePattern selectedPattern;
+	private Node node;
+	private String rawExpr;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		selectedPattern = recipePatterns[matchedPattern / 2];
+		if (exprs[0].isDefault()) {
+			if (getParser().isCurrentEvent(CreateRecipeEvent.class)) {
+				if (!getParser().isCurrentEvent(selectedPattern.eventClasses)) {
+					Skript.error(selectedPattern.error);
+					return false;
+				}
+				isEvent = true;
+			}
+		}
+		//noinspection unchecked
+		setExpr((Expression<? extends Recipe>) exprs[0]);
+		node = getParser().getNode();
+		rawExpr = parseResult.expr;
+		return true;
+	}
+
+	@Override
+	protected ItemStack @Nullable [] get(Event event, Recipe[] source) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		for (Recipe recipe : source) {
+			switch (selectedPattern) {
+				case INGREDIENTS -> {
+					ingredients.addAll(getterIngredients(recipe));
+				}
+				case FIRSTROW, SECONDROW, THIRDROW -> {
+					ingredients.addAll(getterRows(recipe));
+				}
+				case BASE, TEMPLATE, ADDITION -> {
+					ingredients.addAll(getterSmithing(recipe));
+				}
+				case INPUT -> {
+					ingredients.addAll(getterInput(recipe));
+				}
+				case TRANSMUTE -> {
+					ingredients.addAll(getterTransmute(recipe));
+				}
+			}
+		}
+		return ingredients.toArray(ItemStack[]::new);
+	}
+
+	private List<ItemStack> getterIngredients(Recipe recipe) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		if (recipe instanceof MutableCraftingRecipe mutableCraftingRecipe) {
+			Arrays.stream(mutableCraftingRecipe.getIngredients()).forEach(recipeChoice -> {
+				if (recipeChoice instanceof ExactChoice exactChoice) {
+					ingredients.addAll(exactChoice.getChoices());
+				} else if (recipeChoice instanceof MaterialChoice materialChoice) {
+					materialChoice.getChoices().stream().forEach(material -> {
+						ingredients.add(new ItemStack(material));
+					});
+				}
+			});
+		} else if (recipe instanceof ShapedRecipe shapedRecipe) {
+			Map<Character, ItemStack> ingredientMap = shapedRecipe.getIngredientMap();
+			ingredients.addAll(ingredientMap.values().stream()
+				.map(itemStack -> {
+					if (itemStack == null) return new ItemStack(Material.AIR);
+					return itemStack;
+				}).toList());
+		} else if (recipe instanceof ShapelessRecipe shapelessRecipe) {
+			ingredients.addAll(shapelessRecipe.getIngredientList());
+		} else {
+			error("You can only get the ingredients of a Shaped or Shapeless Recipe.");
+		}
+		return ingredients;
+	}
+
+	private List<ItemStack> getterRows(Recipe recipe) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		if (recipe instanceof MutableShapedRecipe mutableShapedRecipe) {
+			RecipeChoice[] choices = mutableShapedRecipe.getIngredients();
+			int row = selectedPattern.ordinal() - 1;
+			for (int i = 0; i < 3; i++) {
+				ExactChoice exactChoice = (ExactChoice) choices[i * row + i];
+				ingredients.addAll(exactChoice.getChoices());
+			}
+		} else if (recipe instanceof ShapedRecipe shapedRecipe) {
+			String[] shape = shapedRecipe.getShape();
+			Map<Character, ItemStack> ingredientMap = shapedRecipe.getIngredientMap();
+			String row = shape[selectedPattern.ordinal() - 1];
+			for (Character character : row.toCharArray()) {
+				ItemStack stack = ingredientMap.get(character);
+				if (stack == null) stack = new ItemStack(Material.AIR);
+				ingredients.add(stack);
+			}
+		} else {
+			error("You can only get the ingredients of a row for a Shaped Recipe.");
+		}
+		return ingredients;
+	}
+
+	private List<ItemStack> getterSmithing(Recipe recipe) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		if (recipe instanceof MutableSmithingRecipe mutableSmithingRecipe) {
+			ExactChoice exactChoice = (ExactChoice) switch (selectedPattern) {
+				case BASE -> mutableSmithingRecipe.getBase();
+				case ADDITION -> mutableSmithingRecipe.getAddition();
+				case TEMPLATE -> {
+					if (recipe instanceof MutableSmithingTransformRecipe || recipe instanceof MutableSmithingTrimRecipe) {
+						yield mutableSmithingRecipe.getTemplate();
+					}
+					yield null;
+				}
+				default -> null;
+			};
+			if (exactChoice != null)
+				ingredients.addAll(exactChoice.getChoices());
+		} else if (recipe instanceof SmithingRecipe smithingRecipe) {
+			RecipeChoice choice = switch (selectedPattern) {
+				case BASE -> smithingRecipe.getBase();
+				case TEMPLATE -> {
+					if (recipe instanceof SmithingTransformRecipe transformRecipe)
+						yield transformRecipe.getTemplate();
+					else if (recipe instanceof SmithingTrimRecipe trimRecipe)
+						yield trimRecipe.getTemplate();
+					yield null;
+				}
+				case ADDITION -> smithingRecipe.getAddition();
+				default -> null;
+			};
+			if (choice instanceof ExactChoice exactChoice) {
+				ingredients.addAll(exactChoice.getChoices());
+			} else if (choice instanceof MaterialChoice materialChoice) {
+				ingredients.addAll(
+					materialChoice.getChoices().stream().map(ItemStack::new).toList()
+				);
+			}
+		} else {
+			error("You can only get the base items of a Smithing, Smithing Transform and Smithing Trim Recipe.");
+		}
+		return ingredients;
+	}
+
+	private List<ItemStack> getterInput(Recipe recipe) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		if (recipe instanceof MutableRecipe mutableRecipe) {
+			if (mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+				ingredients.addAll(((ExactChoice) mutableCookingRecipe.getInput()).getChoices());
+			} else if (mutableRecipe instanceof MutableStonecuttingRecipe mutableStonecuttingRecipe) {
+				ingredients.addAll(((ExactChoice) mutableStonecuttingRecipe.getInput()).getChoices());
+			} else if (mutableRecipe instanceof MutableTransmuteRecipe mutableTransmuteRecipe) {
+				ingredients.addAll(((ExactChoice) mutableTransmuteRecipe.getInput()).getChoices());
+			}
+		} else {
+			RecipeChoice choice = null;
+			if (recipe instanceof CookingRecipe<?> cookingRecipe) {
+				choice = cookingRecipe.getInputChoice();
+			} else if (recipe instanceof StonecuttingRecipe stonecuttingRecipe) {
+				choice = stonecuttingRecipe.getInputChoice();
+			} else if (recipe instanceof TransmuteRecipe transmuteRecipe) {
+				choice = transmuteRecipe.getInput();
+			} else {
+				error("You can only get the input item of a Cooking, Blasting, Furnace, Campfire, Smoking and Stonecutting Recipe.");
+			}
+			if (choice instanceof ExactChoice exactChoice) {
+				ingredients.addAll(exactChoice.getChoices());
+			} else if (choice instanceof MaterialChoice materialChoice) {
+				ingredients.addAll(materialChoice.getChoices().stream().map(ItemStack::new).toList());
+			}
+		}
+		return ingredients;
+	}
+
+	private List<ItemStack> getterTransmute(Recipe recipe) {
+		List<ItemStack> ingredients = new ArrayList<>();
+		if (recipe instanceof MutableTransmuteRecipe mutableTransmuteRecipe) {
+			ingredients.addAll(((ExactChoice) mutableTransmuteRecipe.getInput()).getChoices());
+		} else if (recipe instanceof TransmuteRecipe transmuteRecipe) {
+			RecipeChoice choice = transmuteRecipe.getMaterial();
+			if (choice instanceof ExactChoice exactChoice) {
+				ingredients.addAll(exactChoice.getChoices());
+			} else if (choice instanceof MaterialChoice materialChoice) {
+				ingredients.addAll(materialChoice.getChoices().stream().map(ItemStack::new).toList());
+			}
+		}
+		return ingredients;
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			error("You can not set the " + selectedPattern.toString + " of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(ItemType[].class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (!(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+
+		Map<Integer, ItemStack[]> items = new HashMap<>();
+		for (int i = 0; i < delta.length; i++) {
+			Object object = delta[i];
+			if (object instanceof ItemType itemType) {
+				List<ItemStack> thisItems = new ArrayList<>();
+				itemType.getAll().forEach(thisItems::add);
+				items.put(i, thisItems.toArray(ItemStack[]::new));
+			}
+		}
+
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+
+		switch (selectedPattern) {
+			case INGREDIENTS -> {
+				changerIngredients(items, mutableRecipe, recipeEvent);
+			}
+			case FIRSTROW, SECONDROW, THIRDROW -> {
+				changerRows(items,  mutableRecipe, recipeEvent);
+			}
+			case BASE, TEMPLATE, ADDITION -> {
+				changerSmithing(items, mutableRecipe, recipeEvent);
+			}
+			case INPUT -> {
+				changerInput(items, mutableRecipe, recipeEvent);
+			}
+			case TRANSMUTE -> {
+				changerTransmute(items, mutableRecipe, recipeEvent);
+			}
+		}
+	}
+
+	private void changerIngredients(Map<Integer, ItemStack[]> items, MutableRecipe mutableRecipe, CreateRecipeEvent recipeEvent) {
+		if (!(mutableRecipe instanceof MutableCraftingRecipe mutableCraftingRecipe))
+			return;
+
+		if (items.size() > 9) {
+			error("You can only provide up to 9 items when setting the ingredients for a '" + recipeEvent.getRecipeType()  + "' recipe.");
+			recipeEvent.setErrorInSection();
+			return;
+		}
+		for (Map.Entry<Integer, ItemStack[]> entry : items.entrySet()) {
+			ItemStack[] ingredients = entry.getValue();
+			if (Arrays.stream(ingredients).anyMatch(itemStack -> itemStack.getType().isAir())) {
+				if (ingredients.length > 1) {
+					error("You can not provide air with a list of other items.");
+					recipeEvent.setErrorInSection();
+					return;
+				} else {
+					continue;
+				}
+			}
+			RecipeChoice choice = new ExactChoice(ingredients);
+			mutableCraftingRecipe.setIngredients(entry.getKey(), choice);
+		}
+	}
+
+	private void changerRows(Map<Integer, ItemStack[]> items, MutableRecipe mutableRecipe, CreateRecipeEvent recipeEvent) {
+		if (!(mutableRecipe instanceof MutableShapedRecipe mutableShapedRecipe))
+			return;
+		if (items.size() > 3) {
+			error("You can only provide up to 3 items when setting the ingredients of a row for a '" + recipeEvent.getRecipeType() + "' recipe.");
+			recipeEvent.setErrorInSection();
+			return;
+		}
+		for (Map.Entry<Integer, ItemStack[]> entry : items.entrySet()) {
+			ItemStack[] ingredients = entry.getValue();
+			if (Arrays.stream(ingredients).anyMatch(itemStack -> itemStack.getType().isAir())) {
+				if (ingredients.length > 1) {
+					error("You can not provide 'air' with a list of other items.");
+					recipeEvent.setErrorInSection();
+					return;
+				} else {
+					continue;
+				}
+			}
+			RecipeChoice choice = new ExactChoice(ingredients);
+			mutableShapedRecipe.setIngredients(((3 * (selectedPattern.ordinal() - 1)) + entry.getKey()), choice);
+		}
+	}
+
+	private void changerSmithing(Map<Integer, ItemStack[]> items, MutableRecipe mutableRecipe, CreateRecipeEvent recipeEvent) {
+		if (!(mutableRecipe instanceof MutableSmithingRecipe mutableSmithingRecipe))
+			return;
+		List<ItemStack> stackList = new ArrayList<>();
+		items.entrySet().stream().forEach(entry -> stackList.addAll(Arrays.asList(entry.getValue())));
+		if (stackList.stream().anyMatch(itemStack -> itemStack.getType().isAir())) {
+			error("You can not provide 'air' with this expression.");
+			recipeEvent.setErrorInSection();
+			return;
+		}
+		RecipeChoice choice = new ExactChoice(stackList);
+		switch (selectedPattern) {
+			case BASE -> mutableSmithingRecipe.setBase(choice);
+			case TEMPLATE -> mutableSmithingRecipe.setTemplate(choice);
+			case ADDITION -> mutableSmithingRecipe.setAddition(choice);
+		}
+	}
+
+	private void changerInput(Map<Integer, ItemStack[]> items, MutableRecipe mutableRecipe, CreateRecipeEvent recipeEvent) {
+		List<ItemStack> stackList = new ArrayList<>();
+		items.entrySet().stream().forEach(entry -> stackList.addAll(Arrays.asList(entry.getValue())));
+		if (stackList.stream().anyMatch(itemStack -> itemStack.getType().isAir())) {
+			error("You can not provide 'air' with this expression.");
+			recipeEvent.setErrorInSection();
+			return;
+		}
+		RecipeChoice choice = new ExactChoice(stackList);
+		if (mutableRecipe instanceof MutableCookingRecipe mutableCookingRecipe) {
+			mutableCookingRecipe.setInput(choice);
+		} else if (mutableRecipe instanceof MutableStonecuttingRecipe mutableStonecuttingRecipe) {
+			mutableStonecuttingRecipe.setInput(choice);
+		} else if (mutableRecipe instanceof MutableTransmuteRecipe mutableTransmuteRecipe) {
+			mutableTransmuteRecipe.setInput(choice);
+		}
+	}
+
+	private void changerTransmute(Map<Integer, ItemStack[]> items, MutableRecipe mutableRecipe, CreateRecipeEvent recipeEvent) {
+		List<ItemStack> stackList = new ArrayList<>();
+		items.entrySet().stream().forEach(entry -> stackList.addAll(Arrays.asList(entry.getValue())));
+		if (stackList.stream().anyMatch(itemStack -> itemStack.getType().isAir())) {
+			error("You can not provide 'air' with this expression.");
+			recipeEvent.setErrorInSection();
+			return;
+		}
+		RecipeChoice choice = new ExactChoice(stackList);
+		if (mutableRecipe instanceof MutableTransmuteRecipe mutableTransmuteRecipe) {
+			mutableTransmuteRecipe.setMaterial(choice);
+		}
+	}
+
+	@Override
+	public Class<ItemStack> getReturnType() {
+		return ItemStack.class;
+	}
+
+	@Override
+	public Node getNode() {
+		return node;
+	}
+
+	@Override
+	public @Nullable String toHighlight() {
+		return rawExpr;
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return "the " + selectedPattern.toString + " of " + getExpr().toString(event, debug);
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeKey.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeKey.java
new file mode 100644
index 00000000000..f453b07c739
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeKey.java
@@ -0,0 +1,47 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import org.bukkit.Keyed;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+
+@Name("Recipe Name")
+@Description("Get the namespaced key of a recipe.")
+@Examples({
+	"loop all of the server's recipes:",
+		"\tbroadcast the recipe key of loop-recipe",
+		"\tadd loop-recipe's id to {_list::*}"
+})
+@Since("INSERT VERSION")
+public class ExprRecipeKey extends SimplePropertyExpression<Recipe, String> {
+
+	static {
+		register(ExprRecipeKey.class, String.class, "recipe (key|id)[s]", "recipes");
+	}
+
+	@Override
+	public @Nullable String convert(Recipe recipe) {
+		if (recipe instanceof MutableRecipe recipeWrapper) {
+			return recipeWrapper.getKey().toString();
+		} else if (recipe instanceof Keyed key) {
+			return key.getKey().toString();
+		}
+		return null;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe key";
+	}
+
+	@Override
+	public Class<String> getReturnType() {
+		return String.class;
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeResult.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeResult.java
new file mode 100644
index 00000000000..1fb74732711
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeResult.java
@@ -0,0 +1,80 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+
+@Name("Recipe Result")
+@Description("The result item for a recipe.")
+@Examples({
+	"set {_recipe} to a new shaped recipe with the key \"my_recipe\":",
+		"\tset the recipe ingredients of 1st row to diamond, air and diamond",
+		"\tset the recipe result to diamond sword named \"Chosen One\""
+})
+@Since("INSERT VERSION")
+public class ExprRecipeResult extends SimplePropertyExpression<Recipe, ItemStack> {
+
+	static {
+		registerDefault(ExprRecipeResult.class, ItemStack.class, "recipe result [item]", "recipes");
+	}
+
+	private boolean isEvent = false;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+		if (getParser().isCurrentEvent(CreateRecipeEvent.class))
+			isEvent = true;
+		return super.init(exprs, matchedPattern, isDelayed, parseResult);
+	}
+
+	@Override
+	public @Nullable ItemStack convert(Recipe recipe) {
+		return recipe.getResult();
+	}
+
+	@Override
+	public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
+		if (!isEvent) {
+			Skript.error("You can not set the recipe result of existing recipes.");
+		} else if (mode == ChangeMode.SET) {
+			return CollectionUtils.array(ItemStack.class);
+		}
+		return null;
+	}
+
+	@Override
+	public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+		if (delta == null || !(event instanceof CreateRecipeEvent recipeEvent))
+			return;
+
+		MutableRecipe mutableRecipe = recipeEvent.getMutableRecipe();
+
+		ItemStack result = (ItemStack) delta[0];
+		mutableRecipe.setResult(result);
+	}
+
+	@Override
+	public Class<ItemStack> getReturnType() {
+		return ItemStack.class;
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe result item";
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeType.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeType.java
new file mode 100644
index 00000000000..95f3dbd30dd
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprRecipeType.java
@@ -0,0 +1,44 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+
+@Name("Recipe Type")
+@Description("Get the recipe type of a recipe.")
+@Examples({
+	"loop all of the server's recipes:",
+		"\tbroadcast the recipe type of loop-recipe"
+})
+@Since("INSERT VERSION")
+public class ExprRecipeType extends SimplePropertyExpression<Recipe, RecipeType> {
+
+	static {
+		register(ExprRecipeType.class, RecipeType.class, "recipe type", "recipes");
+	}
+
+	@Override
+	public @Nullable RecipeType convert(Recipe recipe) {
+		if (recipe instanceof MutableRecipe mutableRecipe)
+			return mutableRecipe.getRecipeType();
+		return RecipeUtils.getRecipeType(recipe);
+	}
+
+	@Override
+	protected String getPropertyName() {
+		return "recipe type";
+	}
+
+	@Override
+	public Class<RecipeType> getReturnType() {
+		return RecipeType.class;
+	}
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprSecCreateRecipe.java b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprSecCreateRecipe.java
new file mode 100644
index 00000000000..f42b9fdf505
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/recipes/elements/ExprSecCreateRecipe.java
@@ -0,0 +1,189 @@
+package org.skriptlang.skript.bukkit.recipes.elements;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.config.Node;
+import ch.njol.skript.config.SectionNode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SectionExpression;
+import ch.njol.skript.lang.*;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.registrations.EventValues;
+import ch.njol.skript.variables.Variables;
+import ch.njol.util.Kleenean;
+import org.bukkit.NamespacedKey;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.Recipe;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.recipes.CreateRecipeEvent;
+import org.skriptlang.skript.bukkit.recipes.MutableRecipe;
+import org.skriptlang.skript.bukkit.recipes.RecipeUtils.RecipeType;
+import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Name("New Recipe")
+@Description({
+	"Create a custom recipe for any of the following types:",
+	"shaped, shapeless, blasting, furnace, campfire, smoking, smithing transform, smithing trim, stonecutting or transmute.",
+	"",
+	"All recipes except smithing trim require a 'result item'.",
+	"Blasting, furnace, campfire and smoking all fall under cooking recipe type.",
+	"Groups only apply to shaped, shapeless, cooking, and stonecutting recipes.",
+	"Category only applies to shaped, shapeless and cooking recipes.",
+	"You can not create a cooking, crafting and complex recipe type.",
+	"Custom recipes are not persistent across server restart.",
+	"MC versions below 1.20 convert smithing transform and smithing trim recipes to a smithing recipe when added to the server."
+})
+@Examples({
+	"set {_recipe} to a new shaped recipe with the key \"my_recipe\":",
+		"\tset the recipe ingredients to diamond, air, diamond, air, emerald, air, diamond and diamond #OR",
+		"\tset recipe ingredients of 1st row to diamond, air and diamond",
+		"\tset recipe ingredients of second row to air, emerald and air",
+		"\tset recipe ingredients of 3rd row to diamond, air and diamond",
+		"\tset recipe group to \"custom group\"",
+		"\tset recipe category to misc crafting category",
+		"\tset recipe result to diamond sword named \"Heavenly Sword\"",
+	"",
+	"set {_recipe} to a new shapeless recipe with id \"my_recipe\":",
+		"\tset recipe ingredients to 3 diamonds, 3 emeralds and 3 iron ingots",
+		"\tset the recipe group to \"custom group\"",
+		"\tset the recipe category to misc crafting category",
+		"\tset the recipe result item to diamond helmet named \"Heavenly Helm\"",
+	"",
+	"#Furnace, Campfire and Smoking follow same format as Blasting example",
+	"set {_recipe} to a new blasting recipe with the id \"my_recipe\":",
+		"\tset the recipe experience to 5",
+		"\tset the recipe cooking time to 10 seconds",
+		"\tset the recipe group to \"custom group\"",
+		"\tset the recipe category to misc cooking category",
+		"\tset the recipe input item to coal named \"Ash\"",
+		"\tset the recipe result item to gunpowder named \"Dust\"",
+	"",
+	"#Smithing Trim follows the same format, except for 'result item'",
+	"set {_recipe} to a new smithing transform recipe with key \"my_recipe\":",
+		"\tset the recipe base item to diamond helmet",
+		"\tset the recipe template item to paper named \"Blueprint\"",
+		"\tset the recipe addition item to netherite ingot named \"Pure Netherite\"",
+		"\tset the recipe result to netherite helmet named \"Pure Helmet\"",
+	"",
+	"set {_recipe} to a new stonecutting recipe with id \"my_recipe\":",
+		"\tset the recipe source item to cobblestone named \"Cracked Stone\"",
+		"\tset the recipe group to \"custom group\"",
+		"\tset the recipe result to stone named \"Refurnished Stone\"",
+	"",
+	"set {_recipe} to a new transmute recipe with key \"my_recipe\":",
+		"\tset the recipe input item to leather helmet",
+		"\tset the recipe transmute item to nether star named \"Free Upgrade\"",
+		"\tset the recipe result to netherite helmet"
+})
+@Since("INSERT VERSION")
+public class ExprSecCreateRecipe extends SectionExpression<Recipe> implements SyntaxRuntimeErrorProducer {
+
+	private static final boolean SUPPORT_SMITHING = !Skript.isRunningMinecraft(1, 20, 0);
+
+	static {
+		Skript.registerExpression(ExprSecCreateRecipe.class, Recipe.class, ExpressionType.SIMPLE,
+			"a new %*recipetype% with [the] (key|id) %string%");
+		EventValues.registerEventValue(CreateRecipeEvent.class, Recipe.class, CreateRecipeEvent::getMutableRecipe);
+	}
+
+	private RecipeType providedType;
+	private Expression<String> providedName;
+	private Trigger trigger;
+	private Node node;
+	private String rawExpr;
+
+	@Override
+	public boolean init(Expression<?>[] exprs, int pattern, Kleenean isDelayed, ParseResult result, @Nullable SectionNode node, @Nullable List<TriggerItem> triggerItems) {
+		if (node == null) {
+			Skript.error("Creating a new recipe requires a section.");
+			return false;
+		}
+		//noinspection unchecked
+		providedType = ((Literal<RecipeType>) exprs[0]).getSingle();
+		if (providedType == RecipeType.COOKING || providedType == RecipeType.CRAFTING || providedType == RecipeType.COMPLEX) {
+			Skript.error("You can not create a new '" + providedType + "' recipe type.");
+			return false;
+		} else if (providedType == RecipeType.SMITHING && !SUPPORT_SMITHING) {
+			Skript.error("You can not create a new 'smithing' recipe type on MC version 1.20+.");
+			return false;
+		} else if (providedType.getRecipeClass() == null) {
+			Skript.error("You can not create a new '" + providedType + "' recipe type on this MC version.");
+			return false;
+		}
+		//noinspection unchecked
+		providedName = (Expression<String>) exprs[1];
+		AtomicBoolean delayed = new AtomicBoolean(false);
+		Runnable afterLoading = () -> delayed.set(!getParser().getHasDelayBefore().isFalse());
+		//noinspection unchecked
+		trigger = loadCode(node, "create recipe", afterLoading, providedType.getEventClass());
+		if (delayed.get()) {
+			Skript.error("Delays cannot be used within a 'create recipe' section.");
+			return false;
+		}
+		this.node = getParser().getNode();
+		rawExpr = result.expr;
+		return true;
+	}
+
+	@Override
+	protected Recipe @Nullable [] get(Event event) {
+		String name = providedName.getSingle(event);
+		if (name == null || name.isEmpty()) {
+			error("The id for a recipe must not be null nor empty.");
+			return null;
+		}
+		NamespacedKey key = NamespacedKey.fromString(name, Skript.getInstance());
+		if (key == null) {
+			error("The provided id is invalid.");
+			return null;
+		}
+		CreateRecipeEvent recipeEvent = providedType.createRecipeEvent(key);
+		if (recipeEvent == null)
+			throw new IllegalStateException("Unexpected value: " + providedType);
+
+		Variables.withLocalVariables(event, recipeEvent, () -> TriggerItem.walk(trigger, recipeEvent));
+		// If any of the used expressions or effects produce an error, fail creation
+		if (recipeEvent.getErrorInSection())
+			return null;
+
+		MutableRecipe recipeWrapper = recipeEvent.getMutableRecipe();
+		Recipe recipe = recipeWrapper.create();
+		// If the recipe failed to build
+		if (recipe == null) {
+			error(recipeWrapper.getErrors().toString());
+			return null;
+		}
+		return new Recipe[]{recipe};
+	}
+
+	@Override
+	public boolean isSingle() {
+		return true;
+	}
+
+	@Override
+	public Class<Recipe> getReturnType() {
+		return Recipe.class;
+	}
+
+	@Override
+	public Node getNode() {
+		return node;
+	}
+
+	@Override
+	public @Nullable String toHighlight() {
+		return rawExpr;
+	}
+
+	@Override
+	public String toString(@Nullable Event event, boolean debug) {
+		return "a new " + providedType + " recipe with the key " + providedName.toString(event, debug);
+	}
+
+}
diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang
index 999c0f394bd..bf23af59409 100644
--- a/src/main/resources/lang/default.lang
+++ b/src/main/resources/lang/default.lang
@@ -2555,6 +2555,33 @@ input keys:
 	sneak: sneak key, sneaking key
 	sprint: sprint key, sprinting key
 
+# -- Recipe Categories --
+recipe categories:
+	crafting_building: building crafting category
+	crafting_equipment: equipment crafting category
+	crafting_misc: misc crafting category, miscellaneous crafting category
+	crafting_redstone: redstone crafting category
+	cooking_blocks: blocks cooking category
+	cooking_food: food cooking category
+	cooking_misc: misc cooking category, miscellaneous cooking category
+
+# -- Recipe Types --
+recipe types:
+	crafting: crafting recipe, crafting recipes
+	shaped: shaped recipe, shaped crafting recipe, shaped recipes, shaped crafting recipes
+	shapeless: shapeless recipe, shapeless crafting recipe, shapeless recipes, shapeless crafting recipes
+	cooking: cooking recipe, cooking recipes
+	blasting: blasting recipe, blasting recipes
+	furnace: furnace recipe, furnace recipes
+	campfire: campfire recipe, campfire recipes
+	smoking: smoking recipe, smoking recipes
+	smithing: smithing recipe, smithing recipes
+	smithing_transform: smithing transform recipe, smithing transform recipes
+	smithing_trim: smithing trim recipe, smithing trim recipes
+	stonecutting: stonecutting recipe, stonecutting recipes
+	complex: complex recipe, complex recipes
+	transmute: transmute recipe, transmute recipes
+
 # -- Boolean --
 boolean:
 	true:
@@ -2653,6 +2680,7 @@ types:
 	lootcontext: loot context¦s @a
 	bannerpatterntype: banner pattern type¦s @a
 	bannerpattern: banner pattern¦s @a
+	recipe: recipe¦s @a
 
 	# Skript
 	weathertype: weather type¦s @a
@@ -2681,6 +2709,8 @@ types:
 	named: named thing¦s @a
 	numbered: numbered thing¦s @a
 	containing: container¦s @a
+	recipetype: recipe type¦s @a
+	recipecategory: recipe categor¦y¦ies @a
 
 	# Hooks
 	money: money
diff --git a/src/test/skript/tests/syntaxes/effects/EffRemoveRecipe.sk b/src/test/skript/tests/syntaxes/effects/EffRemoveRecipe.sk
new file mode 100644
index 00000000000..0b5c8d6150d
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/effects/EffRemoveRecipe.sk
@@ -0,0 +1,10 @@
+test "remove recipe":
+	set {_recipe} to a new blasting recipe with the key "test_remove_recipe":
+		set the recipe input item to diamond named "test_remove_recipe"
+		set the recipe result item to nether star named "test_remove_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_remove_recipe"
+	assert {_get} is set with "Blasting recipe was not added to the server"
+	remove recipe with the key "test_remove_recipe" from the server
+	set {_get} to recipe with the key "test_remove_recipe"
+	assert {_get} is not set with "Recipe was not removed from the server"
diff --git a/src/test/skript/tests/syntaxes/effects/EffSmithingCopy.sk b/src/test/skript/tests/syntaxes/effects/EffSmithingCopy.sk
new file mode 100644
index 00000000000..96ba3dcb748
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/effects/EffSmithingCopy.sk
@@ -0,0 +1,19 @@
+test "smithing recipe data copy" when running minecraft "1.20.0":
+	###
+		Combines:
+			EffSmithingData
+			CondSmithingData
+	###
+	set {_base} to diamond named "test_smithing_recipe_data_component_copy"
+	set {_template} to netherite ingot named "test_smithing_recipe_data_component_copy"
+	set {_addition} to emerald named "test_smithing_recipe_data_component_copy"
+	set {_recipe} to a new smithing transform recipe with the key "test_smithing_recipe_data_component_copy":
+		set the recipe base item to {_base}
+		set the recipe template item to {_template}
+		set the recipe addition item to {_addition}
+		set the recipe result to netherite sword named "test_smithing_recipe_data_component_copy"
+		disallow the item data to be copied from the base item to the result item
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_smithing_recipe_data_component_copy"
+	assert {_recipe} does not allow data copying with "Created recipe should not allow data component copying"
+	assert {_get} does not allow data copying with "Retrieved recipe should not allow data component copying"
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprAllRecipes.sk b/src/test/skript/tests/syntaxes/expressions/ExprAllRecipes.sk
new file mode 100644
index 00000000000..415de88f728
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprAllRecipes.sk
@@ -0,0 +1,17 @@
+test "all recipes":
+	set {_all} to size of all of the server's recipes
+	assert {_all} > 0 with "All recipes did not return sufficient data"
+	set {_all_shaped} to size of all of the server's shaped recipes
+	assert {_all_shaped} > 0 with "All shaped recipes did not return sufficient data"
+	set {_custom} to size of all of the server's custom recipes
+	set {_custom_shaped} to size of all of the server's custom shaped recipes
+
+	set {_recipe} to a new shaped recipe with the key "test_all_recipes":
+		set the recipe ingredients to raw iron, raw gold and raw copper
+		set the recipe result to lightning rod
+	add {_recipe} to the server's recipes
+
+	assert (size of all of the server's recipes) = ({_all} + 1) with "Mismatching size of all comparison"
+	assert (size of all of the server's shaped recipes) = ({_all_shaped} + 1) with "Mismatching size of all shaped comparison"
+	assert (size of all of the server's custom recipes) = ({_custom} + 1) with "Mismatching size of all custom comparison"
+	assert (size of all of the server's custom shaped recipes) = ({_custom_shaped} + 1) with "Mismacthing size of all custom shaped comparison"
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRecipeIngredients.sk b/src/test/skript/tests/syntaxes/expressions/ExprRecipeIngredients.sk
new file mode 100644
index 00000000000..405c5dba1f7
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprRecipeIngredients.sk
@@ -0,0 +1,79 @@
+test "recipe ingredients":
+	set {_ingredients::*} to raw iron, raw gold, raw copper, iron ingot, gold ingot, copper ingot, diamond, emerald and netherite ingot
+	set {_recipe} to a new shaped recipe with the key "test_recipe_ingredients":
+		set the recipe ingredients to {_ingredients::*}
+		set the recipe result to nether star named "test_recipe_ingredients"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_ingredients"
+	assert the recipe ingredients of {_recipe} contains {_ingredients::*} with "Mismatch of recipe ingredients from created recipe"
+	assert the recipe ingredients of {_get} contains {_ingredients::*} with "Mismatch of recipe ingredients from retrieved recipe"
+
+test "recipe ingredients of rows":
+	set {_first::*} to diamond, emerald and netherite ingot
+	set {_second::*} to copper ingot, gold ingot and iron ingot
+	set {_third::*} to raw copper, raw gold and raw iron
+	set {_recipe} to a new shaped recipe with the key "test_recipe_ingredients_rows":
+		set the recipe ingredients of first row to {_first::*}
+		set the recipe ingredients of second row to {_second::*}
+		set the recipe ingredients of third row to {_third::*}
+		set the recipe result to nether star named "test_recipe_ingredients_rows"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_ingredients_rows"
+	assert the recipe ingredients of first row of {_recipe} contains {_first::*} with "Mismatch of recipe ingredients first row from created recipe"
+	assert the recipe ingredients of second row of {_recipe} contains {_second::*} with "Mismatch of recipe ingredients second row from created recipe"
+	assert the recipe ingredients of third row of {_recipe} contains {_third::*} with "Mismatch of recipe ingredients third row from created recipe"
+	assert the recipe ingredients of first row of {_get} contains {_first::*} with "Mismatch of recipe ingredients first row from retrieved recipe"
+	assert the recipe ingredients of second row of {_get} contains {_second::*} with "Mismatch of recipe ingredients second row from retrieved recipe"
+	assert the recipe ingredients of third row of {_get} contains {_third::*} with "Mismatch of recipe ingredients third row from retrieved recipe"
+
+test "recipe cooking input":
+	set {_input} to grass block named "test_recipe_cooking_input"
+	set {_recipe} to a new blasting recipe with the key "test_recipe_cooking_input":
+		set the recipe input item to {_input}
+		set the recipe result to nether star named "test_recipe_cooking_input"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_cooking_input"
+	assert the recipe input item of {_recipe} = {_input} with "Mismatch of recipe input item from created recipe"
+	assert the recipe input item of {_get} = {_input} with "Mismatch of recipe input item from retrieved recipe"
+
+test "recipe stonecutting input":
+	set {_input} to stone named "test_recipe_stonecutting_input"
+	set {_recipe} to a new stonecutting recipe with the key "test_recipe_stonecutting_input":
+		set the recipe input item to {_input}
+		set the recipe result to smooth stone named "test_recipe_stonecutting_input"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_stonecutting_input"
+	assert the recipe input item of {_recipe} = {_input} with "Mismatch of recipe input item from created recipe"
+	assert the recipe input item of {_get} = {_input} with "Mismatch of recipe input item from retrieved recipe"
+
+test "recipe base, template and addition item" when running minecraft "1.20":
+	set {_base} to diamond named "test_recipe_base_template_addition"
+	set {_template} to netherite ingot named "test_recipe_base_template_addition"
+	set {_addition} to emerald named "test_recipe_base_template_addition"
+	set {_recipe} to a new smithing transform recipe with the key "test_recipe_base_template_addition":
+		set the recipe base item to {_base}
+		set the recipe template item to {_template}
+		set the recipe addition item to {_addition}
+		set the recipe result to netherite sword named "test_recipe_base_template_addition"
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_base_template_addition"
+	assert the recipe base item of {_recipe} = {_base} with "Mismatch of recipe base item from created recipe"
+	assert the recipe template item of {_recipe} = {_template} with "Mismatch of recipe template item from created recipe"
+	assert the recipe addition item of {_recipe} = {_addition} with "Mismatch of recipe addition item from created recipe"
+	assert the recipe base item of {_get} = {_base} with "Mismatch of recipe base item from retrieved recipe"
+	assert the recipe template item of {_get} = {_template} with "Mismatch of recipe template item from retrieved recipe"
+	assert the recipe addition item of {_get} = {_addition} with "Mismatch of recipe addition item from retrieved recipe"
+
+test "recipe transmute item" when running minecraft "1.21.2":
+	set {_recipe} to a new transmute recipe with the key "test_recipe_transmute":
+		set the recipe input item to a leather helmet
+		set the recipe transmute item to a nether star
+		set the recipe result item to a netherite helmet
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_transmute"
+	assert the recipe input item of {_recipe} is a leather helmet with "Mismatch of recipe input item from created recipe"
+	assert the recipe transmute item of {_recipe} is a nether star with "Mismatch of recipe transmute item from created recipe"
+	assert the recipe result item of {_recipe} is a netherite helmet with "Mismatch of recipe result item from created recipe"
+	assert the recipe input item of {_get} is a leather helmet with "Mismatch of recipe input item from retrieved recipe"
+	assert the recipe transmute item of {_get} is a nether star with "Mismatch of recipe transmute item from retrieved recipe"
+	assert the recipe result item of {_get} is a netherite helmet with "Mismatch of recipe result item from retrieved recipe"
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRecipeResult.sk b/src/test/skript/tests/syntaxes/expressions/ExprRecipeResult.sk
new file mode 100644
index 00000000000..80793b994af
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprRecipeResult.sk
@@ -0,0 +1,9 @@
+test "recipe result":
+	set {_result} to copper ingot named "test_recipe_result"
+	set {_recipe} to a new blasting recipe with key "test_recipe_result":
+		set the recipe input item to raw copper named "test_recipe_result"
+		set the recipe result to {_result}
+	add {_recipe} to the server's recipes
+	set {_get} to recipe with the key "test_recipe_result"
+	assert the recipe result of {_recipe} = {_result} with "Result mismatch of created recipe"
+	assert the recipe result of {_get} = {_result} with "Result mismatch of retrieved recipe"
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprRecipeValues.sk b/src/test/skript/tests/syntaxes/expressions/ExprRecipeValues.sk
new file mode 100644
index 00000000000..3fcb91cd4ac
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprRecipeValues.sk
@@ -0,0 +1,59 @@
+test "cooking recipe values":
+	###
+		Combines:
+			ExprRecipeCookingTime
+			ExprRecipeCategory
+			ExprRecipeExperience
+			ExprRecipeGroup
+			ExprGetRecipe
+			ExprRecipeKey
+	###
+
+	set {_recipe} to a new blasting recipe with the key "test_cooking_recipe_values":
+		set the recipe input item to raw copper named "test_cooking_recipe_values"
+		set the recipe cooking time to 20 seconds
+		set the recipe category to misc cooking category
+		set the recipe experience to 100
+		set the recipe group to "test_cooking_recipe_values"
+		set the recipe result to copper ingot named "test_cooking_recipe_values"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_cooking_recipe_values"
+	assert {_get} is set with "Blasting recipe was not added to the server"
+
+	assert the recipe cooking time of {_recipe} is 20 seconds with "Cooking time mismatch of created recipe"
+	assert the recipe category of {_recipe} is misc cooking category with "Cooking category mismatch of created recipe"
+	assert the recipe experience of {_recipe} is 100 with "Experience mismatch of created recipe"
+	assert the recipe group of {_recipe} is "test_cooking_recipe_values" with "Group mismatch of created recipe"
+	assert the recipe key of {_recipe} is "skript:test_cooking_recipe_values" with "Recipe key mismatch of created recipe"
+
+	assert the recipe cooking time of {_get} is 20 seconds with "Cooking time mismatch of retrieved recipe"
+	assert the recipe category of {_get} is misc cooking category with "Cooking category mismatch of retrieved recipe"
+	assert the recipe experience of {_get} is 100 with "Experience mismatch of retrieved recipe"
+	assert the recipe group of {_get} is "test_cooking_recipe_values" with "Group mismatch of retrieved recipe"
+	assert the recipe key of {_get} is "skript:test_cooking_recipe_values" with "Recipe key mismatch of retrieved recipe"
+
+test "crafting recipe values":
+	###
+		Combines:
+			ExprRecipeCategory
+			ExprRecipeGroup
+			ExprGetRecipe
+			ExprRecipeKey
+	###
+
+	set {_recipe} to a new shaped recipe with the key "test_crafting_recipe_values":
+		set the recipe ingredients to oak log, oak planks and oak wood
+		set the recipe category to misc crafting category
+		set the recipe group to "test_crafting_recipe_values"
+		set the recipe result to oak sapling
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_crafting_recipe_values"
+	assert {_get} is set with "Shaped recipe was not added to the server"
+
+	assert the recipe category of {_recipe} is misc crafting category with "Crafting category mismatch of created recipe"
+	assert the recipe group of {_recipe} is "test_crafting_recipe_values" with "Group mismatch of created recipe"
+	assert the recipe key of {_recipe} is "skript:test_crafting_recipe_values" with "Recipe key mismatch of created recipe"
+
+	assert the recipe category of {_get} is misc crafting category with "Crafting category mismatch of retrieved recipe"
+	assert the recipe group of {_get} is "test_crafting_recipe_values" with "Group mismatch of retrieved recipe"
+	assert the recipe key of {_get} is "skript:test_crafting_recipe_values" with "Recipe key mismatch of retrieved recipe"
diff --git a/src/test/skript/tests/syntaxes/sections/ExprSecCreateRecipe.sk b/src/test/skript/tests/syntaxes/sections/ExprSecCreateRecipe.sk
new file mode 100644
index 00000000000..d96194e9e48
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/sections/ExprSecCreateRecipe.sk
@@ -0,0 +1,147 @@
+###
+	Combines:
+		ExprSecCreateRecipe
+		ExprRecipeType
+###
+
+test "register shaped recipe":
+	set {_recipe} to a new shaped recipe with the key "test_register_shaped_recipe":
+		set the recipe ingredients to diamond, air, diamond, air, emerald, air, diamond, air and diamond
+		set the recipe result to netherite sword named "test_register_shaped_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_shaped_recipe"
+	assert {_get} is set with "Shaped recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is shaped recipe with "Created recipe is not a shaped recipe"
+	assert the recipe type of {_get} is shaped recipe with "Retrieved recipe is not a shaped recipe"
+
+test "register shapeless recipe":
+	set {_recipe} to a new shapeless recipe with the key "test_register_shapeless_recipe":
+		set the recipe ingredients to diamond, iron ingot and netherite ingot
+		set the recipe result to netherite sword named "test_register_shapeless_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_shapeless_recipe"
+	assert {_get} is set with "Shapeless recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is shapeless recipe with "Created recipe is not a shapeless recipe"
+	assert the recipe type of {_get} is shapeless recipe with "Retrieved recipe is not a shapeless recipe"
+
+test "register shapeless recipe fail":
+	parse:
+		set {_recipe} to a new shapeless recipe with the key "test_register_shapeless_recipe_fail":
+			set the recipe ingredients of the 1st row to iron ingot, diamond and gold ingot #Fail
+			set the recipe result to netherite sword named "test_register_shapeless_recipe_fail"
+	assert last parse logs contains "This can only be used when creating a Shaped Recipe." with "Ingredients of a row did not fail for Shapeless recipe"
+
+test "register blasting recipe":
+	set {_recipe} to a new blasting recipe with the key "test_register_blasting_recipe":
+		set the recipe input item to raw iron named "test_register_blasting_recipe"
+		set the recipe result to iron ingot named "test_register_blasting_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_blasting_recipe"
+	assert {_get} is set with "Blasting recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is blasting recipe with "Created recipe is not a blasting recipe"
+	assert the recipe type of {_get} is blasting recipe with "Retrieved recipe is not a blasting recipe"
+
+test "register furnace recipe":
+	set {_recipe} to a new furnace recipe with the key "test_register_furnace_recipe":
+		set the recipe input item to raw gold named "test_register_furnace_recipe"
+		set the recipe result to gold ingot named "test_register_furnace_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_furnace_recipe"
+	assert {_get} is set with "Furnace recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is furnace recipe with "Created recipe is not a furnace recipe"
+	assert the recipe type of {_get} is furnace recipe with "Retrieved recipe is not a furnace recipe"
+
+test "register campfire recipe":
+	set {_recipe} to a new campfire recipe with the key "test_register_campfire_recipe":
+		set the recipe input item to raw copper named "test_register_campfire_recipe"
+		set the recipe result to copper ingot named "test_register_campfire_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_campfire_recipe"
+	assert {_get} is set with "Campfire recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is campfire recipe with "Created recipe is not a campfire recipe"
+	assert the recipe type of {_get} is campfire recipe with "Retrieved recipe is not a campfire recipe"
+
+test "register smoking recipe":
+	set {_recipe} to a new smoking recipe with the key "test_register_smoking_recipe":
+		set the recipe input item to raw iron named "test_register_smoking_recipe"
+		set the recipe result to iron ingot named "test_register_smoking_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_smoking_recipe"
+	assert {_get} is set with "Smoking recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is smoking recipe with "Created recipe is not a smoking recipe"
+	assert the recipe type of {_get} is smoking recipe with "Retrieved recipe is not a smoking recipe"
+
+test "register smithing recipe" when running below minecraft "1.20":
+	set {_recipe} to a new smithing recipe with the key "test_register_smithing_recipe":
+		set the recipe base item to iron ingot named "test_register_smithing_recipe"
+		set the recipe additional item to gold ingot named "test_register_smithing_recipe"
+		set the recipe result to netherite ingot named "test_register_smithing_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_smithing_recipe"
+	assert {_get} is set with "Smithing recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is smithing recipe with "Created recipe is not a smithing recipe"
+	assert the recipe type of {_get} is smithing recipe with "Retrieved recipe is not a smithing recipe"
+
+test "register smithing transform recipe":
+	set {_recipe} to a new smithing transform recipe with the key "test_register_smithing_transform_recipe":
+		set the recipe base item to iron ingot named "test_register_smithing_transform_recipe"
+		set the recipe additional item to gold ingot named "test_register_smithing_transform_recipe"
+		set the recipe template item to diamond named "test_register_smithing_transform_recipe"
+		set the recipe result to netherite ingot named "test_register_smithing_transform_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_smithing_transform_recipe"
+	assert {_get} is set with "Smithing Transform recipe was not added to the server"
+
+	if running below minecraft "1.20":
+		assert the recipe type of {_recipe} is smithing recipe with "Created recipe is not a smithing recipe"
+		assert the recipe type of {_get} is smithing recipe with "Retrieved recipe is not a smithing recipe"
+	else:
+		assert the recipe type of {_recipe} is smithing transform recipe with "Created recipe is not a smithing transform recipe"
+		assert the recipe type of {_get} is smithing transform recipe with "Retrieved recipe is not a smithing transform recipe"
+
+test "register smithing trim recipe":
+	set {_recipe} to a new smithing trim recipe with the key "test_register_smithing_trim_recipe":
+		set the recipe base item to iron ingot named "test_register_smithing_trim_recipe"
+		set the recipe additional item to gold ingot named "test_register_smithing_trim_recipe"
+		set the recipe template item to diamond named "test_register_smithing_trim_recipe"
+		set the recipe result to netherite ingot named "test_register_smithing_trim_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_smithing_trim_recipe"
+	assert {_get} is set with "Smithing Trim recipe was not added to the server"
+
+	if running below minecraft "1.20":
+		assert the recipe type of {_recipe} is smithing recipe with "Created recipe is not a smithing recipe"
+		assert the recipe type of {_get} is smithing recipe with "Retrieved recipe is not a smithing recipe"
+	else:
+		assert the recipe type of {_recipe} is smithing trim recipe with "Created recipe is not a smithing trim recipe"
+		assert the recipe type of {_get} is smithing trim recipe with "Retrieved recipe is not a smithing trim recipe"
+
+test "register stonecutting recipe":
+	set {_recipe} to a new stonecutting recipe with the key "test_register_stonecutting_recipe":
+		set the recipe input item to stone named "test_register_stonecutting_recipe"
+		set the recipe result item to smooth stone named "test_register_stonecutting_recipe"
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_stonecutting_recipe"
+	assert {_get} is set with "Stonecutting recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is stonecutting recipe with "Created recipe is not a stonecutting recipe"
+	assert the recipe type of {_get} is stonecutting recipe with "Retrieved recipe is not a stonecutting recipe"
+
+test "register transmute recipe" when running minecraft "1.21.2":
+	set {_recipe} to a new transmute recipe with the key "test_register_transmute_recipe":
+		set the recipe input item to leather helmet
+		set the recipe transmute item to nether star
+		set the recipe result item to netherite helmet
+	add {_recipe} to the server's recipes
+	set {_get} to the recipe with the key "test_register_transmute_recipe"
+	assert {_get} is set with "Transmute recipe was not added to the server"
+
+	assert the recipe type of {_recipe} is transmute recipe with "Created recipe is not a transmute recipe"
+	assert the recipe type of {_recipe} is transmute recipe with "Retrieved recipe is not a transmute recipe"