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?
- Create a Go struct implementing
module.Component - Implement
GetInfo(),Ports(),Handle(), andInstance() - Register it with
registry.Register()in the package'sinit(), and blank-import the package incmd/main.go - 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:
- The field is exported (capitalized)
- The
jsontag is present - The tag syntax is correct (no spaces around
=) - You're using supported tags (
title,description,required,configurable, etc.)
How do I debug component execution?
- Use standard Go logging in your component
- Check pod logs:
kubectl logs <pod> - Run
tools components-infoto verify what your binary registers - Inspect execution traces — every run is traced via OpenTelemetry (
OTLP_DSN), viewable in the editor's telemetry panel or via the MCPget_tracestool
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:
- Module is deployed and running
- TinyModule CR exists for the module
- Namespace matches
- No errors in controller logs
- 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.Staterather than raw metadata writes)
Expressions
Why isn't my expression working?
Common issues:
- Missing
$for root reference:{{$.field}}not{{field}} - Wrong quotes: Use single quotes inside expressions:
{{'text'}} - Missing closing braces:
{{$.field}} - Type mismatch: Ensure numeric operations on numbers
- Wrong nesting: some components emit their payload at the root (
$.field), others undercontext($.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?
- Module pods: Scale the Deployment
- Kubernetes resources: Adjust resource requests/limits
- Leader-reader: Leader-only work stays on one pod; message handling spreads across all
- 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?
- Push a
vX.Y.Ztag — CI builds and pushesghcr.io/<org>/<module>:X.Y.Z - List the version in a module repo's
module.yamland regenerate the index (tiny repo index) - Install with
tiny install my-module(orhelm installwith the sharedtinysystems-operatorchart)
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
- Check edges are correctly configured
- Verify port names match exactly
- Check source/target node names
- Look for errors in controller logs, and check the run's trace for where the message stopped
Messages being lost
- Are you dropping the
Resultfrom a non-blocking emit? - Check for panics in component code
- Verify the downstream component is healthy
- Check resource limits (memory, CPU)
Slow performance
- Profile your component code
- Check for N+1 patterns in database queries
- Review blocking operations
- Consider caching frequently accessed data
Pod keeps restarting
- Check memory limits (OOM kills)
- Look for panics in logs
- Verify environment variables are set
- 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?
- Store them in Kubernetes Secrets in the module's namespace
- 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?
- Check existing issues
- Create a minimal reproduction
- Include:
- SDK version (
sdkVersionin the TinyModule status) and module versions - Kubernetes version
- Component code (if applicable)
- Error messages and logs
- Steps to reproduce
- SDK version (