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:

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

  • ExecutableFunctionAttribute marks C# methods for automatic node generation in the Ceres Flow editor.

  • Instance methods require only the attribute and appear automatically for components inheriting from MonoBehaviour or ScriptableObject.

  • Static methods must reside in a partial class extending ExecutableFunctionLibrary to leverage source generation and avoid reflection costs.

  • The ExecutableFunctionRegistry caches all discovered methods at editor startup in Editor/Flow/ExecutableFunctionRegistry.cs.

  • Custom nodes can invoke registry methods via reflection or directly when extending FlowNode, ForwardNode, or ExecutableNode base 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →