Frequently Asked Questions

Common questions about TinySystems development and usage.


General

What is TinySystems?

TinySystems is a Kubernetes-native, flow-based application development platform. It allows you to build applications by connecting reusable components in visual flows, with each component running as a Kubernetes resource.

How does TinySystems differ from other workflow tools?

Key differences:

  • Kubernetes-native: Components run as Kubernetes resources, leveraging K8s for scaling, networking, and state
  • Go-based SDK: Write components in Go with full type safety
  • Blocking execution: Synchronous message passing ensures reliable processing
  • CR-based state: State propagation via Custom Resources enables horizontal scaling

What can I build with TinySystems?

  • API integrations and data pipelines
  • Webhook processors and event handlers
  • Scheduled automation tasks
  • Data transformation workflows
  • Multi-service orchestration
  • Internal tools and dashboards

Development

How do I create a new component?

  1. Create a Go struct implementing module.Component
  2. Implement GetInfo(), Ports(), Handle(), and Instance()
  3. Register it with registry.Register() in the package's init(), and blank-import the package in cmd/main.go
  4. Build and deploy

See the Component Interface guide.

Why does my component need to implement Instance()?

Instance() returns a fresh component instance for each TinyNode. This ensures:

  • No shared state between nodes
  • Clean initialization
  • Proper garbage collection
func (c *Component) Instance() module.Component {
    return &Component{}
}

How do I add configuration to my component?

Create a Settings struct, expose it on the _settings port, and implement SettingsHandler — the runner calls OnSettings for you, no port switch needed:

type Settings struct {
    Timeout string `json:"timeout" default:"30s"`
}

func (c *Component) Ports() []module.Port {
    return []module.Port{
        {
            Name:          v1alpha1.SettingsPort,
            Configuration: Settings{},
        },
        // ... data ports
    }
}

func (c *Component) OnSettings(ctx context.Context, settings any) error {
    c.settings = settings.(Settings)
    return nil
}

Why isn't my struct tag appearing in the UI?

Check that:

  1. The field is exported (capitalized)
  2. The json tag is present
  3. The tag syntax is correct (no spaces around =)
  4. You're using supported tags (title, description, required, configurable, etc.)

How do I debug component execution?

  1. Use standard Go logging in your component
  2. Check pod logs: kubectl logs <pod>
  3. Run tools components-info to verify what your binary registers
  4. Inspect execution traces — every run is traced via OpenTelemetry (OTLP_DSN), viewable in the editor's telemetry panel or via the MCP get_traces tool

Execution Model

Why does output() block?

The blocking model provides:

  • Backpressure: Slow consumers prevent fast producers from overwhelming the system
  • Error propagation: Errors bubble up through the call chain
  • Reliable delivery: Messages aren't lost to buffer overflows
  • Predictable execution: Easy to reason about flow behavior

Can I send messages without blocking?

For background emission (tickers, watchers, long-running loops), implement EmitterAware or embed module.Base and call b.Emit(ctx, port, data) from your goroutine — the injected handler stays valid for the runner's lifetime.

A bare go output(ctx, "notification", msg) also works, but you lose error handling and backpressure, and the component may complete before delivery. If you fire-and-forget, at least log the dropped Result.

What happens when downstream processing fails?

The failure propagates back through the output() call chain as a module.Result:

res := output(ctx, "result", data)
if err := res.Err(); err != nil {
    // Downstream failed - handle the error
    return module.Fail(err)
}
return module.Ok(nil)

How do I handle errors in my component?

Option 1: Return a failed result (blocks the upstream):

return module.Fail(fmt.Errorf("processing failed: %w", err))

Option 2: Emit on an error port (flow continues) using the canonical shape:

return handler(ctx, ErrorPort, module.NewError(reqContext, err))

If the failure is transient (5xx, 429, timeout), mark it retryable so backoff retry can clear it — unmarked errors are never retried:

return module.Fail(module.Retryable(err))

Kubernetes Integration

Why isn't my TinyNode being processed?

Check:

  1. Module is deployed and running
  2. TinyModule CR exists for the module
  3. Namespace matches
  4. No errors in controller logs
  5. Component name matches, and the node's module reference resolves (bare names like http-module; legacy publisher-prefixed names still match)

How do I expose an HTTP endpoint?

The SDK's client capability exposes a port and returns the resulting hostnames; the resource manager creates and maintains the Service and Ingress:

// module.Client interface:
// ExposePort(ctx context.Context, autoHostName string, hostnames []string, port int) ([]string, error)
urls, err := client.ExposePort(ctx, autoHostName, hostnames, 8080)

Note the module needs rbac.enableKubernetesResourceAccess: true in its install values for the Service/Ingress writes — generate that overlay with tools rbac-values.

How does leader election work?

