Custom K2 Node and Thunk for an Array of FInstancedStruct.

FYI: This is good for stuff which is derived from a base struct type and you want to filter it.

So there is going to be a lot of code here, and a few overrides of the engine, plus a custom module which is UncookedOnly for the K2Node and its stuff to live in.

The Basics

We will define a struct which will be our base, and 2 derived structs. We will hide the base struct from being selected as it should contain no properties.

USTRUCT(BlueprintType)
struct FMyBaseInstancedStruct
{
    GENERATED_BODY()
};

USTRUCT(BlueprintType)
struct FMyChildAInstancedStruct : public FMyBaseInstancedStruct
{
   GENERATED_BODY();

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    FText SomeText;
};

USTRUCT(BlueprintType)
struct FMyChildBInstancedStruct : public FMyBaseInstancedStruct
{
   GENERATED_BODY();

public:
    UPROPERTY(EditAnywhere, BlueprintReadOnly)
    bool SomeBool;
};

With these structs created and in your main module, lets make a DataAsset that will hold our TArray of FInstancedStruct.

UENUM()
enum class EMyFindInstanceStructResult : uint8
{
	Valid,
	NotValid,
};

UCLASS()
class UMyDataAsset : public UDataAsset
{
   GENERATED_BODY()

protected:
    // Here we define the Array of FInstancedStructs we want, and we use two meta
    // properties to define the type that can be used in this array, and exclude the base
   // struct from being used.
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Data, meta = (BaseStruct = "/Script/MyGameModule.MyBaseInstancedStruct", ExcludeBaseStruct))
	TArray<FInstancedStruct> MyData;

private:
    // Function called from our custom K2 Node. Uses a custom thunk.
    // The Thunk will use the InstancedStructType to find the instanced struct
    // in the array and set the value of Value. This will get converted to the
    // the correct custom type (not here but in our K2Node), but the data in Value
    /// will be valid if the Instanced Struct was found.
    UFUNCTION(BlueprintCallable, CustomThunk, Category = "MyData", meta = (DisplayName = "GetMyData", CustomStructureParam = "Value", ExpandEnumAsExecs = "FindResult", BlueprintInternalUseOnly="true"))
    void GetMyDataBP(EMyFindInstanceStructResult& FindResult, UScriptStruct* InstancedStructType, int32& Value);

    //Needed to declare our custom thunk
	DECLARE_FUNCTION(execGetMyDataBP);

    // So our K2Node can access this private function, we don't want the function above
    // called by anything BUT the K2Node.
	friend class UK2Node_GetMyData;
};

Now we need to implement our CustomThunk in the cpp file

void UMyDataAsset ::GetItemDataBP(EMyFindInstanceStructResult& FindResult, UScriptStruct* InstancedStructType, int32& Value)
{
    // We should never hit this! stubs to avoid NoExport on the class.
    checkNoEntry();
}

DEFINE_FUNCTION(UMyDataAsset::execGetMyDataBP)
{
    //Get the result enum (out ref)
	P_GET_ENUM_REF(EMyFindInstanceStructResult, FindResult);
    // Get the struct type we want to match
	P_GET_OBJECT_REF(UScriptStruct, InstancedStructType);

	// Read wildcard Value input.
	Stack.MostRecentPropertyAddress = nullptr;
	Stack.MostRecentPropertyContainer = nullptr;
	Stack.StepCompiledIn<FStructProperty>(nullptr);
	
	const FStructProperty* ValueProp = CastField<FStructProperty>(Stack.MostRecentProperty);
	void* ValuePtr = Stack.MostRecentPropertyAddress;

	P_FINISH;

    //Set the result as Not Valid for starters
	FindResult = EMyFindInstanceStructResult::NotValid;
	P_NATIVE_BEGIN;
	for (auto& Item : P_THIS->MyData) //Loop through our array on structs
	{
        //If its valid and its the correct type
		if (Item.IsValid() && Item.GetScriptStruct() == ItemDataType)
		{
            //Copy the memory (data) to the Value out param
			ValueProp->Struct->CopyScriptStruct(ValuePtr, Item.GetMemory());
            // Set result as valid
			FindResult = EMyFindInstanceStructResult::Valid;
            //No more need to loop. break out.
			break;
		}
	}
	P_NATIVE_END;
}

The K2Node

You first need to make an UncookedOnly module in your plugin or project. You can refer to https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-modules?application_version=4.27 if you need info on modules.

Create 2 classes in your uncooked modules, one called
MyDataGraphPin and another called: K2Node_GetMyData.

MyDataGraphPin will define some filters for our K2Node pin and the K2Node will contain a very small amount of code to glue it all together.

MyDataGraphPin

#include "Framework/SlateDelegates.h"
#include "Input/Reply.h"
#include "Internationalization/Text.h"
#include "KismetPins/SGraphPinObject.h"
#include "Templates/SharedPointer.h"
#include "Widgets/DeclarativeSyntaxSupport.h"

