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)
| |
+---------+ | +--------------+ | +---------+
| input |---------->| | Handle() |-------------|---------->| output |
+---------+ | +--------------+ | +---------+
| |
+---------+ | +--------------+ |
|_settings|---------->| | OnSettings() | |
+---------+ | +--------------+ |
| |
+---------+ | +--------------+ |
|_control |<--------->| | OnControl() | |
+---------+ | +--------------+ |
+-------------------------------------+
Business ports (input, output, ...) are delivered to Handle(). System ports (names starting with _) are dispatched to typed capability interfaces — they never reach Handle().
Input Ports
Receive data from other nodes or the system:
{
Name: "input",
Label: "Input",
Position: module.Left,
Source: false, // Input port
Configuration: InputMessage{}, // Typed schema definition
}
Output Ports
Send data to connected nodes:
{
Name: "output",
Label: "Output",
Position: module.Right,
Source: true, // Output port (source of data)
Configuration: OutputMessage{}, // Typed 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{},
}
Incoming control messages are dispatched to ControlHandler.OnControl; the component pushes widget updates by emitting to v1alpha1.ControlPort.
Port Definition
Port Struct
type Port struct {
Source bool // true = output (source of data), false = input
Position Position // Visual position (module.Left, Right, Top, Bottom)
Name string // Unique lower-case identifier within component
Label string // Display name in UI
Configuration interface{} // Struct defining the message schema (request)
ResponseConfiguration interface{} // Struct defining the response schema (blocking calls)
Schema json.RawMessage // Optional: verbatim JSON Schema for runtime-authored forms
}
Typed ports set Configuration to a struct value (e.g. InputMessage{}) and leave Schema nil — the runtime reflects the JSON Schema from Configuration automatically. Schema exists for the rare case where no compile-time Go type exists at all (a component that receives a JSON Schema as data and renders it to a human, like ask); when non-nil it is published verbatim instead of the reflected schema.
Position Constants
const (
Top Position = iota // 0 - Special ports (rarely used)
Right // 1 - Output ports
Bottom // 2 - Fallback/default ports
Left // 3 - Input ports
)
Visual Layout:
+-----------+
Top | |
| |
Left ---->| Component |----> Right
| |
| |
+-----------+
Bottom
Message 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. They are never delivered to Handle() — the framework dispatches each one to a typed capability interface. A component opts in by implementing the interface; there is no fallback port switch.
Settings Port (_settings)
Receives user configuration. Declare the port with a typed Configuration; implement SettingsHandler to receive it:
const SettingsPort = "_settings"
// In Ports():
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Source: false,
Configuration: Settings{},
}
// Capability interface — replaces the old port switch in Handle():
func (c *MyComponent) OnSettings(ctx context.Context, settings any) error {
s, ok := settings.(Settings)
if !ok {
return fmt.Errorf("expected Settings, got %T", settings)
}
c.settings = s
return nil
}
OnSettings fires after OnReconcile on a fresh runner, so settings always win over reconcile-restored state. It re-fires only when the configured value changes.
Control Port (_control)
UI interaction (buttons, status). Implement ControlHandler:
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"`
}
func (c *MyComponent) OnControl(ctx context.Context, control any) error {
ctrl := control.(Control)
if ctrl.Start {
// ...
}
return nil
}
Reconcile Port (_reconcile)
Fires on every TinyNode reconcile. Implement ReconcileHandler to read node state; emit a patch callback to update it:
const ReconcilePort = "_reconcile"
// Receive the TinyNode on each reconcile
func (c *MyComponent) OnReconcile(ctx context.Context, node v1alpha1.TinyNode) error {
value := node.Status.Metadata["key"]
// restore state, react to spec changes
return nil
}
// Send state update (callback form) — the callback MUST return error
output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) error {
node.Status.Metadata["key"] = "value"
return nil
})
Identity Port (_identity)
Delivers the node's own identity (name, namespace, flow, project). Implement IdentityAware — or embed module.Base and call c.Identity():
const IdentityPort = "_identity"
func (c *MyComponent) OnIdentity(id v1alpha1.NodeIdentity) {
c.prefix = id.Namespace + "/" + id.NodeName
}
Client Port (_client)
Access to Kubernetes resources. Implement ClientAware — or embed module.Base and call c.Client():
const ClientPort = "_client"
func (c *MyComponent) OnClient(client module.K8sClient) {
c.client = client
}
Embedding module.Base satisfies IdentityAware, ClientAware, Stateful, and EmitterAware in one line — most components never implement those four by hand.
Message Flow Through Ports
Node A Node B
+---------------------+ +---------------------+
| | | |
| Handle() processes | | |
| input message | | |
| | | | |
| v | | |
| output(ctx, | | |
| "out", -+----------+> Handle() receives |
| data) | Edge | "input" port |
| | | |
+---------------------+ +---------------------+
|
v
Expression evaluation
transforms data
Multiple Output Ports
Components can have multiple output ports:
func (c *Router) Ports() []module.Port {
ports := []module.Port{
{Name: "input", Position: module.Left, Source: false, 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 — leave Port.Schema nil for typed ports. 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) module.Result {
switch port {
case "input":
input, ok := msg.(Input)
if !ok {
return module.Fail(fmt.Errorf("expected Input, got %T", msg))
}
// Process input...
return output(ctx, "output", process(input))
}
return module.Fail(fmt.Errorf("unknown port: %s", port))
}
Sending Output
Use the output handler to send data. output returns module.Result — check it or chain it, never drop it:
// Simple output — chain the Result up the call stack
return output(ctx, "output", OutputMessage{
Result: "success",
Data: processedData,
})
// Output with error handling
if res := output(ctx, "output", data); res.Err() != nil {
return res // Propagate the failure
}
return module.Ok(nil)
Best 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