diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 6e370b5ff..a339d2655 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -27,6 +27,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
 import funkin.data.freeplay.album.AlbumRegistry;
 import funkin.data.song.SongRegistry;
 import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.notes.notekind.NoteKindScriptManager;
 import funkin.modding.module.ModuleHandler;
 import funkin.ui.title.TitleState;
 import funkin.util.CLIUtil;
@@ -176,6 +177,8 @@ class InitState extends FlxState
     // Move it to use a BaseRegistry.
     CharacterDataParser.loadCharacterCache();
 
+    NoteKindScriptManager.loadScripts();
+
     ModuleHandler.buildModuleCallbacks();
     ModuleHandler.loadModuleCache();
     ModuleHandler.callOnCreate();
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 8d7d82aab..93306e9d5 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -49,6 +49,7 @@ import funkin.play.notes.NoteSprite;
 import funkin.play.notes.notestyle.NoteStyle;
 import funkin.play.notes.Strumline;
 import funkin.play.notes.SustainTrail;
+import funkin.play.notes.notekind.NoteKindScriptManager;
 import funkin.play.scoring.Scoring;
 import funkin.play.song.Song;
 import funkin.play.stage.Stage;
@@ -1177,7 +1178,12 @@ class PlayState extends MusicBeatSubState
     // Dispatch event to conversation script.
     ScriptEventDispatcher.callEvent(currentConversation, event);
 
-    // TODO: Dispatch event to note scripts
+    // Dispatch event to note script
+    if (Std.isOfType(event, NoteScriptEvent))
+    {
+      var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
+      NoteKindScriptManager.callEvent(noteEvent.note.noteData.kind, noteEvent);
+    }
   }
 
   /**
diff --git a/source/funkin/play/notes/notekind/NoteKindScript.hx b/source/funkin/play/notes/notekind/NoteKindScript.hx
new file mode 100644
index 000000000..baa57b146
--- /dev/null
+++ b/source/funkin/play/notes/notekind/NoteKindScript.hx
@@ -0,0 +1,45 @@
+package funkin.play.notes.notekind;
+
+import funkin.modding.IScriptedClass.INoteScriptedClass;
+import funkin.modding.events.ScriptEvent;
+
+/**
+ * Class for note scripts
+ */
+class NoteKindScript implements INoteScriptedClass
+{
+  /**
+   * the name of the note kind
+   */
+  public var noteKind:String;
+
+  /**
+   * description used in chart editor
+   */
+  public var description:String = "";
+
+  public function new(noteKind:String, description:String = "")
+  {
+    this.noteKind = noteKind;
+    this.description = description;
+  }
+
+  public function toString():String
+  {
+    return noteKind;
+  }
+
+  public function onScriptEvent(event:ScriptEvent):Void {}
+
+  public function onCreate(event:ScriptEvent):Void {}
+
+  public function onDestroy(event:ScriptEvent):Void {}
+
+  public function onUpdate(event:UpdateScriptEvent):Void {}
+
+  public function onNoteIncoming(event:NoteScriptEvent):Void {}
+
+  public function onNoteHit(event:HitNoteScriptEvent):Void {}
+
+  public function onNoteMiss(event:NoteScriptEvent):Void {}
+}
diff --git a/source/funkin/play/notes/notekind/NoteKindScriptManager.hx b/source/funkin/play/notes/notekind/NoteKindScriptManager.hx
new file mode 100644
index 000000000..dc22732b6
--- /dev/null
+++ b/source/funkin/play/notes/notekind/NoteKindScriptManager.hx
@@ -0,0 +1,46 @@
+package funkin.play.notes.notekind;
+
+import funkin.modding.events.ScriptEventDispatcher;
+import funkin.modding.events.ScriptEvent;
+import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+
+class NoteKindScriptManager
+{
+  static var noteKindScripts:Map<String, NoteKindScript> = [];
+
+  public static function loadScripts():Void
+  {
+    var scriptedClassName:Array<String> = ScriptedNoteKindScript.listScriptClasses();
+    if (scriptedClassName.length > 0)
+    {
+      trace('Instantiating ${scriptedClassName.length} scripted note kind...');
+      for (scriptedClass in scriptedClassName)
+      {
+        try
+        {
+          var script:NoteKindScript = ScriptedNoteKindScript.init(scriptedClass, "unknown");
+          trace(' Initialized scripted note kind: ${script.noteKind}');
+          noteKindScripts.set(script.noteKind, script);
+          ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
+        }
+        catch (e)
+        {
+          trace(' FAILED to instantiate scripted note kind: ${scriptedClass}');
+          trace(e);
+        }
+      }
+    }
+  }
+
+  public static function callEvent(noteKind:String, event:ScriptEvent):Void
+  {
+    var noteKindScript:NoteKindScript = noteKindScripts.get(noteKind);
+
+    if (noteKindScript == null)
+    {
+      return;
+    }
+
+    ScriptEventDispatcher.callEvent(noteKindScript, event);
+  }
+}
diff --git a/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx b/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
new file mode 100644
index 000000000..d54a0cde2
--- /dev/null
+++ b/source/funkin/play/notes/notekind/ScriptedNoteKindScript.hx
@@ -0,0 +1,9 @@
+package funkin.play.notes.notekind;
+
+/**
+ * A script that can be tied to a NoteKindScript.
+ * Create a scripted class that extends NoteKindScript,
+ * then call `super('noteKind')` in the constructor to use this.
+ */
+@:hscriptClass
+class ScriptedNoteKindScript extends NoteKindScript implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index 55aab0ab0..f20b75650 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -146,7 +146,7 @@ class ChartEditorDropdowns
     return returnValue;
   }
 
-  static final NOTE_KINDS:Map<String, String> = [
+  public static final NOTE_KINDS:Map<String, String> = [
     // Base
     "" => "Default",
     "~CUSTOM~" => "Custom",