class SWidget;
class UEdGraphPin;
class UScriptStruct;

/////////////////////////////////////////////////////
// SGraphPinStruct

class SMyDataGraphPin : public SGraphPinObject
{
public:
	SLATE_BEGIN_ARGS(SMyDataGraphPin ) {}
	SLATE_END_ARGS()

	void Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj);

protected:
	// Called when a new struct was picked via the asset picker
	void OnPickedNewStruct(const UScriptStruct* ChosenStruct);

	//~ Begin SGraphPinObject Interface
	virtual FReply OnClickUse() override;
	virtual bool AllowSelfPinWidget() const override { return false; }
	virtual TSharedRef<SWidget> GenerateAssetPicker() override;
	virtual FText GetDefaultComboText() const override;
	virtual FOnClicked GetOnUseButtonDelegate() override;
	//~ End SGraphPinObject Interface
};

CPP File

#include "Containers/UnrealString.h"
#include "Delegates/Delegate.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraph/EdGraphSchema.h"
#include "Editor.h"
#include "InstancedStructDetails.h"
#include "Editor/EditorEngine.h"
#include "Engine/UserDefinedStruct.h"
#include "Internationalization/Internationalization.h"
#include "Layout/Margin.h"
#include "Misc/Attribute.h"
#include "Modules/ModuleManager.h"
#include "SGraphPin.h"
#include "ScopedTransaction.h"
#include "Selection.h"
#include "SlotBase.h"
#include "StructViewerFilter.h"
#include "StructViewerModule.h"
#include "Styling/AppStyle.h"
#include "Types/SlateStructs.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "Widgets/Input/SMenuAnchor.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"

class SWidget;
class UObject;

#define LOCTEXT_NAMESPACE "SMyDataGraphPin "

/////////////////////////////////////////////////////
// SMyDataGraphPin 

void SMyDataGraphPin::Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj)
{
	SGraphPin::Construct(SGraphPin::FArguments(), InGraphPinObj);
}

FReply SMyDataGraphPin::OnClickUse()
{
	FEditorDelegates::LoadSelectedAssetsIfNeeded.Broadcast();

	UObject* SelectedObject = GEditor->GetSelectedObjects()->GetTop(UScriptStruct::StaticClass());
	if (SelectedObject)
	{
		const FScopedTransaction Transaction(NSLOCTEXT("GraphEditor", "ChangeStructPinValue", "Change Struct Pin Value"));
		GraphPinObj->Modify();

		GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, SelectedObject);
	}

	return FReply::Handled();
}

TSharedRef<SWidget> SMyDataGraphPin::GenerateAssetPicker()
{
	FStructViewerModule& StructViewerModule = FModuleManager::LoadModuleChecked<FStructViewerModule>("StructViewer");

	// Fill in options
	FStructViewerInitializationOptions Options;
	Options.Mode = EStructViewerMode::StructPicker;
	Options.bShowNoneOption = true;

	// Set your instanced struct here!
	const UScriptStruct* MetaStruct = FMyBaseInstancedStruct::StaticStruct();

    //We use FInstancedStructFilter cause its convienient
	TSharedRef<FInstancedStructFilter> StructFilter = MakeShared<FInstancedStructFilter>();
	Options.StructFilter = StructFilter;
	StructFilter->BaseStruct = MetaStruct;
	StructFilter->bAllowBaseStruct = false;

	return
		SNew(SBox)
		.WidthOverride(280)
		[
			SNew(SVerticalBox)

			+ SVerticalBox::Slot()
			.FillHeight(1.0f)
			.MaxHeight(500)
			[ 
				SNew(SBorder)
				.Padding(4)
				.BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") )
				[
					StructViewerModule.CreateStructViewer(Options, FOnStructPicked::CreateSP(this, &SMyDataGraphPin::OnPickedNewStruct))
				]
			]			
		];
}

FOnClicked SMyDataGraphPin::GetOnUseButtonDelegate()
{
	return FOnClicked::CreateSP(this, &SMyDataGraphPin::OnClickUse);
}

void SMyDataGraphPin::OnPickedNewStruct(const UScriptStruct* ChosenStruct)
{
	if(GraphPinObj->IsPendingKill())
	{
		return;
	}

	FString NewPath;
	if (ChosenStruct)
	{
		NewPath = ChosenStruct->GetPathName();
	}

	if (GraphPinObj->GetDefaultAsString() != NewPath)
	{
		const FScopedTransaction Transaction( NSLOCTEXT("GraphEditor", "ChangeStructPinValue", "Change Struct Pin Value" ) );
		GraphPinObj->Modify();

		AssetPickerAnchor->SetIsOpen(false);
		GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, const_cast<UScriptStruct*>(ChosenStruct));
	}
}

FText SMyDataGraphPin::GetDefaultComboText() const
{ 
	return LOCTEXT("DefaultComboText", "Select Struct");
}

#undef LOCTEXT_NAMESPACE

I won’t go over every detail in the above, but inside the function GenerateAssetPicker you will see the struct defined here.

