# How to Create Port Arrays Using IPortArrayNode for Dynamic Connections in Ceres

> Learn to create port arrays in Ceres using IPortArrayNode for dynamic connections. Expose array fields and manage length for flexible node graph editing.

- Repository: [AkiKurisu/ceres](https://github.com/akikurisu/ceres)
- Tags: how-to-guide
- Published: 2026-02-24

---

**Implement the `IPortArrayNode` interface on your node class, expose an array field decorated with `[InputPort]` or `[OutputPort]`, and store the array length in a hidden integer field to enable dynamic port addition and removal in the Ceres graph editor.**

Ceres is an open-source visual scripting framework for Unity that supports dynamic port configurations through the `IPortArrayNode` interface. When you need to create port arrays using `IPortArrayNode` for dynamic connections, you enable designers to add, remove, and reorder ports at edit-time without modifying code. This article explains the architecture and provides complete implementation examples from the `akikurisu/ceres` repository.

## Understanding the IPortArrayNode Architecture

The dynamic port system relies on a clear separation between the runtime node logic and the editor visualization. When a node implements `IPortArrayNode`, the Ceres editor automatically switches to the `ExecutablePortArrayNodeView` resolver, which handles the variable port count.

### Core Interface Components

The `IPortArrayNode` interface, defined in [`Runtime/Core/Models/Graph/Nodes/PortArrayNodeReflection.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/Graph/Nodes/PortArrayNodeReflection.cs), requires three members:

- **`GetPortArrayLength()`**: Returns the current number of ports to display.
- **`GetPortArrayFieldName()`**: Returns the string name of the field that holds the port array (e.g., `"outputs"`).
- **`SetPortArrayLength(int length)`**: Called during compilation to allocate the runtime array.

A read-only variant, `IReadOnlyPortArrayNode`, exists for nodes that only need to query port counts without mutation.

### Editor Integration and Visualization

The `ExecutablePortArrayNodeView` class in [`Editor/Flow/UIElements/Nodes/Core/ExecutablePortArrayNodeView.cs`](https://github.com/akikurisu/ceres/blob/main/Editor/Flow/UIElements/Nodes/Core/ExecutablePortArrayNodeView.cs) manages the visual representation. When initializing, it queries `GetPortArrayLength` and creates the corresponding number of port widgets. The view automatically adds context-menu items for **Add new port**, **Remove last port**, and **Remove unconnected ports**, invoking the interface methods to keep the node state synchronized.

During graph compilation, the view's `CompileNode` method calls `SetPortArrayLength` before resolving field values, ensuring the runtime node allocates the correct sized array.

## Step-by-Step Implementation Guide

Follow these steps to create port arrays using `IPortArrayNode` for dynamic connections in your own Ceres nodes.

### 1. Define the Port Array Field

Declare a public array field using `NodePort[]` for flow connections or `CeresPort<T>[]` for data. Apply the `[InputPort]` or `[OutputPort]` attribute, plus `[CeresLabel]` for display names and `[CeresMetadata]` to specify the default length.

```csharp
[OutputPort(false), CeresLabel("Then"), CeresMetadata("DefaultLength = 2")]
public NodePort[] outputs;

```

### 2. Store the Array Length

Add a hidden integer field to persist the current port count. The `[HideInGraphEditor]` attribute keeps this value invisible in the node UI while allowing serialization.

```csharp
[HideInGraphEditor]
public int outputCount;

```

### 3. Implement the IPortArrayNode Interface

Implement the three required members to bridge the editor and runtime. Return the hidden length field in `GetPortArrayLength`, provide the field name in `GetPortArrayFieldName`, and update the length in `SetPortArrayLength`.

```csharp
public int GetPortArrayLength()            => outputCount;
public string GetPortArrayFieldName()      => nameof(outputs);
public void SetPortArrayLength(int length) => outputCount = length;

```

### 4. Handle Runtime Execution

In your `Execute` method, iterate over the array to process connections. Ensure you handle null checks since unconnected ports may contain null references.

```csharp
protected sealed override async UniTask Execute(ExecutionContext ctx)
{
    foreach (var outPort in outputs)
    {
        var next = outPort.GetT<ExecutableNode>();
        if (next != null) await ctx.Forward(next);
    }
}

```

## Complete Code Examples from the Ceres Repository

These concrete implementations from `akikurisu/ceres` demonstrate the pattern in production code.

### FlowNode_Sequence: Output Port Array

Located in [`Runtime/Flow/Models/Nodes/Utilities/FlowNode_Sequence.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/Utilities/FlowNode_Sequence.cs), this node executes multiple output branches sequentially using a dynamic `outputs` array.

```csharp
using System;
using Cysharp.Threading.Tasks;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using Ceres.Graph.Flow.Utilities;
using UnityEngine;

namespace Ceres.Graph.Flow.Utilities
{
    [Serializable]
    [CeresGroup("Utilities")]
    [CeresLabel("Sequence")]
    public class FlowNode_Sequence : ForwardNode,
                                     ISerializationCallbackReceiver,
                                     IPortArrayNode
    {
        [OutputPort(false), CeresLabel("Then"), CeresMetadata("DefaultLength = 2")]
        public NodePort[] outputs;

        [HideInGraphEditor] public int outputCount;

        protected sealed override async UniTask Execute(ExecutionContext ctx)
        {
            foreach (var outPort in outputs)
            {
                var next = outPort.GetT<ExecutableNode>();
                if (next != null) await ctx.Forward(next);
            }
        }

        public int GetPortArrayLength()            => outputCount;
        public string GetPortArrayFieldName()      => nameof(outputs);
        public void SetPortArrayLength(int length) => outputCount = length;
        
        public void OnBeforeSerialize() { }
        public void OnAfterDeserialize() 
        { 
            if (outputs == null || outputs.Length != outputCount)
                outputs = new NodePort[outputCount];
        }
    }
}

```

### FlowNode_MakeArrayT: Generic Input Port Array

Found in [`Runtime/Flow/Models/Nodes/Utilities/Array/FlowNode_MakeArrayT.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/Utilities/Array/FlowNode_MakeArrayT.cs), this generic node aggregates multiple inputs into an array, demonstrating input port arrays with type safety.

```csharp
using System;
using Cysharp.Threading.Tasks;
using Ceres.Annotations;
using Ceres.Graph.Flow;
using UnityEngine;

namespace Ceres.Graph.Flow.Utilities
{
    [Serializable]
    [CeresGroup("Utilities/Array")]
    [CeresLabel("Make {0} Array")]
    public class FlowNode_MakeArrayT<T> : FlowNode_MakeArray,
                                          ISerializationCallbackReceiver,
                                          IPortArrayNode
    {
        [InputPort, CeresMetadata("DefaultLength = 1")]
        public CeresPort<T>[] items;

        [HideInGraphEditor] public int inputCount;

        [OutputPort] public CeresPort<T[]> array;

        protected sealed override UniTask Execute(ExecutionContext ctx)
        {
            array.Value = new T[inputCount];
            for (int i = 0; i < inputCount; i++)
                array.Value[i] = items[i].Value;
            return UniTask.CompletedTask;
        }

        public int GetPortArrayLength()            => inputCount;
        public string GetPortArrayFieldName()      => nameof(items);
        public void SetPortArrayLength(int length)
        {
            inputCount = length;
            items = new CeresPort<T>[inputCount];
            for (int i = 0; i < length; i++) items[i] = new CeresPort<T>();
        }
        
        public void OnBeforeSerialize() { }
        public void OnAfterDeserialize() { }
    }
}

```

## Editor-Side Compilation Process

While you do not need to write editor code, understanding the compilation process helps debug array allocation issues. The `ExecutablePortArrayNodeView` class handles the transition from editor visualization to runtime instance.

When the graph compiles, the view performs these operations:

1. Instantiates the runtime node using `Activator.CreateInstance`
2. Checks if the instance implements `IPortArrayNode`
3. Calls `SetPortArrayLength` with the current editor port count
4. Proceeds with field resolution and port value committing

This sequence ensures the backing array exists before the runtime attempts to access it.

```csharp
// From Editor/Flow/UIElements/Nodes/Core/ExecutablePortArrayNodeView.cs
public override ExecutableNode CompileNode()
{
    var nodeInstance = (ExecutableNode)Activator.CreateInstance(NodeType);
    if (nodeInstance is IPortArrayNode portArrayNode)
        portArrayNode.SetPortArrayLength(PortLength);
    // …field resolution & port committing…
    return nodeInstance;
}

```

## Key Source Files

| File | Purpose | Location |
|---|---|---|
| [`PortArrayNodeReflection.cs`](https://github.com/akikurisu/ceres/blob/main/PortArrayNodeReflection.cs) | Defines `IPortArrayNode` and `IReadOnlyPortArrayNode` interfaces | [`Runtime/Core/Models/Graph/Nodes/PortArrayNodeReflection.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Core/Models/Graph/Nodes/PortArrayNodeReflection.cs) |
| [`ExecutablePortArrayNodeView.cs`](https://github.com/akikurisu/ceres/blob/main/ExecutablePortArrayNodeView.cs) | Editor resolver that builds the dynamic port UI | [`Editor/Flow/UIElements/Nodes/Core/ExecutablePortArrayNodeView.cs`](https://github.com/akikurisu/ceres/blob/main/Editor/Flow/UIElements/Nodes/Core/ExecutablePortArrayNodeView.cs) |
| [`FlowNode_Sequence.cs`](https://github.com/akikurisu/ceres/blob/main/FlowNode_Sequence.cs) | Concrete example of output port arrays | [`Runtime/Flow/Models/Nodes/Utilities/FlowNode_Sequence.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/Utilities/FlowNode_Sequence.cs) |
| [`FlowNode_MakeArrayT.cs`](https://github.com/akikurisu/ceres/blob/main/FlowNode_MakeArrayT.cs) | Generic example of input port arrays | [`Runtime/Flow/Models/Nodes/Utilities/Array/FlowNode_MakeArrayT.cs`](https://github.com/akikurisu/ceres/blob/main/Runtime/Flow/Models/Nodes/Utilities/Array/FlowNode_MakeArrayT.cs) |

## Summary

- **Implement `IPortArrayNode`** to enable dynamic port arrays in Ceres nodes.
- **Store array length** in a hidden field marked with `[HideInGraphEditor]` to persist the port count.
- **Return the field name** via `GetPortArrayFieldName()` so the editor knows which array to manipulate.
- **Allocate arrays** in `SetPortArrayLength()` during compilation to ensure runtime availability.
- **Use `NodePort[]`** for flow connections and `CeresPort<T>[]` for typed data inputs or outputs.

## Frequently Asked Questions

### What is the difference between IPortArrayNode and IReadOnlyPortArrayNode?

`IPortArrayNode` provides full read-write access to the port array length, allowing the editor to add and remove ports via `SetPortArrayLength`. `IReadOnlyPortArrayNode` only declares `GetPortArrayLength` and `GetPortArrayFieldName`, suitable for nodes that need to report dynamic port counts without allowing external mutation.

### How does the editor know which field represents the port array?

The editor calls `GetPortArrayFieldName()` on your node instance, which must return the exact string name of the array field (e.g., `"outputs"` or `"items"`). The `ExecutablePortArrayNodeView` uses this name to locate the field via reflection and to generate the correct number of port widgets in the UI.

### Can I use IPortArrayNode with generic type parameters?

Yes, as demonstrated by `FlowNode_MakeArrayT<T>` in [`FlowNode_MakeArrayT.cs`](https://github.com/akikurisu/ceres/blob/main/FlowNode_MakeArrayT.cs). The interface implementation works identically for generic classes. You declare the array using `CeresPort<T>[]` and implement the three interface members normally, with the generic type parameter propagating to the port types automatically.

### When is SetPortArrayLength called during the node lifecycle?

`SetPortArrayLength` is called during graph compilation in the editor, specifically within `ExecutablePortArrayNodeView.CompileNode()` before field values are committed. It is not called at runtime during execution; by the time `Execute` runs, the array should already be allocated and populated. This method essentially transfers the editor's port count to the runtime instance.