Blueprint Compiler Extensions

February 28, 2026
4 min read

Standard Delay nodes in Unreal Engine don’t respect the Gameplay Ability lifecycle. In the Gameplay Ability System (GAS), abilities should use the Wait Delay task node (UAbilityTask_WaitDelay) instead of standard latent delays.

To enforce this automatically, we can create a Blueprint Compiler Extension (UBlueprintCompilerExtension). By hooking directly into the Blueprint compilation process, we can check node graphs during compilation and fail the build if a forbidden node is used.


How Blueprint Compiler Extensions Work

Unreal Engine provides the UBlueprintCompilerExtension class (in the BlueprintCompiler module) to run custom validation rules during Blueprint compilation.

When a developer compiles a Blueprint:

  1. The compiler instantiates all loaded subclasses of UBlueprintCompilerExtension.
  2. It calls ProcessBlueprintCompiled() on each extension.
  3. The extension inspects the Blueprint’s graphs, nodes, and variables.
  4. If an invalid pattern is found, errors are pushed directly to the Compiler Results log, aborting compilation.

Setting Up Your Editor Module

Because compiler extensions rely on editor-only modules like BlueprintCompiler, KismetCompiler, and UnrealEd, this code must live in an Editor Module (e.g., MyProjectEditor). Placing editor dependencies inside a primary game module will break standalone builds.

Step 1: Update the .uproject File

Add the editor module configuration to your project’s .uproject file under "Modules":

MyProject.uproject
		{
			"Name": "MyProjectEditor",
			"Type": "Editor",
			"LoadingPhase": "PostEngineInit",
			"AdditionalDependencies": [
				"MyProject"
			]
		}

Step 2: Update Target Files

In your project’s MyProjectEditor.Target.cs, add the editor module to ExtraModuleNames:

Source/MyProjectEditor.Target.cs
		ExtraModuleNames.AddRange(new string[] { "MyProject", "MyProjectEditor" });

Step 3: Create the Editor Module Build.cs

Create MyProjectEditor.Build.cs in your editor module directory:

Source/MyProjectEditor/MyProjectEditor.Build.cs
using UnrealBuildTool;

public class MyProjectEditor : ModuleRules
{
	public MyProjectEditor(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] {
			"Core",
			"CoreUObject",
			"Engine",
			"MyProject"
		});

		PrivateDependencyModuleNames.AddRange(new string[] {
			"UnrealEd",
			"BlueprintGraph",
			"KismetCompiler",
			"Kismet",
			"GameplayAbilities"
		});
	}
}

Step 4: Create Module Header and Source Files

Initialize the editor module with main header and source files:

Source/MyProjectEditor/MyProjectEditor.h
#pragma once

#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"

class FMyProjectEditorModule : public IModuleInterface {
public:
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};
Source/MyProjectEditor/MyProjectEditor.cpp
#include "MyProjectEditor.h"
#include "Modules/ModuleManager.h"
#include "BlueprintCompilationManager.h"
#include "BPCE_Ability_Delay.h"

static UBPCE_Ability_Delay* AbilityDelayCheckExtension = nullptr;

void FMyProjectEditorModule::StartupModule() {
	AbilityDelayCheckExtension = NewObject<UBPCE_Ability_Delay>();
	AbilityDelayCheckExtension->AddToRoot();
	FBlueprintCompilationManager::RegisterCompilerExtension(UBlueprint::StaticClass(), AbilityDelayCheckExtension);
}

void FMyProjectEditorModule::ShutdownModule() {
	if (AbilityDelayCheckExtension) {
		AbilityDelayCheckExtension->RemoveFromRoot();
		AbilityDelayCheckExtension = nullptr;
	}
}

IMPLEMENT_MODULE(FMyProjectEditorModule, MyProjectEditor);

Implementing the Compiler Extension

Implement the validator class inside the editor module:

C++ Header File

Source/MyProjectEditor/BPCE_Ability_Delay.h
#pragma once

