Creating Custom Flow Nodes
Creating custom nodes in Flow allows you to encapsulate reusable logic and extend Flow's functionality. This guide will teach you how to create your own Flow nodes with practical examples.
Understanding Flow Node Base Classes
Flow provides three base classes for creating custom nodes, each serving different purposes:
ExecutableNode
The base class for all executable nodes. Use this when you need full control over execution flow.
- Must implement
Execute(ExecutionContext)method - Can handle both synchronous and asynchronous operations
- Full control over execution flow continuation
ForwardNode
Inherits from ExecutableNode and adds an input port for forward execution path.
- Automatically includes
inputport for connecting previous nodes - Use when your node is part of a sequential execution chain
- Most common base class for custom nodes
FlowNode
Inherits from ForwardNode and provides the simplest interface for synchronous nodes.
- Automatically handles execution flow continuation via
execoutput port - Override
LocalExecute(ExecutionContext)for your logic - Best for simple, synchronous operations
Basic Node Structure
Every Flow node must:
- Inherit from one of the base classes
- Be marked with
[Serializable] - Define input/output ports using attributes
- Implement execution logic
Port Attributes
[InputPort]- Marks a field as an input port[OutputPort(false)]- Marks a field as an execution output port (white port)[OutputPort]- Marks a field as a data output port[CeresLabel("Label")]- Custom label for the port[HideInGraphEditor]- Hides field from graph editor (for internal data)
Example 1: Simple Delay Node
Let's create a delay node that waits for a specified duration before continuing execution.
using System;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
[Serializable]
[CeresGroup("Utilities")]
[CeresLabel("Delay")]
public class FlowNode_Delay : ForwardNode
{
[InputPort, CeresLabel("Duration")]
public CeresPort<float> duration = new CeresPort<float>(1.0f);
[OutputPort(false), CeresLabel("")]
public NodePort exec;
protected override async UniTask Execute(ExecutionContext executionContext)
{
// Wait for the specified duration
await UniTask.Delay(TimeSpan.FromSeconds(duration.Value),
cancellationToken: executionContext.Context.GetCancellationTokenOnDestroy());
// Continue execution to next node
executionContext.SetNext(exec.GetT<ExecutableNode>());
}
}
Key Points:
- Inherits from
ForwardNodefor async support - Uses
UniTask.Delayfor async waiting - Uses
SetNext()to continue execution flow - Handles cancellation token for proper cleanup
Example 2: Conditional Branch Node
Let's create a branch node that routes execution based on a boolean condition.
using System;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
[Serializable]
[CeresGroup("Flow Control")]
[CeresLabel("Branch")]
public class FlowNode_Branch : FlowNode
{
[InputPort, CeresLabel("Condition")]
public CeresPort<bool> condition = new CeresPort<bool>();
[OutputPort(false), CeresLabel("True")]
public NodePort trueExec;
[OutputPort(false), CeresLabel("False")]
public NodePort falseExec;
protected override void LocalExecute(ExecutionContext executionContext)
{
// Get the next node based on condition
var nextNode = condition.Value
? trueExec.GetT<ExecutableNode>()
: falseExec.GetT<ExecutableNode>();
// FlowNode automatically continues execution via exec port
// But we need custom logic, so we override Execute instead
}
protected override UniTask Execute(ExecutionContext executionContext)
{
var nextNode = condition.Value
? trueExec.GetT<ExecutableNode>()
: falseExec.GetT<ExecutableNode>();
executionContext.SetNext(nextNode);
return UniTask.CompletedTask;
}
}
Note: Since we need custom execution flow (choosing between two outputs), we override Execute() instead of LocalExecute().
Example 3: Calculate Node
A simple synchronous node that performs calculations.
using System;
using Ceres.Annotations;
using Ceres.Graph.Flow;
[Serializable]
[CeresGroup("Math")]
[CeresLabel("Add")]
public class FlowNode_Add : FlowNode
{
[InputPort, CeresLabel("A")]
public CeresPort<float> a = new CeresPort<float>();
[InputPort, CeresLabel("B")]
public CeresPort<float> b = new CeresPort<float>();
[OutputPort, CeresLabel("Result")]
public CeresPort<float> result = new CeresPort<float>();
protected override void LocalExecute(ExecutionContext executionContext)
{
// Simple synchronous calculation
result.Value = a.Value + b.Value;
// FlowNode automatically continues execution via exec port
}
}
Key Points:
- Inherits from
FlowNodefor simple synchronous operations - Overrides
LocalExecute()for the calculation logic - Execution flow continues automatically through
execport
Example 4: Sequence Node with Port Array
A node that executes multiple outputs sequentially. This demonstrates using IPortArrayNode for dynamic port arrays.
using System;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
[Serializable]
[CeresGroup("Utilities")]
[CeresLabel("Sequence")]
[CeresMetadata("style = ForwardNode")]
public class FlowNode_Sequence : ForwardNode, ISerializationCallbackReceiver, IPortArrayNode
{
[OutputPort(false), CeresLabel("Then"), CeresMetadata("DefaultLength = 2")]
public NodePort[] outputs;
[HideInGraphEditor]
public int outputCount;
protected override async UniTask Execute(ExecutionContext executionContext)
{
// Execute each output sequentially
foreach (var output in outputs)
{
var next = output.GetT<ExecutableNode>();
if (next == null) continue;
await executionContext.Forward(next);
}
}
// ISerializationCallbackReceiver implementation
public void OnBeforeSerialize()
{
// Called before serialization
}
public void OnAfterDeserialize()
{
// Reconstruct port array after deserialization
outputs = new NodePort[outputCount];
for (int i = 0; i < outputCount; i++)
{
outputs[i] = new NodePort();
}
}
// IPortArrayNode implementation
public int GetPortArrayLength()
{
return outputCount;
}
public string GetPortArrayFieldName()
{
return nameof(outputs);
}
public void SetPortArrayLength(int newLength)
{
outputCount = newLength;
}
}
Key Points:
- Implements
IPortArrayNodefor dynamic port arrays - Uses
CeresMetadata("DefaultLength = 2")to set default array size - Implements
ISerializationCallbackReceiverto handle serialization - Uses
executionContext.Forward()to execute nodes sequentially
Node Metadata
Use attributes to customize node appearance and behavior:
[CeresGroup("Group Name")]- Groups nodes in the search window[CeresLabel("Display Name")]- Custom display name[CeresMetadata("key = value")]- Additional metadata[NodeInfo("Description")]- Tooltip description
Execution Context
The ExecutionContext provides important information and methods:
executionContext.Graph- Access to the FlowGraph instanceexecutionContext.Context- The Unity Object that owns this graphexecutionContext.SetNext(node)- Set the next node to executeexecutionContext.Forward(node)- Execute a node in forward pathexecutionContext.GetEvent<T>()- Get the event that triggered execution
Synchronous vs Asynchronous
Synchronous Nodes (FlowNode)
- Override
LocalExecute(ExecutionContext) - Execution completes immediately
- Automatically continues via
execport - Use for calculations, data transformations, simple logic
Asynchronous Nodes (ForwardNode/ExecutableNode)
- Override
Execute(ExecutionContext)returningUniTask - Can await async operations
- Must manually call
SetNext()orForward() - Use for delays, coroutines, async operations
Best Practices
Choose the right base class - Use
FlowNodewhen possible, only useForwardNode/ExecutableNodewhen neededHandle null ports - Always check if ports are connected:
var next = exec.GetT<ExecutableNode>(); if (next == null) return;Use cancellation tokens - For async operations, respect cancellation:
await UniTask.Delay(duration, cancellationToken: executionContext.Context.GetCancellationTokenOnDestroy());Provide default values - Initialize ports with sensible defaults:
public CeresPort<float> duration = new CeresPort<float>(1.0f);Use meaningful labels - Help users understand your node:
[CeresLabel("Calculate Distance")] [CeresGroup("Math")]Document your nodes - Add
[NodeInfo]for tooltips:[NodeInfo("Waits for the specified duration before continuing execution.")]
Common Patterns
Pattern 1: Data Transformation
protected override void LocalExecute(ExecutionContext executionContext)
{
output.Value = Transform(input.Value);
}
Pattern 2: Conditional Execution
protected override UniTask Execute(ExecutionContext executionContext)
{
var next = condition.Value ? trueExec : falseExec;
executionContext.SetNext(next.GetT<ExecutableNode>());
return UniTask.CompletedTask;
}
Pattern 3: Async Operation
protected override async UniTask Execute(ExecutionContext executionContext)
{
await SomeAsyncOperation();
executionContext.SetNext(exec.GetT<ExecutableNode>());
}
Next Steps
- Learn about Generic Nodes for type-safe generic nodes
- Explore ExecutableFunctionLibrary for exposing C# methods
- Check Advanced Features for port arrays and more
- See Graph Tracker for debugging custom nodes