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):
| Label | Purpose | Example |
|---|---|---|
tinysystems.io/flow-name | Flow resource name | flow-abc123 |
tinysystems.io/project-name | Project resource name | project-xyz |
tinysystems.io/module-version-major | Module major version | common-v1 |
tinysystems.io/execution-mode | Opt node into durable execution | durable |
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
- TinyModule CRD - Module service discovery
- Signals (NATS wire) - Triggering execution
- Controller Reconciliation - Reconciliation patterns