#include "CoreMinimal.h"
#include "BlueprintCompilerExtension.h"
#include "BPCE_Ability_Delay.generated.h"

UCLASS()
class MYPROJECTEDITOR_API UBPCE_Ability_Delay : public UBlueprintCompilerExtension {
	GENERATED_BODY()

public:
	virtual void ProcessBlueprintCompiled(const FKismetCompilerContext& CompilationContext, const FBlueprintCompiledData& Data) override;
};

C++ Source File

Source/MyProjectEditor/BPCE_Ability_Delay.cpp
#include "BPCE_Ability_Delay.h"
#include "K2Node_CallFunction.h"
#include "KismetCompiler.h"
#include "Abilities/GameplayAbility.h"
#include "EdGraph/EdGraph.h"
#include "Kismet/KismetSystemLibrary.h"

void UBPCE_Ability_Delay::ProcessBlueprintCompiled(const FKismetCompilerContext& CompilationContext, const FBlueprintCompiledData& Data) {
	const UBlueprint* Blueprint = CompilationContext.Blueprint;
	if (!Blueprint || !Blueprint->ParentClass) {
		return;
	}

	// Check if the blueprint is a Gameplay Ability
	if (!Blueprint->ParentClass->IsChildOf(UGameplayAbility::StaticClass())) {
		return;
	}

	TArray<UEdGraph*> AllGraphs;
	Blueprint->GetAllGraphs(AllGraphs);

	// Change the Blacklisted Function here
	const UClass* LibraryClass = UKismetSystemLibrary::StaticClass();
	static UFunction* DelayFunc = LibraryClass->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Delay));

	for (UEdGraph* Graph : AllGraphs) {
		if (!Graph) {
			continue;
		}
		
		for (UEdGraphNode* Node : Graph->Nodes) {
			if (const UK2Node_CallFunction* FuncNode = Cast<UK2Node_CallFunction>(Node)) {
				const UFunction* Func = FuncNode->GetTargetFunction();

				if (Func == DelayFunc) {
					UE_LOG(LogTemp, Error, TEXT("Critical: Blueprint '%s' contains a forbidden Delay node in graph '%s'!"), *Blueprint->GetName(), *Graph->GetName());

					FText ErrorMessage = FText::FromString(TEXT("Delay nodes are forbidden in Gameplay Abilities. Use 'Wait Delay' node or other latent ability tasks instead (Error in @@)"));
					CompilationContext.MessageLog.Error(*ErrorMessage.ToString(), Node);
				}
			}
		}
	}
}

How It Works

Graph and Node Traversal

Blueprint->GetAllGraphs(AllGraphs) collects all graphs within the Blueprint (event graph, custom functions, macro graphs, interfaces). Inside each graph, we iterate through Graph->Nodes and cast to UK2Node_CallFunction to inspect target function calls.

Function Signature Matching

Instead of string comparisons on function names every compile pass, we cache the target UFunction pointer using FindFunctionByName and match against the node’s target function:

const UClass* LibraryClass = UKismetSystemLibrary::StaticClass();
static UFunction* DelayFunc = LibraryClass->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Delay));

Unreal Engine’s compiler log parser supports the @@ token. When included in the error message, the compiler results window replaces @@ with a clickable hyperlink pointing to the node passed as the second parameter:

FText ErrorMessage = FText::FromString(TEXT("... Error in @@ ..."));

When pushed with CompilationContext.MessageLog.Error(Message, Node), double-clicking the compiler error jumps directly to the problematic node in the graph editor.

Validating Existing Gameplay Abilities

To run validation on all existing Gameplay Ability assets at once:

  1. Filter for GameplayAbility assets in the Content Browser.
  2. Select all matching assets, right-click, and click Validate Assets.

This executes compiler validation across every selected ability and outputs errors for any forbidden Delay nodes directly to the log.


Once UBlueprintCompilerExtension is registered, you can expand it to enforce additional node blacklists or architectural constraints across your Blueprints.