Defining Ports
Ports are the connection points of your component. They define what data your component can receive and send.
Port Structure
package module
type Port struct {
// true = this port is a SOURCE of data (an output).
// false (zero value) = input.
Source bool
// Which side of the node shows this port
Position Position // module.Top / module.Right / module.Bottom / module.Left
// Lower-case programmatic name
Name string
// Human-readable name
Label string
// Typed shape of the port. The runtime reflects its Go type into the
// JSON schema shown in the editor, and decodes incoming messages into
// this type before calling Handle.
Configuration interface{}
// For request/reply ports: the shape of the synchronous response that
// flows back (e.g. http-server's Request port declares Response here).
ResponseConfiguration interface{}
// Escape hatch: when non-nil, published as the port's JSON schema
// verbatim instead of reflecting Configuration. For forms whose shape
// is only known at runtime.
Schema json.RawMessage
}
Sourcesemantics:Source: truemeans the port emits data — it is an output. Input ports leaveSourceunset. Edges run from a source port on one node to an input port on another.
Basic Port Definition
func (c *MyComponent) Ports() []module.Port {
return []module.Port{
// Input port
{
Name: "input",
Label: "Input",
Position: module.Left,
Configuration: InputMessage{},
},
// Output port
{
Name: "output",
Label: "Output",
Source: true,
Position: module.Right,
Configuration: new(OutputMessage),
},
}
}
Port Positions
module.Top
|
+---------------+---------------+
| |
module.Left Component module.Right
| |
+---------------+---------------+
|
module.Bottom
| Constant | Typical Use |
|---|---|
module.Left | Input ports (data flowing in) |
module.Right | Output ports (data flowing out) |
module.Top | Control/dashboard ports |
module.Bottom | Error outputs, default routes |
Configuration Is Required to Receive Data
An input port's Configuration is not optional decoration — the runtime uses reflect on its type to decode every incoming message. A port with a nil Configuration silently drops messages (the runner logs "port has nil configuration" and returns without calling Handle).
// WRONG — messages to this port are dropped
{
Name: "input",
Position: module.Left,
}
// RIGHT — the runtime decodes incoming JSON into InputMessage
{
Name: "input",
Position: module.Left,
Configuration: InputMessage{},
}
For output ports, Configuration supplies the sample value/schema the editor uses for edge mapping and build-time validation. Populate it with representative defaults where you can — downstream edges are validated against this shape.
Schema Generation from Go Types
The runtime generates the port's JSON schema by reflecting the Configuration value's struct tags:
type Request struct {
Method string `json:"method" title:"HTTP Method" enum:"GET,POST,PUT,DELETE" default:"GET"`
URL string `json:"url" title:"URL" format:"uri" required:"true"`
Body any `json:"body,omitempty" title:"Request Body" configurable:"true"`
}
// In Ports()
Configuration: Request{}
Schema Tags
| Tag | Description | Example |
|---|---|---|
json | JSON field name | json:"fieldName" |
title | Human-readable label | title:"Field Name" |
description | Help text | description:"Enter the value" |
required | Mark as required | required:"true" |
configurable | Field accepts arbitrary user-shaped data in the editor | configurable:"true" |
readonly | Display-only field | readonly:"true" |
enum | Allowed values | enum:"a,b,c" |
enumTitles | Display names for enum values | enumTitles:"A,B,C" |
format | UI/validation format | format:"button", format:"textarea", format:"password", format:"uri" |
default | Default value | default:"hello" |
minimum / maximum | Number bounds | minimum:"0" |
minItems / uniqueItems | Array constraints | minItems:"1" |
propertyOrder | Field order in rendered forms | propertyOrder:"1" |
The Schema Field: Runtime-Authored Shapes
Port.Schema (json.RawMessage) is the escape hatch for ports whose shape is only known at runtime — for example a component that receives a JSON Schema as data and renders it as a human form (the ask component). When Schema is non-nil it is published verbatim; otherwise the runtime reflects Configuration as usual.
{
Name: v1alpha1.ControlPort,
Label: "Control",
Source: true,
Position: module.Top,
// The data is a map so an untyped submission has something to decode
// into; the schema is the authored form, published verbatim.
Configuration: c.control(), // map[string]interface{}
Schema: c.form(), // json.RawMessage
}
Two rules:
- Always pair
Schemawith aConfiguration. A port carrying onlySchemaand a nilConfigurationstill drops incoming messages — the decode target comes fromConfiguration. - Don't rely on JSON key order in an authored schema; set
propertyOrderon each field — that is what the editor sorts on.
schema.FromGo (in pkg/schema) bridges the two worlds — it builds schema bytes from a Go value, useful when you pick one of several Go shapes at runtime:
import "github.com/tiny-systems/module/pkg/schema"
{Name: v1alpha1.ControlPort, Source: true, Schema: schema.FromGo(c.getControl())}
A port whose shape is its Go type needs none of this: leave Schema nil.
Request/Reply Ports
A source port that expects a synchronous response back (http-server's Request port is the canonical case) declares the response shape via ResponseConfiguration:
{
Name: "request",
Label: "Request",
Source: true,
Position: module.Right,
Configuration: Request{},
ResponseConfiguration: Response{},
}
Dynamic Ports
Ports can be generated from settings. This is the real router pattern — one input, one output port per configured route:
type Settings struct {
Routes []string `json:"routes" required:"true" title:"Routes" minItems:"1" uniqueItems:"true"`
}
func (r *Component) Ports() []module.Port {
ports := []module.Port{
{
Name: v1alpha1.SettingsPort,
Label: "Settings",
Configuration: r.settings,
},
{
Name: "input",
Label: "IN",
Position: module.Left,
Configuration: InMessage{},
},
}
for _, route := range r.settings.Routes {
ports = append(ports, module.Port{
Name: "out_" + strings.ToLower(route),
Label: route,
Source: true,
Position: module.Right,
Configuration: new(OutMessage),
})
}
return ports
}
Common Port Patterns
Request/Response with Error Port
func (c *HTTPClient) Ports() []module.Port {
return []module.Port{
{
Name: "request",
Label: "Request",
Position: module.Left,
Configuration: HTTPRequest{},
},
{
Name: "response",
Label: "Response",
Source: true,
Position: module.Right,
Configuration: new(HTTPResponse),
},
{
Name: "error",
Label: "Error",
Source: true,
Position: module.Bottom,
Configuration: module.ErrorMessage{},
},
}
}
Use module.ErrorMessage for error ports — it is the canonical {context, error, retryable} shape the retry component and the platform expect.
Filter/Split
func (c *Filter) Ports() []module.Port {
return []module.Port{
{Name: "input", Label: "Input", Position: module.Left, Configuration: Item{}},
{Name: "match", Label: "Match", Source: true, Position: module.Right, Configuration: new(Item)},
{Name: "no_match", Label: "No Match", Source: true, Position: module.Bottom, Configuration: new(Item)},
}
}
Aggregator
func (c *Aggregator) Ports() []module.Port {
return []module.Port{
{Name: "item", Label: "Item", Position: module.Left, Configuration: Item{}},
{Name: "flush", Label: "Flush", Position: module.Top, Configuration: FlushSignal{}},
{Name: "batch", Label: "Batch", Source: true, Position: module.Right, Configuration: new(Batch)},
}
}
Next Steps
- Handling Messages - Process port messages
- Schema Definition - Schema details
- System Ports - Built-in ports