Each module deployment runs one leader election, backed by a single Kubernetes Lease (<module>-lock) — not one lease per node. The pod holding the lease is the leader for the whole module: it performs status updates and leader-only component work (timers, cron, servers' singleton duties).

if utils.IsLeader(ctx) {
    // Leader-only operations
}

Can multiple pods handle the same TinyNode?

Yes, with the leader-reader pattern:

  • One leader pod (per module) handles state changes and leader-only work
  • All pods can process messages
  • State propagates via TinyNode metadata (use module.State rather than raw metadata writes)

Expressions

Why isn't my expression working?

Common issues:

  1. Missing $ for root reference: {{$.field}} not {{field}}
  2. Wrong quotes: Use single quotes inside expressions: {{'text'}}
  3. Missing closing braces: {{$.field}}
  4. Type mismatch: Ensure numeric operations on numbers
  5. Wrong nesting: some components emit their payload at the root ($.field), others under context ($.context.field) — check the port's schema in the editor

How do I provide a default value?

Use the OR operator:

name: "{{$.user.name || 'Anonymous'}}"
count: "{{$.count || 0}}"

Can I use complex logic in expressions?

Expressions support:

  • Path access: {{$.deep.nested.value}}
  • Operators: {{$.a + $.b}}
  • Conditionals: {{$.active ? 'yes' : 'no'}}
  • Functions: {{upper($.name)}}

For complex logic, use a Modify component instead.

How do I access array elements?

first: "{{$.items[0]}}"
last: "{{$.items[-1]}}"
dynamic: "{{$.items[$.index]}}"

Scaling

How do I scale my flow?

  1. Module pods: Scale the Deployment
  2. Kubernetes resources: Adjust resource requests/limits
  3. Leader-reader: Leader-only work stays on one pod; message handling spreads across all
  4. Parallel processing: Use iterator/aggregator patterns

Why isn't scaling improving performance?

Consider:

  • Is the bottleneck CPU/memory or I/O?
  • Are you using leader-only operations?
  • Is there contention on shared resources?
  • Does the flow support parallelization?

How do I share state across pods?

Use module.State (embed module.Base and call b.State()):

_ = b.State().Set(ctx, "key", []byte("value"))
value, ok, _ := b.State().Get(ctx, "key")

The default backend persists via TinyNode metadata and converges across replicas through the Kubernetes watch. For durable, run-lifetime state shared across pods, use State().Scoped(module.ScopeExecution, runID) (JetStream-backed when a broker is configured).


Deployment

How do I deploy my module?

  1. Push a vX.Y.Z tag — CI builds and pushes ghcr.io/<org>/<module>:X.Y.Z
  2. List the version in a module repo's module.yaml and regenerate the index (tiny repo index)
  3. Install with tiny install my-module (or helm install with the shared tinysystems-operator chart)

See Registry Publishing. There is no per-module Helm chart to write.

What Kubernetes version is required?

Any reasonably current cluster works — TinySystems relies on Custom Resources, server-side apply, and Lease-based leader election, all standard for years. kind, k3s, minikube, EKS, GKE are all fine.

Can I run TinySystems locally?

Yes: minikube start (or kind/k3d/Docker Desktop), then tiny up provisions the runtime and core modules. During module development, run your module from source with go run ./cmd run ... against the same cluster.


Troubleshooting

Component not receiving messages

  1. Check edges are correctly configured
  2. Verify port names match exactly
  3. Check source/target node names
  4. Look for errors in controller logs, and check the run's trace for where the message stopped

Messages being lost

  1. Are you dropping the Result from a non-blocking emit?
  2. Check for panics in component code
  3. Verify the downstream component is healthy
  4. Check resource limits (memory, CPU)

Slow performance

  1. Profile your component code
  2. Check for N+1 patterns in database queries
  3. Review blocking operations
  4. Consider caching frequently accessed data

Pod keeps restarting

  1. Check memory limits (OOM kills)
  2. Look for panics in logs
  3. Verify environment variables are set
  4. Check liveness/readiness probes

Best Practices

How should I structure my module?

my-module/
+-- cmd/
|   +-- main.go              # cobra root + cli.RegisterCommands + blank imports
+-- components/
|   +-- component1/
|   |   +-- component1.go
|   +-- component2/
|       +-- component2.go
+-- .github/workflows/
|   +-- release.yml
+-- Dockerfile
+-- go.mod

No chart directory — deployment uses the shared operator chart.

When should I create a new component vs. use expressions?

Use expressions for:

  • Simple data mapping
  • Field renaming
  • Basic transformations

Create a component for:

  • Complex business logic
  • External API calls
  • State management
  • Custom protocols

How do I handle secrets?

  1. Store them in Kubernetes Secrets in the module's namespace
  2. Reference them in node settings with a placeholder — the SDK resolves it at dispatch time:
apiKey: "[[secret:my-keys/anthropic]]"

The placeholder must be the whole field value (Bearer [[secret:a/b]] does not resolve), and the module's install needs secrets.enabled: true in its chart values to be allowed to read Secrets. Never log secret values; resolved values are redacted from traces.


Getting Help

Where can I get support?

  • Documentation: This site
  • GitHub Issues: Report bugs and feature requests
  • Community: Join discussions

How do I report a bug?

  1. Check existing issues
  2. Create a minimal reproduction
  3. Include:
    • SDK version (sdkVersion in the TinyModule status) and module versions
    • Kubernetes version
    • Component code (if applicable)
    • Error messages and logs
    • Steps to reproduce