Migration Guide
Notes for module authors tracking recent SDK changes (the v0.13.x line, github.com/tiny-systems/module). Update with go get github.com/tiny-systems/module@latest, then work through whichever of these applies to your code.
Handle and Handler Return module.Result
Component.Handle and the injected module.Handler both return module.Result instead of any. Build results with module.Ok / module.Fail; read them with Err() / Value().
Before:
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) any {
if in, ok := msg.(InMessage); ok {
return output(ctx, "out", in.Context)
}
return fmt.Errorf("invalid message")
}
After:
func (c *Component) Handle(ctx context.Context, output module.Handler, port string, msg any) module.Result {
if in, ok := msg.(InMessage); ok {
return output(ctx, "out", OutMessage{Context: in.Context})
}
return module.Fail(fmt.Errorf("invalid message"))
}
Chain the handler's return up the call stack — that propagation is what makes blocking I/O work (http-server hands a request downstream and waits on the response flowing back). The typed return exists precisely because _ = handler(...) silently dropped responses before.
System-Port Switches → Capability Interfaces
Components no longer switch on v1alpha1.SettingsPort / ControlPort / ReconcilePort inside Handle. The runner dispatches to typed capability interfaces first:
| Interface | Method | Replaces |
|---|---|---|
SettingsHandler | OnSettings(ctx, settings any) error | _settings case |
ControlHandler | OnControl(ctx, control any) error | _control case |
ReconcileHandler | OnReconcile(ctx, node v1alpha1.TinyNode) error | _reconcile case |
IdentityAware | OnIdentity(id v1alpha1.NodeIdentity) | _identity case |
ClientAware | OnClient(client K8sClient) | _client case |
Stateful | OnState(s module.State) | ad-hoc metadata plumbing |
EmitterAware | OnEmitter(emit module.Handler) | stashing the handler for goroutines |
Embed module.Base to get Stateful, IdentityAware, ClientAware, and EmitterAware for free, with accessors b.State(), b.Identity(), b.Client(), b.Emit(...). Dispatch order on a fresh runner: OnIdentity → OnClient → OnNATS → OnState → OnReconcile → OnSettings (settings always wins over reconcile-restored state).
Retry Is Opt-In (May 2026)
Edges no longer retry every failure. module.ShouldRetry is the single predicate used by both the scheduler's edge dispatch and the retry component, and unmarked errors are not retried — re-attempting a hop whose side effect landed duplicates it (the old retry-everything default burned money on a storm of duplicate LLM calls).
Mark transient failures explicitly:
if resp.StatusCode >= 500 {
return module.Fail(module.Retryable(fmt.Errorf("upstream 5xx: %s", body)))
}
And emit the canonical shape on error ports so downstream (the retry component, the platform) understands it:
return handler(ctx, ErrorPort, module.NewError(reqContext, err))
module.ErrorMessage is {context, error, retryable}; tools components-info warns when an error port emits anything else. module.Permanent(err) explicitly forbids retry; pkg/errors permanent markings still win for compatibility.
TinySignal Removed
The TinySignal CR is gone. Messages between modules travel over the NATS wire (set TINY_NATS_URL; TINY_NATS_TRANSPORT=jetstream for the durable work-queue transport), and manual triggering goes through the MCP send_signal tool served by the tiny dev server. Delete any code or fixtures that create TinySignal resources; the CRDs installed by tiny up are TinyModule, TinyNode, and TinyFlow.
On the durable (jetstream) wire, failed hops are Nak'd for redelivery rather than terminated — but only failures marked retryable re-drive, per the retry rules above.
Bare Module References
A module's identity no longer embeds its publisher. New references in cluster state are written bare (http-module, not tinysystems/http-module), and the runtime's module.NameMatches is tolerant in both directions (/ and - equivalent, prefix optional), so existing prefixed nodes keep resolving without renames. Stop generating publisher-prefixed names in anything that writes specs.
Runtime-Authored Port Schemas (Port.Schema)
module.Port gained a Schema json.RawMessage field. When non-nil it is published as the port's JSON schema verbatim, instead of reflecting Configuration. This is only for ports whose shape exists at runtime, not compile time — e.g. a component that receives a JSON Schema as data and renders it as a human form (the ask component). Components with a Go type change nothing: reflection remains the default. Use propertyOrder on fields to control rendered order; key order is not preserved.
Checklist
-
Handlereturnsmodule.Result; allhandler(...)returns chained or checked - System-port
switchremoved; capability interfaces implemented (ormodule.Baseembedded) - Transient failures wrapped with
module.Retryable; error ports emitmodule.NewError - No TinySignal usage; manual triggers via MCP
send_signal - No publisher-prefixed module references written
- Go 1.25+, SDK v0.13.x in
go.mod