Is State Tree Request Transition Broken in 5.4?

I found a way to fix this for 5.5 and lower. It will involve some C++, if you’re using Unreal source build, just add this into FStateTreeTransitionRequest Request; in RequestTransition function

Request.SourceStateTree = CachedFrameStateTree.Get();
Request.SourceRootState = CachedFrameRootState;

Or if not, you can make a base class for the task and use my custom transition function

// © 2025 Team Bacon. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/StateTreeTaskBlueprintBase.h"
#include "BrawlStateTreeTaskBlueprintBase.generated.h"

/**
 * Base class for Blueprint based State Tree Task
 * This class fixes an issue in UE5.5 and earlier where RequestTransition called from Blueprint tasks
 */
UCLASS()
class PROJECTBRAWL_API UBrawlStateTreeTaskBlueprintBase : public UStateTreeTaskBlueprintBase
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "StateTree", meta = (HideSelfPin = "true", DisplayName = "Brawl StateTree Request Transition"))
	void BrawlRequestTransition(const FStateTreeStateLink& TargetState, const EStateTreeTransitionPriority Priority = EStateTreeTransitionPriority::Normal);

#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION <= 5)
public:
	virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) override;
	virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) override;

protected:
	void BrawlSetCachedInstanceDataFromContext(const FStateTreeExecutionContext& Context) const;
	void BrawlClearCachedInstanceData() const;
	
private: 
	/** Cached instance data while the node is active. */
	mutable TWeakPtr<FStateTreeInstanceStorage> WeakInstanceStorage;

	/** Cached owner while the node is active. */
	// It's TObjectPtr in base class so we only need TWeakObjectPtr here, the base class will keep it alive
	mutable TWeakObjectPtr<UObject> CachedOwner = nullptr;

	/** Cached State Tree of owning execution frame. */ 
	// It's TObjectPtr in base class so we only need TWeakObjectPtr here, the base class will keep it alive
	mutable TWeakObjectPtr<const UStateTree> CachedFrameStateTree = nullptr;

	/** Cached root state of owning execution frame. */
	mutable FStateTreeStateHandle CachedFrameRootState;

	/** Cached State where the node is processed on. */
	mutable FStateTreeStateHandle CachedState;
#endif
};

// © 2025 Team Bacon. All Rights Reserved.


#include "AI/ST/BrawlStateTreeTaskBlueprintBase.h"

#include "StateTreeExecutionContext.h" 

void UBrawlStateTreeTaskBlueprintBase::BrawlRequestTransition(const FStateTreeStateLink& TargetState,
	const EStateTreeTransitionPriority Priority)
{ 
#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION <= 5)
	TSharedPtr<FStateTreeInstanceStorage> InstanceStorage = WeakInstanceStorage.Pin();

	if (InstanceStorage == nullptr || CachedOwner == nullptr)
	{
		UE_VLOG_UELOG(this, LogStateTree, Error, TEXT("Trying to call SendEvent() while node is not active. Use SendEvent() on UStateTreeComponent instead for sending signals externally."));
		return;
	}

	FStateTreeTransitionRequest Request;
	Request.SourceState = CachedState;
	Request.TargetState = TargetState.StateHandle;
	Request.Priority = Priority;

	// The fix
	Request.SourceStateTree = CachedFrameStateTree.Get();
	Request.SourceRootState = CachedFrameRootState;
	
	InstanceStorage->AddTransitionRequest(CachedOwner.Get(), Request);
#else
	// Fixed in 5.6, fallback to base implementation.
	RequestTransition(TargetState, Priority);
#endif
}

#if (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION <= 5)

EStateTreeRunStatus UBrawlStateTreeTaskBlueprintBase::EnterState(FStateTreeExecutionContext& Context,
                                                                 const FStateTreeTransitionResult& Transition)
{
	BrawlSetCachedInstanceDataFromContext(Context);
	return Super::EnterState(Context, Transition);
}

void UBrawlStateTreeTaskBlueprintBase::ExitState(FStateTreeExecutionContext& Context,
	const FStateTreeTransitionResult& Transition)
{ 
	BrawlClearCachedInstanceData(); 
	
	Super::ExitState(Context, Transition);
}

void UBrawlStateTreeTaskBlueprintBase::BrawlSetCachedInstanceDataFromContext(const FStateTreeExecutionContext& Context) const
{
	if (FStateTreeInstanceData* InstanceData = Context.GetMutableInstanceData())
	{
		WeakInstanceStorage = InstanceData->GetWeakMutableStorage();
	}
	CachedState = Context.GetCurrentlyProcessedState();
	CachedOwner = Context.GetOwner();

	const FStateTreeExecutionFrame* CurrentlyProcessedFrame = Context.GetCurrentlyProcessedFrame();
	check(CurrentlyProcessedFrame);

	CachedFrameStateTree = CurrentlyProcessedFrame->StateTree;
	CachedFrameRootState = CurrentlyProcessedFrame->RootState;
}

void UBrawlStateTreeTaskBlueprintBase::BrawlClearCachedInstanceData() const
{
	WeakInstanceStorage = nullptr;
	CachedState = FStateTreeStateHandle::Invalid;
	CachedOwner = nullptr;
	CachedFrameStateTree = nullptr;
	CachedFrameRootState = FStateTreeStateHandle::Invalid;
}

#endif