Skip to content

Ports and Messages

Ports define the interface of a component - what data it accepts and produces. Understanding ports is crucial for building interoperable components.

Port Types

                        ┌─────────────────────────────────────┐
                        │            COMPONENT                 │
                        │                                      │
     INPUT PORTS        │                                      │     OUTPUT PORTS
     (Source: false)    │                                      │     (Source: true)
                        │                                      │
  ┌─────────┐           │                                      │           ┌─────────┐
  │ settings│──────────▶│                                      │           │         │
  └─────────┘           │                                      │           └─────────┘
                        │         ┌──────────────┐             │
  ┌─────────┐           │         │              │             │           ┌─────────┐
  │  input  │──────────▶│         │   Handle()   │─────────────│──────────▶│ output  │
  └─────────┘           │         │              │             │           └─────────┘
                        │         └──────────────┘             │
  ┌─────────┐           │                                      │           ┌─────────┐
  │ control │◀─────────▶│                                      │◀─────────▶│ control │
  └─────────┘           │                                      │           └─────────┘
                        │                                      │
                        └─────────────────────────────────────┘

Input Ports

Receive data from other nodes or the system:

go
{
    Name:          "input",
    Label:         "Input",
    Position:      module.Left,
    Source:        false,           // Input port
    Configuration: InputMessage{},  // Schema definition
}

Output Ports

Send data to connected nodes:

go
{
    Name:          "output",
    Label:         "Output",
    Position:      module.Right,
    Source:        true,            // Output port
    Configuration: OutputMessage{}, // Schema definition
}

Bidirectional Ports

Control ports can be both input and output (for UI interaction):

go
{
    Name:          v1alpha1.ControlPort,  // "_control"
    Label:         "Control",
    Source:        true,                  // Marked as source for UI updates
    Configuration: Control{},
}

Port Definition

Port Struct

go
type Port struct {
    Name          string      // Unique identifier within component
    Label         string      // Display name in UI
    Position      Position    // Visual position (Left, Right, Top, Bottom)
    Source        bool        // true = output port, false = input port
    Configuration interface{} // Struct defining the message schema
}

Position Constants

go
const (
    Top    Position = iota  // 0 - Special ports (rarely used)
    Right  Position = 1     // Output ports
    Bottom Position = 2     // Fallback/default ports
    Left   Position = 3     // Input ports
)

Visual Layout:

              ┌───────────┐
         Top  │           │
              │           │
    Left ────▶│ Component │────▶ Right
              │           │
              │           │
              └───────────┘
                 Bottom

Message Types

Messages are strongly typed using Go structs:

Input Message

go
type RequestInput struct {
    Method  string            `json:"method" required:"true"`
    URL     string            `json:"url" required:"true"`
    Headers map[string]string `json:"headers"`
    Body    any               `json:"body" configurable:"true"`
}

Output Message

go
type ResponseOutput struct {
    StatusCode int               `json:"statusCode"`
    Headers    map[string]string `json:"headers"`
    Body       any               `json:"body" shared:"true"`
}

Key Struct Tags

TagPurposeExample
json:"name"JSON field namejson:"userId"
required:"true"Field must be providedValidation
configurable:"true"User can set via expressionsEdge configuration
shared:"true"Available for reference by other componentsType-safe references
readonly:"true"Display only, no editingStatus display

System Ports

The SDK defines special system ports:

Settings Port (_settings)

Receives initial configuration:

go
const SettingsPort = "_settings"

// In Ports():
{
    Name:          v1alpha1.SettingsPort,
    Label:         "Settings",
    Configuration: Settings{},
}

// In Handle():
case v1alpha1.SettingsPort:
    c.settings = msg.(Settings)
    return nil

Control Port (_control)

UI interaction (buttons, status):

go
const ControlPort = "_control"

// Control struct with buttons
type Control struct {
    Status string `json:"status" readonly:"true"`
    Start  bool   `json:"start" format:"button"`
    Stop   bool   `json:"stop" format:"button"`
}

Reconcile Port (_reconcile)

Triggered periodically or for state updates:

go
const ReconcilePort = "_reconcile"

// Receive TinyNode for state reading
case v1alpha1.ReconcilePort:
    if node, ok := msg.(v1alpha1.TinyNode); ok {
        // Read shared metadata
        value := node.Status.Metadata["key"]
    }

// Send state update (callback form)
output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) {
    node.Status.Metadata["key"] = "value"
})

Client Port (_client)

Access to Kubernetes resources:

go
const ClientPort = "_client"

// Used internally by resource manager

Message Flow Through Ports

Node A                           Node B
┌─────────────────────┐          ┌─────────────────────┐
│                     │          │                     │
│ Handle() processes  │          │                     │
│ input message       │          │                     │
│         │           │          │                     │
│         ▼           │          │                     │
│ output(ctx,         │          │                     │
│   "out",           ─┼──────────┼▶ Handle() receives  │
│   data)             │   Edge   │    "input" port     │
│                     │          │                     │
└─────────────────────┘          └─────────────────────┘


                    Expression evaluation
                    transforms data

Multiple Output Ports

Components can have multiple output ports:

go
func (c *Router) Ports() []module.Port {
    ports := []module.Port{
        {Name: "input", Position: module.Left, Configuration: Input{}},
    }

    // Dynamic output ports based on settings
    for _, route := range c.settings.Routes {
        ports = append(ports, module.Port{
            Name:          fmt.Sprintf("out_%s", route),
            Label:         route,
            Position:      module.Right,
            Source:        true,
            Configuration: Output{},
        })
    }

    // Default port
    if c.settings.EnableDefault {
        ports = append(ports, module.Port{
            Name:          "default",
            Position:      module.Bottom,
            Source:        true,
            Configuration: Output{},
        })
    }

    return ports
}

Port Configuration Schema

The Configuration field defines the JSON Schema for the port:

go
type Input struct {
    // Basic types
    Name    string  `json:"name" required:"true"`
    Count   int     `json:"count" minimum:"0" maximum:"100"`
    Enabled bool    `json:"enabled" default:"true"`

    // Complex types
    Items   []Item  `json:"items" minItems:"1"`
    Options Options `json:"options"`

    // Configurable fields (can use expressions)
    Data    any     `json:"data" configurable:"true"`
}

The SDK automatically generates JSON Schema from these structs. See Schema from Go.

Handling Messages

Type Assertion

Always assert message types in Handle():

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
    switch port {
    case "input":
        input, ok := msg.(Input)
        if !ok {
            return fmt.Errorf("expected Input, got %T", msg)
        }
        // Process input...

    case v1alpha1.SettingsPort:
        settings, ok := msg.(Settings)
        if !ok {
            return fmt.Errorf("expected Settings, got %T", msg)
        }
        c.settings = settings
    }
    return nil
}

Sending Output

Use the output handler to send data:

go
// Simple output
output(ctx, "output", OutputMessage{
    Result: "success",
    Data:   processedData,
})

// Output with error handling
if err := output(ctx, "output", data); err != nil {
    return err  // Propagate error
}
return nil

Best Practices

1. Use Descriptive Port Names

go
// Good
{Name: "request", ...}
{Name: "response", ...}
{Name: "out_success", ...}
{Name: "out_error", ...}

// Bad
{Name: "in1", ...}
{Name: "out", ...}

2. Document with Struct Tags

go
type Input struct {
    UserID string `json:"userId" required:"true" title:"User ID" description:"The unique identifier of the user"`
}

3. Keep Schemas Focused

go
// Good: Focused schema
type RequestInput struct {
    URL    string `json:"url"`
    Method string `json:"method"`
}

// Bad: Kitchen sink
type Everything struct {
    URL, Method, Headers, Body, Auth, Retry, Timeout, Cache, Log...
}

4. Use Shared Types

Mark output fields as shared:"true" for type-safe references:

go
type Output struct {
    UserID   string `json:"userId" shared:"true"`   // Can be referenced
    UserName string `json:"userName" shared:"true"` // Can be referenced
}

Next Steps

Build flow-based applications on Kubernetes