K2Node_GetMyData

struct FMyInstancedStructPinFactory : public FGraphPanelPinFactory
{
public:
	virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const override;
};

/**
 * 
 */
UCLASS()
class UK2Node_GetMyData : public UK2Node_CallFunction
{
	GENERATED_BODY()
public:
	virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
	virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override;
	virtual void PostReconstructNode() override;
	virtual void ClearCachedBlueprintData(UBlueprint* Blueprint) override;
	void RefreshOutputStructType();
};

The CPP file

TSharedPtr<class SGraphPin> FMyInstancedStructPinFactory::CreatePin(class UEdGraphPin* Pin) const
{
	 if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object)
	{
        //Only if the node is our special custom K2Node.
		if (UK2Node_GetMyData* Node = Cast<UK2Node_GetMyData>(Pin->GetOwningNode()))
		{
            //Only use this custom graph slate if its the pin we want to use it on
            //this must be the same name as your UScriptStruct* pointer name in your
            //custom thunk!
			if (Pin->PinName == "InstancedStructType")
			{
               //Return our custom GraphPin we made earlier.
				return SNew(SMyDataGraphPin, Pin);
			}
		}
	}
    //Let other filters try we dont want it.
	return nullptr;
}


void UK2Node_GetMyData::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
	Super::GetMenuActions(ActionRegistrar);
	UClass* Action = GetClass();
	if (ActionRegistrar.IsOpenForRegistration(Action))
	{
		auto CustomizeLambda = [](UEdGraphNode* NewNode, bool bIsTemplateNode, const FName FunctionName)
		{
			UK2Node_GetMyData* Node = CastChecked<UK2Node_GetMyData>(NewNode);
			UFunction* Function = UMyData::StaticClass()->FindFunctionByName(FunctionName);
			check(Function);
			Node->SetFromFunction(Function);
		};
		
		// Our custom thunk
		UBlueprintNodeSpawner* GetNodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
		check(GetNodeSpawner != nullptr);
		GetNodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic(CustomizeLambda, GET_FUNCTION_NAME_CHECKED(UMyData, GetMyDataBP));
		ActionRegistrar.AddBlueprintAction(Action, GetNodeSpawner);
	}
}

void UK2Node_GetMyData::PinDefaultValueChanged(UEdGraphPin* ChangedPin)
{
	Super::PinDefaultValueChanged(ChangedPin);
	//Refresh our wildcard pin if the default value is changed
	if (ChangedPin->PinName == "InstancedStructType")
	{
		if (ChangedPin->LinkedTo.Num() == 0)
		{
			RefreshOutputStructType();
		}
	}
}

void UK2Node_GetMyData::PostReconstructNode()
{
	Super::PostReconstructNode();
    //Refresh our wildcard pin if the node has been rebuilt
	RefreshOutputStructType();
}

void UK2Node_GetMyData::ClearCachedBlueprintData(UBlueprint* Blueprint)
{
	Super::ClearCachedBlueprintData(Blueprint);
    //Refresh our wildcard pin if the cached data has been cleared
	RefreshOutputStructType();
}

void UK2Node_GetMyData::RefreshOutputStructType()
{
	auto GetPinForMe = [this] (FName PinName) { 
		UEdGraphPin* Pin = FindPinChecked(PinName);
		return Pin;
	};

    //Grab the value pin (the return pin with our data
	UEdGraphPin* ValuePin = GetPinForMe("Value");

    //Our type struct pin
	UEdGraphPin* StructTypePin =  GetPinForMe("InstancedStructType");

	if (StructTypePin->DefaultObject != ValuePin->PinType.PinSubCategoryObject)
	{
		if (ValuePin->SubPins.Num() > 0)
		{    //If the pin has been broken (split), recombine it.
             GetSchema()->RecombinePin(ValuePin);
		}
       //Set the value of our value pin to your selected InstancedStructType
		ValuePin->PinType.PinSubCategoryObject = StructTypePin->DefaultObject;
		ValuePin->PinType.PinCategory = (StructTypePin->DefaultObject == nullptr) ? UEdGraphSchema_K2::PC_Wildcard : UEdGraphSchema_K2::PC_Struct;
	}
}

One last step is to register our custom pin factory, in your uncooked only modules StartupModule and ShutdownModule

void FMyUncookedOnlyModule::StartupModule()
{
	MyInstancedStructPinFactory = MakeShared<FMyInstancedStructPinFactory>();
	FEdGraphUtilities::RegisterVisualPinFactory(MyInstancedStructPinFactory );
}

void FMyUncookedOnlyModule::ShutdownModule()
{
	FEdGraphUtilities::UnregisterVisualPinFactory(MyInstancedStructPinFactory);
}

//Place the following in the header:
TSharedPtr<FMyInstancedStructPinFactory> MyInstancedStructPinFactory;

Well that was a lot but, that should be everything to make it work.

I will provide some screenshots in a bit of it in action 🙂