Skip to content

Settings and Configuration

Settings allow users to configure component behavior through the visual editor. The SDK provides the _settings system port for this purpose.

Settings Port Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│                         SETTINGS FLOW                                        │
└─────────────────────────────────────────────────────────────────────────────┘

    ┌───────────────┐        ┌───────────────┐        ┌───────────────┐
    │  Visual UI    │        │    TinyNode   │        │   Component   │
    │               │───────▶│               │───────▶│               │
    │ User edits    │ Save   │  spec.edges   │ Deliver │  Handle()    │
    │ settings form │        │  [_settings]  │        │  _settings   │
    └───────────────┘        └───────────────┘        └───────────────┘

Defining Settings

Settings Struct

go
type Settings struct {
    Timeout  int    `json:"timeout" title:"Timeout (ms)" default:"5000" minimum:"100"`
    RetryCount int  `json:"retryCount" title:"Retry Count" default:"3" minimum:"0" maximum:"10"`
    Endpoint string `json:"endpoint" title:"API Endpoint" format:"uri" required:"true"`
    Debug    bool   `json:"debug" title:"Debug Mode" default:"false"`
}

Component with Settings

go
type HTTPClient struct {
    settings Settings
}

func (c *HTTPClient) Ports() []module.Port {
    return []module.Port{
        // Settings port (system port)
        {
            Name:     v1alpha1.SettingsPort,  // "_settings"
            Label:    "Settings",
            Source:   true,
            Position: module.PositionTop,
            Schema:   schema.FromGo(Settings{}),
        },
        // Other ports...
        {
            Name:     "request",
            Label:    "Request",
            Source:   true,
            Position: module.PositionLeft,
            Schema:   schema.FromGo(Request{}),
        },
    }
}

func (c *HTTPClient) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    switch port {
    case v1alpha1.SettingsPort:
        c.settings = msg.(Settings)
        return nil

    case "request":
        return c.handleRequest(ctx, output, msg)
    }
    return nil
}

func (c *HTTPClient) handleRequest(ctx context.Context, output module.Handler, msg any) error {
    // Use settings
    client := &http.Client{
        Timeout: time.Duration(c.settings.Timeout) * time.Millisecond,
    }

    req := msg.(Request)
    req.URL = c.settings.Endpoint + req.Path

    // Make request with configured client
    // ...
}

Settings Schema Tags

Basic Tags

TagDescriptionExample
jsonField name in JSONjson:"fieldName"
titleDisplay labeltitle:"Field Name"
descriptionHelp textdescription:"Enter value"
defaultDefault valuedefault:"100"
requiredMark requiredrequired:"true"

Validation Tags

TagDescriptionExample
minimumMin number valueminimum:"0"
maximumMax number valuemaximum:"100"
minLengthMin string lengthminLength:"1"
maxLengthMax string lengthmaxLength:"255"
patternRegex patternpattern:"^[a-z]+$"
enumAllowed valuesenum:"GET,POST,PUT"
formatFormat validationformat:"email"

UI Tags

TagDescriptionExample
propertyOrderField orderpropertyOrder:"1"
enumTitlesEnum display namesenumTitles:"Get,Post,Put"
widgetCustom widgetwidget:"textarea"

Settings Examples

Enum Field

go
type Settings struct {
    Method string `json:"method" title:"HTTP Method" enum:"GET,POST,PUT,DELETE" default:"GET"`
}

Nested Settings

go
type Settings struct {
    Server  ServerSettings  `json:"server" title:"Server Configuration"`
    Logging LoggingSettings `json:"logging" title:"Logging Options"`
}

type ServerSettings struct {
    Host string `json:"host" title:"Host" default:"localhost"`
    Port int    `json:"port" title:"Port" default:"8080" minimum:"1" maximum:"65535"`
}

type LoggingSettings struct {
    Level   string `json:"level" title:"Log Level" enum:"debug,info,warn,error" default:"info"`
    Format  string `json:"format" title:"Format" enum:"json,text" default:"json"`
}

Array Settings

go
type Settings struct {
    Endpoints []EndpointConfig `json:"endpoints" title:"Endpoints" minItems:"1"`
}

type EndpointConfig struct {
    Name    string `json:"name" title:"Name" required:"true"`
    URL     string `json:"url" title:"URL" format:"uri" required:"true"`
    Timeout int    `json:"timeout" title:"Timeout (ms)" default:"5000"`
}

Optional Fields

go
type Settings struct {
    // Required
    APIKey string `json:"apiKey" title:"API Key" required:"true"`

    // Optional (omitempty)
    CustomHeader string `json:"customHeader,omitempty" title:"Custom Header"`
}

Secret Fields

