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:

InterfaceMethodReplaces
SettingsHandlerOnSettings(ctx, settings any) error_settings case
ControlHandlerOnControl(ctx, control any) error_control case
ReconcileHandlerOnReconcile(ctx, node v1alpha1.TinyNode) error_reconcile case
IdentityAwareOnIdentity(id v1alpha1.NodeIdentity)_identity case
ClientAwareOnClient(client K8sClient)_client case
StatefulOnState(s module.State)ad-hoc metadata plumbing
EmitterAwareOnEmitter(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: OnIdentityOnClientOnNATSOnStateOnReconcileOnSettings (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

  • Handle returns module.Result; all handler(...) returns chained or checked
  • System-port switch removed; capability interfaces implemented (or module.Base embedded)
  • Transient failures wrapped with module.Retryable; error ports emit module.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

Getting Help

  1. Check the FAQ
  2. tools components-info reports error-port conformance warnings
  3. Open an issue on the SDK repo with the SDK version, error output, and the component code involved