Component Interface Reference
Complete reference for the module.Component interface that all TinySystems components must implement.
Source of truth: module/component.go, module/lifecycle.go, module/result.go in the SDK (github.com/tiny-systems/module).
Interface Definition
type Component interface {
// GetInfo returns component metadata
GetInfo() ComponentInfo
// Handle processes a message arriving on `port`; emits via `output`
Handle(ctx context.Context, output Handler, port string, message any) Result
// Ports returns the component's port definitions
Ports() []Port
// Instance creates a new component instance with default settings
Instance() Component
}
Methods
GetInfo
Returns metadata about the component.
func (c *Component) GetInfo() module.ComponentInfo
Returns: module.ComponentInfo
type ComponentInfo struct {
Name string // Unique component identifier
Description string // Short human-readable description
Info string // Longer help text
Tags []string // Categorization tags
}
Example:
func (c *Component) GetInfo() module.ComponentInfo {
return module.ComponentInfo{
Name: "array_get",
Description: "Get an array element by index",
Info: "Returns {item, index} for the requested 1-based index, or an error when out of range.",
Tags: []string{"array", "utility"},
}
}
Notes:
Namemust be unique within the module; use lowercasesnake_case(e.g.array_get,http_server)Namemust not contain dots — full node names have the form{project-prefix}.{module}.{component}- There is no
Iconfield
Handle
Processes incoming messages on a data port. Returns module.Result, never a bare error or any.
func (c *Component) Handle(
ctx context.Context,
output module.Handler,
port string,
message any,
) module.Result
Parameters:
| Parameter | Type | Description |
|---|---|---|
| ctx | context.Context | Request context with tracing, cancellation |
| output | module.Handler | Function to emit output messages; itself returns Result |
| port | string | Name of the port receiving the message |
| message | any | The incoming message data |
Returns: module.Result — construct with module.Ok(v) / module.Fail(err); the runner unwraps it via Err() / Value().
Chain the Result returned by output up the call stack: blocking-I/O callers (e.g. http-server) receive the synchronous response that flows back from downstream nodes through these returns.
System ports never arrive at Handle. _settings, _control, _reconcile, _client and _identity are dispatched exclusively through the capability interfaces below; there is no legacy fallback into Handle. See the System Ports Reference.
Example:
func (c *Component) Handle(
ctx context.Context,
output module.Handler,
port string,
message any,
) module.Result {
switch port {
case "input":
in, ok := message.(Input)
if !ok {
return module.Fail(fmt.Errorf("invalid input type: %T", message))
}
result, err := c.process(in)
if err != nil {
return module.Fail(err)
}
return output(ctx, "output", result)
default:
return module.Fail(fmt.Errorf("unknown port: %s", port))
}
}
Error retryability: unmarked errors are never retried. Wrap transient failures with module.Retryable(err) so per-edge retry policies and the retry component can act on them; module.Permanent(err) marks the opposite explicitly. Emit module.NewError(ctx, err) on error ports to produce the canonical {context, error, retryable} payload. See the Handler Interface Reference for details.
Ports
Returns the component's port definitions.
func (c *Component) Ports() []module.Port
Returns: []module.Port
type Port struct {
Source bool // true = OUTPUT (source of data), false = input
Position Position // Top(0), Right(1), Bottom(2), Left(3)
Name string // lower-case programmatic name
Label string // human-readable name
Configuration interface{} // request schema struct
ResponseConfiguration interface{} // response schema struct (blocking ports)
Schema json.RawMessage // raw JSON schema override (runtime-shaped forms)
}
See the Port Interface Reference for field semantics.
Example:
func (c *Component) Ports() []module.Port {
return []module.Port{
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Configuration: Settings{},
},
{
Name: "input",
Label: "Input",
Position: module.Left,
Configuration: Input{},
},
{
Name: "output",
Label: "Output",
Source: true, // output port
Position: module.Right,
Configuration: Output{},
},
{
Name: "error",
Label: "Error",
Source: true, // output port
Position: module.Bottom,
Configuration: module.ErrorMessage{},
},
}
}
Instance
Creates a new instance of the component.
func (c *Component) Instance() module.Component
Returns: module.Component — a new, independent instance
Example:
func (c *Component) Instance() module.Component {
return &Component{}
}
Notes:
- Must return a fresh instance
- Do not share state between instances
- Each TinyNode gets its own instance
Registration
Register the component with the module's registry in an init function:
import "github.com/tiny-systems/module/registry"
func init() {
registry.Register((&Component{}).Instance())
}
Capability Interfaces
Optional interfaces the framework detects on the component instance. System ports are delivered only through these — never through Handle.
| Interface | Method | Delivers |
|---|---|---|
SettingsHandler | OnSettings(ctx context.Context, settings any) error | Deserialized _settings configuration |
ControlHandler | OnControl(ctx context.Context, control any) error | _control messages (dashboard widgets, Start/Stop) |
ReconcileHandler | OnReconcile(ctx context.Context, node v1alpha1.TinyNode) error | The TinyNode (by value) on every reconcile |
IdentityAware | OnIdentity(id v1alpha1.NodeIdentity) | Node name, namespace, flow, project |
ClientAware | OnClient(client module.K8sClient) | Kubernetes client access |
NATSAware | OnNATS(js jetstream.JetStream) | JetStream handle (nil when TINY_NATS_URL unset) |
Stateful | OnState(s module.State) | Persistent key-value state backend |
EmitterAware | OnEmitter(emit module.Handler) | Long-lived handler for emitting from goroutines |
Destroyer | OnDestroy(metadata map[string]string) | Called on node destruction (finalizer) with Status.Metadata |
Dispatch order on a fresh runner:
OnIdentityOnClientOnNATSOnStateOnReconcile— restore state from metadata, react to specOnSettings— user settings applied last, so they win over reconcile-restored state
On subsequent reconciles OnReconcile fires again; OnSettings only re-fires when the configured value changes.
Example — settings and reconcile:
func (c *Component) OnSettings(_ context.Context, msg any) error {
in, ok := msg.(Settings)
if !ok {
return fmt.Errorf("invalid settings")
}
c.settings = in
return nil
}
func (c *Component) OnReconcile(_ context.Context, node v1alpha1.TinyNode) error {
if v, ok := node.Status.Metadata["http-port"]; ok {
c.listenPort = v
}
return nil
}
module.Base: embed module.Base to satisfy Stateful, IdentityAware, ClientAware and EmitterAware for free; access the injected dependencies via b.State(), b.Identity(), b.Client(), b.Emit(ctx, port, data). Base deliberately does not implement ReconcileHandler or SettingsHandler — those are always component-specific.
Handler Function
The output handler for emitting messages.
type Handler func(ctx context.Context, port string, data any) module.Result
| Parameter | Type | Description |
|---|---|---|
| ctx | context.Context | Must pass through the incoming context |
| port | string | Output port name |
| data | any | Data to emit |
Returns: module.Result — Err() is non-nil on delivery/downstream failure; Value() carries the synchronous response that flows back through blocking-I/O chains. Chain it into your own return.
For emitting outside Handle (tickers, watchers, background loops), implement EmitterAware (or embed Base) to receive a long-lived Handler valid for the runner's lifetime.
Complete Example
package multiplier
import (
"context"
"fmt"
"github.com/tiny-systems/module/api/v1alpha1"
"github.com/tiny-systems/module/module"
"github.com/tiny-systems/module/registry"
)
const (
ComponentName = "multiplier"
InputPort = "input"
OutputPort = "output"
)
type Settings struct {
Multiplier int `json:"multiplier" title:"Multiplier" default:"1"`
}
type Input struct {
Value int `json:"value" required:"true" title:"Value"`
}
type Output struct {
Result int `json:"result" title:"Result"`
}
type Component struct {
settings Settings
}
func (c *Component) GetInfo() module.ComponentInfo {
return module.ComponentInfo{
Name: ComponentName,
Description: "Multiplies input value by the configured multiplier",
Info: "Emits {result: value * multiplier} for every input message.",
Tags: []string{"math", "transform"},
}
}
// OnSettings receives _settings; the port never reaches Handle.
func (c *Component) OnSettings(_ context.Context, msg any) error {
in, ok := msg.(Settings)
if !ok {
return fmt.Errorf("invalid settings")
}
c.settings = in
return nil
}
func (c *Component) Handle(
ctx context.Context,
output module.Handler,
port string,
message any,
) module.Result {
switch port {
case InputPort:
in, ok := message.(Input)
if !ok {
return module.Fail(fmt.Errorf("invalid input type: %T", message))
}
return output(ctx, OutputPort, Output{Result: in.Value * c.settings.Multiplier})
default:
return module.Fail(fmt.Errorf("unknown port: %s", port))
}
}
func (c *Component) Ports() []module.Port {
return []module.Port{
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Configuration: Settings{Multiplier: 1},
},
{
Name: InputPort,
Label: "Input",
Position: module.Left,
Configuration: Input{},
},
{
Name: OutputPort,
Label: "Output",
Source: true,
Position: module.Right,
Configuration: Output{},
},
}
}
func (c *Component) Instance() module.Component {
return &Component{}
}
var (
_ module.Component = (*Component)(nil)
_ module.SettingsHandler = (*Component)(nil)
)
func init() {
registry.Register((&Component{}).Instance())
}