How to Create Custom Executable Nodes with ExecutableFunctionAttribute in Ceres Flow
[ExecutableFunctionAttribute] turns ordinary C# methods into visual scripting nodes by marking them with metadata that the Ceres Flow editor automatically discovers and exposes as searchable graph nodes.
The akikurisu/ceres repository provides a powerful visual scripting framework that eliminates boilerplate when extending node libraries. By applying ExecutableFunctionAttribute to your methods, you enable the ExecutableFunctionRegistry to index them at editor startup and generate corresponding Flow nodes without writing custom node classes.
Understanding the ExecutableFunctionAttribute Architecture
Ceres Flow uses a multi-layered reflection system to transform attributed methods into graph nodes. The architecture separates metadata definition from runtime discovery.
Core Components
The system relies on three primary classes defined in the Runtime and Editor assemblies:
-
ExecutableFunctionAttribute– Defined inRuntime/Flow/Annotations/ExecutableFunctionAttribute.cs, this attribute stores metadata flags likeIsScriptMethodandExecuteInDependencythat control node behavior and appearance. -
ExecutableReflection– Located inRuntime/Flow/Models/ExecutableReflection.cs, this wrapper extracts method signatures, parameter lists, and return types to build the node UI description and port mappings. -
ExecutableFunctionRegistry– Found inEditor/Flow/ExecutableFunctionRegistry.cs, this editor-only singleton scans loaded assemblies at startup, caching discovered methods in_staticFunctions,_instanceFunctionTables, and_retargetFunctionTablescollections.
How the Registry Discovers Methods
When the Unity editor loads, ExecutableFunctionRegistry.Get() enumerates runtime-referenced assemblies plus those defined in Project Settings → Ceres → Flow Settings. For types deriving from ExecutableFunctionLibrary, the registry gathers static methods via source generation (see Runtime/CodeGen/ExecutableReflectionILPP.cs). For all other types, it calls ExecutableReflection.GetInstanceExecutableFunctions(type) using methodInfo.GetCustomAttribute<ExecutableFunctionAttribute>() to identify candidates.
Creating Instance Method Nodes
The simplest approach attaches executable functions directly to MonoBehaviour or ScriptableObject components. No additional boilerplate or base classes are required.
using Ceres.Graph.Flow.Annotations;
using UnityEngine;
public class MyComponent : MonoBehaviour
{
[ExecutableFunction]
public void DoSomething(int arg1, float arg2)
{
Debug.Log($"Arg1={arg1}, Arg2={arg2}");
}
}
After compilation, the Flow graph editor displays a node named Do Something with two input ports corresponding to the method parameters. The node appears in the search window under the component's namespace, ready for connection to other graph logic.
Creating Static Method Nodes with ExecutableFunctionLibrary
For utility functions or engine wrappers, static methods provide better performance through source generation. You must declare them inside a partial class inheriting from ExecutableFunctionLibrary.
using Ceres.Graph.Flow.Annotations;
using Ceres.Graph.Flow;
using UnityEngine;
public partial class UnityExecutableFunctionLibrary : ExecutableFunctionLibrary
{
[ExecutableFunction(IsScriptMethod = true, IsSelfTarget = true), CeresLabel("Get Name")]
public static string Flow_UObjectGetName(UObject target)
{
return target.name;
}
[ExecutableFunction]
public static UObject Flow_FindObjectOfType(
[ResolveReturn] SerializedType<UObject> type)
{
return UObject.FindObjectOfType(type);
}
}
Key attribute properties:
IsScriptMethod = true– Designates the first parameter as the script target, organizing the node under that type's context menu.IsSelfTarget = true– Automatically supplies the graph owner as the first argument when used within a UObject's Flow graph.CeresLabel– Overrides the auto-generated node name (which defaults to the method name).
The source generator in ExecutableReflectionILPP.cs replaces runtime reflection with direct delegate calls for these static methods, eliminating performance overhead during graph execution.
Invoking Executable Functions from Custom Nodes
When you need additional logic beyond a direct method call, create a custom node class that retrieves the MethodInfo from the registry and invokes it manually.
using System.Reflection;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using Ceres.Editor.Graph.Flow;
using System.Linq;
[Serializable]
[CeresGroup("Utilities")]
[CeresLabel("Call GetName")]
public class FlowNode_CallGetName : FlowNode
{
[InputPort, CeresLabel("Target")]
public CeresPort<UObject> target = new CeresPort<UObject>();
[OutputPort, CeresLabel("Name")]
public CeresPort<string> result = new CeresPort<string>();
protected override void LocalExecute(ExecutionContext ctx)
{
var method = ExecutableFunctionRegistry.Get()
.GetStaticFunctions()
.First(m => m.Name == nameof(UnityExecutableFunctionLibrary.Flow_UObjectGetName));
var name = (string)method.Invoke(null, new object[] { target.Value });
result.Value = name;
}
}
This pattern allows you to wrap executable functions with validation, logging, or error handling while maintaining the underlying method's signature and port structure.
Summary
-
ExecutableFunctionAttributemarks C# methods for automatic node generation in the Ceres Flow editor. -
Instance methods require only the attribute and appear automatically for components inheriting from
MonoBehaviourorScriptableObject. -
Static methods must reside in a
partialclass extendingExecutableFunctionLibraryto leverage source generation and avoid reflection costs. -
The
ExecutableFunctionRegistrycaches all discovered methods at editor startup inEditor/Flow/ExecutableFunctionRegistry.cs. -
Custom nodes can invoke registry methods via reflection or directly when extending
FlowNode,ForwardNode, orExecutableNodebase classes.
Frequently Asked Questions
What is the difference between instance and static executable functions?
Instance functions are methods defined on components like MonoBehaviours that the registry discovers via ExecutableReflection.GetInstanceExecutableFunctions(type) in Runtime/Flow/Models/ExecutableReflection.cs. Static functions must be placed in a class extending ExecutableFunctionLibrary and are processed by the source generator in Runtime/CodeGen/ExecutableReflectionILPP.cs to eliminate runtime reflection overhead.
How does Ceres Flow handle method parameters and return values?
The ExecutableReflection class parses each method's MethodInfo to generate input ports for parameters and output ports for return values. In ExecutableFunctionRegistry.cs, the system maps primitive types, Unity objects, and special types like SerializedType<T> (marked with [ResolveReturn]) to corresponding Ceres port types automatically.
Can I rename the generated node without changing the method name?
Yes. Apply the [CeresLabel("Desired Name")] attribute alongside [ExecutableFunction]. According to the implementation in Runtime/Flow/Annotations/ExecutableFunctionAttribute.cs, this overrides the default naming convention that uses the C# method name.
Why must static method libraries be declared as partial classes?
The partial keyword allows the source generator to inject registration code into your class at compile time. This generated code, handled by ExecutableReflectionILPP.cs, creates direct delegates for static methods, replacing the reflection-based invocation path with high-performance direct calls that the ExecutableFunctionRegistry caches in _staticFunctions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →