TinyNode CRD

TinyNode is the core Custom Resource that represents a component instance in a flow. Understanding TinyNode is essential for module development.

Full Specification

apiVersion: operator.tinysystems.io/v1alpha1
kind: TinyNode
metadata:
  name: my-router-abc123
  namespace: tinysystems
  labels:
    tinysystems.io/flow-name: "flow-xyz"
    tinysystems.io/project-name: "project-123"
    tinysystems.io/module-version-major: "common-v1"
spec:
  # Module name (bare reference)
  module: common

  # Component name within the module
  component: router

  # Port configurations (settings + per-edge data mapping)
  ports:
    - port: _settings
      configuration: '{"routes": ["success", "failure"]}'
    - port: input
      from: upstream-node:output       # mapping for messages from this sender
      configuration: '{"context": "{{$.result}}", "userId": "{{$.user.id}}"}'
      flowID: flow-xyz

  # Outgoing connections
  edges:
    - id: "edge-1"
      port: "out_success"              # Source port on this node
      to: "next-node-name:input"       # Target node:port (single field)
      flowID: flow-xyz
      retryPolicy:                     # optional, per-edge opt-in
        maxAttempts: 3
        initialDelayMs: 1000

status:
  # Observed generation for idempotency
  observedGeneration: 1

  # Module information
  module:
    name: common
    version: "1.0.0"
    sdkVersion: "0.13.59"

  # Component information
  component:
    description: "Routes messages based on conditions"
    info: "Router component"
    tags: ["routing", "conditional"]

  # Port definitions with schemas
  ports:
    - name: input
      label: "Input"
      position: 3              # Left
      source: false
      schema: <base64 JSON Schema>
      configuration: <base64 JSON>
    - name: out_success
      label: "Success"
      position: 1              # Right
      source: true
      schema: <base64 JSON Schema>

  # Status string ("OK" when healthy) and error flag
  status: "OK"
  error: false

  # Shared metadata across pods
  metadata:
    http-server-port: "8080"
    custom-key: "custom-value"

  # Last update timestamp
  lastUpdateTime: "2024-01-15T10:30:00Z"

Spec Fields

module

The module name providing this component. References are written bare (no publisher prefix); already-deployed prefixed names keep working via tolerant matching:

spec:
  module: common

Used by each module's TinyNodeReconciler to decide whether the node belongs to it (the node's name also carries the module).

component

The component name within the module:

spec:
  component: router

Must match the Name returned by GetInfo().

ports

Array of port configurations (TinyNodePortConfig). Each entry configures one port, optionally scoped to a specific sender via from — this is where expression-based data mapping lives:

spec:
  ports:
    - port: _settings                 # own settings (no "from")
      configuration: '{"interval": 5}'
    - port: input                     # mapping for a specific upstream sender
      from: source-node:output
      configuration: |
        {
          "userId": "{{$.user.id}}",
          "message": "Hello {{$.user.name}}!",
          "total": "{{$.price * $.quantity}}",
          "isValid": "{{$.status == 'active'}}",
          "staticValue": "constant"
        }
      flowID: flow-xyz

schema (JSON Schema of the port) may accompany configuration; both are stored as raw bytes.

edges

Array of outgoing connections (TinyNodeEdge). The target is a single to field in node:port form — there is no separate toPort, and no mapping on the edge itself:

spec:
  edges:
    - id: "edge-abc123"            # Unique edge identifier
      port: "output"               # Source port on this node
      to: "target-node-name:input" # Target node:port
      flowID: "flow-xyz"           # Owning flow

retryPolicy (optional)

Edges are single-shot by default. Authors opt into retry per edge for transient-failure-safe targets; only failures the component marked with module.Retryable are re-attempted:

      retryPolicy:
        maxAttempts: 3               # 1-10; 1 = no retry (default)
        initialDelayMs: 1000         # default 1s
        backoffCoefficient: "2.0"    # default 2.0
        maxDelayMs: 30000            # default 30s
        timeoutMs: 600000            # per-attempt handler timeout
        nonRetryableErrorCodes:      # short-circuit on these codes
          - quota_exceeded
          - unauthorized

Status Fields

observedGeneration

Tracks which spec version was last processed:

if node.Status.ObservedGeneration >= node.Generation {
    // Already processed this version
    return ctrl.Result{}, nil
}

// Process and update
node.Status.ObservedGeneration = node.Generation

ports