go
type Settings struct {
    // Marked as password in UI
    Password string `json:"password" title:"Password" format:"password"`

    // Or using configRef for Kubernetes secrets
    APIKeyRef ConfigRef `json:"apiKeyRef" title:"API Key (from Secret)"`
}

type ConfigRef struct {
    SecretName string `json:"secretName" title:"Secret Name"`
    Key        string `json:"key" title:"Key"`
}

Settings in Handle()

Basic Pattern

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    switch port {
    case v1alpha1.SettingsPort:
        c.settings = msg.(Settings)
        // Optionally reinitialize resources
        c.reinitialize()
        return nil

    case "input":
        // Settings guaranteed to be set before input arrives
        return c.processWithSettings(ctx, output, msg)
    }
    return nil
}

Settings with Validation

go
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.SettingsPort {
        settings := msg.(Settings)

        // Validate settings
        if err := c.validateSettings(settings); err != nil {
            // Log warning but accept settings
            log.Warn("invalid settings", "error", err)
        }

        c.settings = settings
        return nil
    }
    return nil
}

func (c *Component) validateSettings(s Settings) error {
    if s.Timeout < 100 {
        return fmt.Errorf("timeout must be at least 100ms")
    }
    if s.Endpoint == "" {
        return fmt.Errorf("endpoint is required")
    }
    return nil
}

Settings-Triggered Initialization

go
type Server struct {
    settings Settings
    listener net.Listener
    mu       sync.Mutex
}

func (s *Server) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.SettingsPort {
        settings := msg.(Settings)

        s.mu.Lock()
        defer s.mu.Unlock()

        // Port changed? Restart listener
        if s.settings.Port != settings.Port && s.listener != nil {
            s.listener.Close()
            s.listener = nil
        }

        s.settings = settings
        return nil
    }
    return nil
}

Settings Delivery Order

Settings are delivered before other messages:

┌─────────────────────────────────────────────────────────────────────────────┐
│                         MESSAGE DELIVERY ORDER                               │
└─────────────────────────────────────────────────────────────────────────────┘

1. Node created in Kubernetes


2. _settings delivered first
   │  Handle(ctx, output, "_settings", Settings{...})


3. _reconcile delivered
   │  Handle(ctx, output, "_reconcile", TinyNode{...})


4. Regular messages delivered
   Handle(ctx, output, "input", Message{...})

Dynamic Settings

Settings Affect Ports

go
type DynamicRouter struct {
    settings Settings
}

type Settings struct {
    Routes []string `json:"routes" title:"Route Names"`
}

func (r *DynamicRouter) Ports() []module.Port {
    ports := []module.Port{
        {
            Name:     v1alpha1.SettingsPort,
            Label:    "Settings",
            Source:   true,
            Position: module.PositionTop,
            Schema:   schema.FromGo(Settings{}),
        },
        {
            Name:     "input",
            Label:    "Input",
            Source:   true,
            Position: module.PositionLeft,
            Schema:   schema.FromGo(Message{}),
        },
    }

    // Generate ports from settings
    for _, route := range r.settings.Routes {
        ports = append(ports, module.Port{
            Name:     route,
            Label:    route,
            Source:   false,
            Position: module.PositionRight,
            Schema:   schema.FromGo(Message{}),
        })
    }

    return ports
}

Settings with Expressions

Settings can use expressions for dynamic values:

yaml
# In TinyNode edge
- from: _settings
  data:
    endpoint: "{{$.env.API_URL}}"
    timeout: "{{$.config.defaultTimeout}}"

Best Practices

1. Sensible Defaults

go
type Settings struct {
    // Good: Reasonable defaults
    Timeout int `json:"timeout" default:"5000"`
    Retries int `json:"retries" default:"3"`

    // Bad: No defaults, requires user action
    Timeout int `json:"timeout"`
}

2. Clear Descriptions

go
type Settings struct {
    // Good: Clear what to enter
    APIKey string `json:"apiKey" title:"API Key" description:"Your API key from the dashboard settings page"`

    // Bad: No guidance
    Key string `json:"key"`
}

3. Appropriate Validation

go
type Settings struct {
    // Good: Constrained to valid values
    Port int `json:"port" minimum:"1" maximum:"65535" default:"8080"`

    // Good: Format validation
    Email string `json:"email" format:"email"`

    // Good: Enum for known values
    LogLevel string `json:"logLevel" enum:"debug,info,warn,error"`
}

4. Thread Safety

go
type Component struct {
    settings Settings
    mu       sync.RWMutex
}

func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) error {
    if port == v1alpha1.SettingsPort {
        c.mu.Lock()
        c.settings = msg.(Settings)
        c.mu.Unlock()
        return nil
    }

    // Read with lock
    c.mu.RLock()
    settings := c.settings
    c.mu.RUnlock()

    return c.processWithSettings(ctx, output, msg, settings)
}

Next Steps

Build flow-based applications on Kubernetes