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:
{
Name: "input",
Label: "Input",
Position: module.Left,
Source: false, // Input port
Configuration: InputMessage{}, // Schema definition
}Output Ports
Send data to connected nodes:
{
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):
{
Name: v1alpha1.ControlPort, // "_control"
Label: "Control",
Source: true, // Marked as source for UI updates
Configuration: Control{},
}Port Definition
Port Struct
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
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
│ │
│ │
└───────────┘
BottomMessage Types
Messages are strongly typed using Go structs:
Input Message
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
type ResponseOutput struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body any `json:"body" shared:"true"`
}Key Struct Tags
| Tag | Purpose | Example |
|---|---|---|
json:"name" | JSON field name | json:"userId" |
required:"true" | Field must be provided | Validation |
configurable:"true" | User can set via expressions | Edge configuration |
shared:"true" | Available for reference by other components | Type-safe references |
readonly:"true" | Display only, no editing | Status display |
System Ports
The SDK defines special system ports:
Settings Port (_settings)
Receives initial configuration:
const SettingsPort = "_settings"
// In Ports():
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Configuration: Settings{},
}
// In Handle():
case v1alpha1.SettingsPort:
c.settings = msg.(Settings)
return nilControl Port (_control)
UI interaction (buttons, status):
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:
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:
const ClientPort = "_client"
// Used internally by resource managerMessage Flow Through Ports
Node A Node B
┌─────────────────────┐ ┌─────────────────────┐
│ │ │ │
│ Handle() processes │ │ │
│ input message │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ output(ctx, │ │ │
│ "out", ─┼──────────┼▶ Handle() receives │
│ data) │ Edge │ "input" port │
│ │ │ │
└─────────────────────┘ └─────────────────────┘
│
▼
Expression evaluation
transforms dataMultiple Output Ports
Components can have multiple output ports:
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:
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():
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:
// 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 nilBest Practices
1. Use Descriptive Port Names
// Good
{Name: "request", ...}
{Name: "response", ...}
{Name: "out_success", ...}
{Name: "out_error", ...}
// Bad
{Name: "in1", ...}
{Name: "out", ...}2. Document with Struct Tags
type Input struct {
UserID string `json:"userId" required:"true" title:"User ID" description:"The unique identifier of the user"`
}3. Keep Schemas Focused
// 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:
type Output struct {
UserID string `json:"userId" shared:"true"` // Can be referenced
UserName string `json:"userName" shared:"true"` // Can be referenced
}Next Steps
- Message Flow - How messages travel through the system
- System Ports - Deep dive into system ports
- Struct Tags Reference - Complete tag documentation