Array of port definitions generated from the component. position uses the module.Position constants (Top=0, Right=1, Bottom=2, Left=3); source: true marks an OUTPUT port. schema and configuration are byte fields (base64-encoded in YAML):

status:
  ports:
    - name: input
      label: "Input"
      position: 3          # module.Left
      source: false        # input port
      schema: eyJ0eXBlIjogIm9iamVjdCIsIC4uLn0=       # JSON Schema bytes
      configuration: eyJjb250ZXh0Ijoge319             # current config bytes

System ports (_reconcile, _client, _identity) are filtered out of the published status.

metadata

Key-value store for sharing state across pods:

status:
  metadata:
    http-server-port: "8080"
    custom-state: "value"

Used for CR-Based State Propagation.

status / error

status.error is a boolean; the human-readable message goes into status.status (which reads "OK" when healthy):

status:
  status: "component initialization failed: missing required setting"
  error: true

Labels

Standard labels applied to TinyNodes (all under the tinysystems.io/ prefix):

LabelPurposeExample
tinysystems.io/flow-nameFlow resource nameflow-abc123
tinysystems.io/project-nameProject resource nameproject-xyz
tinysystems.io/module-version-majorModule major versioncommon-v1
tinysystems.io/execution-modeOpt node into durable executiondurable

Accessing TinyNode in Components

Via the ReconcileHandler Capability

The _reconcile system port never reaches Handle. Components implement module.ReconcileHandler instead — the framework calls it on every node reconcile:

func (c *Component) OnReconcile(ctx context.Context, node v1alpha1.TinyNode) error {
    // Access node information
    c.nodeName = node.Name
    edges := node.Spec.Edges
    metadata := node.Status.Metadata

    // Read shared state
    if port, ok := metadata["http-server-port"]; ok {
        c.configuredPort = port
    }
    return nil
}

Updating Node Status

Emit an updater function (returning error) to the _reconcile port; the runner debounces and patches the node. Gate mutation on leadership:

func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) module.Result {
    // Update metadata (leader only)
    if utils.IsLeader(ctx) {
        output(ctx, v1alpha1.ReconcilePort, func(node *v1alpha1.TinyNode) error {
            if node.Status.Metadata == nil {
                node.Status.Metadata = make(map[string]string)
            }
            node.Status.Metadata["my-key"] = "my-value"
            return nil
        })
    }
    return module.Ok(nil)
}

TinyNode Lifecycle

1. CREATION
   +------------------------------------------------------------+
   | Platform creates TinyNode CR when flow is deployed         |
   | Labels link to flow, project, workspace                    |
   +------------------------------------------------------------+
                              |
                              v
2. RECONCILIATION
   +------------------------------------------------------------+
   | TinyNodeReconciler detects new node                        |
   | Creates Runner instance                                    |
   | Sends to _settings port                                    |
   | Updates status with port schemas                           |
   +------------------------------------------------------------+
                              |
                              v
3. EXECUTION
   +------------------------------------------------------------+
   | Messages arrive over the transport (edge hops and          |
   | wire.Publish signals)                                      |
   | Component.Handle() processes messages                      |
   | Output routed via edges to next nodes                      |
   +------------------------------------------------------------+
                              |
                              v
4. PERIODIC RECONCILIATION
   +------------------------------------------------------------+
   | Every 5 minutes: _reconcile port triggered                 |
   | Component can refresh state                                |
   | Leader updates status if needed                            |
   +------------------------------------------------------------+
                              |
                              v
5. DELETION
   +------------------------------------------------------------+
   | Platform deletes TinyNode CR when flow is undeployed       |
   | Runner instance destroyed                                  |
   | Resources cleaned up                                       |
   +------------------------------------------------------------+

Example: How an Edge Is Dispatched

// Simplified — the runner walks the node's edges for the emitting port
func (r *Runner) processEdges(ctx context.Context, port string, data any) error {
    for _, edge := range r.node.Spec.Edges {
        if edge.Port != port {
            continue
        }

        // edge.To is already the full "node:port" target.
        // The RECEIVING node's matching port config (spec.ports with
        // from == this node:port) supplies the {{expression}} mapping,
        // evaluated on the receiver side.
        r.scheduler.Handle(ctx, &runner.Msg{
            To:     edge.To,
            From:   utils.GetPortFullName(r.node.Name, port),
            EdgeID: edge.ID,
            Data:   data,
        })
    }
    return nil
}

Next Steps