Worker Messages (Configuration RPC)
A FlowBox configuration UI often needs to talk to the running worker: validate a connection string, fetch a list of topics, or echo a value back to the user. Worker Messages is the request/response channel between the configuration microfrontend and a live worker instance. On the C# side the entry point is FlowBoxWorkerBridge (namespace Industream.FlowMaker.Sdk.WorkerBridge), the direct port of the TypeScript SDK's FlowBoxWorkerBridge.
[NOTE!lightbulb/SCOPE] This page covers the worker side (C#). The Svelte side—how a configuration bundle sends a request and how the platform correlates it—is documented in Worker Messages from the Configuration UI. Read that first if you have not.
Request and response flow
- The platform UI owns authentication, worker selection, request IDs, timeouts and cleanup.
- The worker only sees a
WorkerRequeston a Socket.IO namespace and answers with a matchingWorkerResponse. - The bridge dispatches each request to the handler registered for its
boxVersionId— format"<boxId>/<version>", e.g.industream/data-logger/1.0.0. - The
requestIdis echoed back unchanged, so concurrent requests stay isolated.
Registering the bridge
Create a bridge, register one handler per box version, and hand it to the client with UseWorkerBridge. The real cs-data-logger worker does exactly this in Program.cs:
using Industream.FlowMaker.Sdk.Clients;
using Industream.FlowMaker.Sdk.WorkerBridge;
var bridge = new FlowBoxWorkerBridge(host.Services.GetRequiredService<ILoggerFactory>());
bridge.RegisterHandler($"industream/data-logger/{version}", DataLoggerConfigurationHandler.Create().Build());
await host.Services
.GetRequiredService<FlowMakerClientBuilder>()
.DeclareFlowElement(CsDataLoggerBox.GetRegistrationInfo())
.UseWorkerBridge(bridge)
.Build()
.Run();
The bridge constructor is FlowBoxWorkerBridge(ILoggerFactory? loggerFactory = null, string? socketUrlPrefix = null, TimeSpan? handlerTimeout = null). The Socket.IO base URL defaults to FM_WORKER_LOG_SOCKET_IO_ENDPOINT (or http://localhost:4123); the handler timeout defaults to 30 seconds.
[INFO!info/AUTO CONNECT] You never call
bridge.Connect(...)yourself. After the worker registers with the runtime, the client callsbridge.Connect(workerId, boxVersionIds)automatically, using theFM_WORKER_IDenvironment variable asworkerId.Connectis only public for advanced or test scenarios.
Handling events
A handler receives a WorkerRequest and returns the response payload (or throws). WorkerRequest exposes RequestId, BoxVersionId, Event, Payload (a System.Text.Json.JsonElement), and DeserializePayload<T>(JsonSerializerOptions? options = null) which defaults to JsonSerializerOptions.Web. There are three ways to register a handler for a box version.
(a) Typed router with WorkerConfigHandlers (recommended)
WorkerConfigHandlers is a fluent per-event router. Map each event name to a typed handler, then pass .Build() to RegisterHandler. A malformed payload is returned as INVALID_PAYLOAD, an unregistered event as UNKNOWN_EVENT.
using System.Text.Json;
using Industream.FlowMaker.Sdk.WorkerBridge;
public static class DataLoggerConfigurationHandler
{
public static WorkerConfigHandlers Create()
=> new WorkerConfigHandlers()
.On<JsonElement, string>("data-logger:double", (payload, _) =>
{
var raw = payload.TryGetProperty("value", out var valueElement) ? valueElement.ToString() : null;
if (!double.TryParse(raw, out var value))
{
throw new WorkerBridgeException($"Invalid number: {raw}");
}
return ValueTask.FromResult($"your doubled number is : {value * 2}");
});
}
The available overloads are On<TIn, TOut>(@event, Func<TIn, CancellationToken, ValueTask<TOut>>), On<TIn, TOut>(@event, Func<TIn, ValueTask<TOut>>), and On<TOut>(@event, Func<CancellationToken, ValueTask<TOut>>) for events with no payload.
Prefer a typed input record when you want a clean contract and are happy with the SDK's generic error on bad JSON:
public record DoubleInput(double Value);
new WorkerConfigHandlers()
.On<DoubleInput, string>("data-logger:double", (input, _) =>
ValueTask.FromResult($"your doubled number is : {input.Value * 2}"));
Here a payload that is not { "value": <number> } fails deserialization and the router surfaces INVALID_PAYLOAD. Use the loose JsonElement form (as the data-logger does) only when you need a custom error message or partial parsing; use a record otherwise.
(b) Class handler with RegisterClass<T>
Implement IWorkerMessageHandler and register the type. The bridge creates exactly one instance per box version and reuses it for all requests.
using Industream.FlowMaker.Sdk.WorkerBridge;
public sealed class DataLoggerHandler : IWorkerMessageHandler
{
public ValueTask<object?> HandleAsync(WorkerRequest request, CancellationToken cancellationToken = default)
{
var input = request.DeserializePayload<DoubleInput>();
return ValueTask.FromResult<object?>($"your doubled number is : {input!.Value * 2}");
}
}
bridge.RegisterClass<DataLoggerHandler>($"industream/data-logger/{version}");
(c) Raw handler delegate
For full control, register the erased delegate directly. This is the same shape .Build() produces, useful when you already have a dispatch function.
bridge.RegisterHandler($"industream/data-logger/{version}", (request, cancellationToken) =>
{
var input = request.DeserializePayload<DoubleInput>();
return ValueTask.FromResult<object?>($"your doubled number is : {input!.Value * 2}");
});
Error handling
Throw a WorkerBridgeException(message, code) from any handler to return a typed error. The bridge catches it, puts message and code in the response's Error, and the platform rejects the Svelte-side request() promise with that message. Any other exception is reported as HANDLER_ERROR.
throw new WorkerBridgeException("Connection refused by device", WorkerBridgeErrorCodes.HandlerError);
The reserved error codes are defined on WorkerBridgeErrorCodes:
| Code | Constant | When |
|---|---|---|
UNKNOWN_BOX |
UnknownBox |
boxVersionId is not served by this worker |
NO_HANDLER |
NoHandler |
Box is served but has no handler registered |
TIMEOUT |
Timeout |
Handler exceeded the configured timeout (default 30s) |
HANDLER_ERROR |
HandlerError |
Handler threw a non-WorkerBridgeException |
INVALID_PAYLOAD |
InvalidPayload |
Payload failed to deserialize into TIn |
UNKNOWN_EVENT |
UnknownEvent |
Event not registered on the WorkerConfigHandlers router |
The :health event
The bridge answers :health with { status: "ok" } for every registered box version, before dispatching to any handler.
[NOTE!info/AUTO HEALTH] Do not implement
:healthyourself. It is answered automatically byFlowBoxWorkerBridge(seeWorkerBridgeEvents.HealthEvent). A configuration bundle can always ping it to check that a matching worker is live.
Full worked example
The two pieces from cs-data-logger together: the typed router and the wiring.
// Boxes/DataLoggerConfigurationHandler.cs
using System.Text.Json;
using Industream.FlowMaker.Sdk.WorkerBridge;
namespace CsDataLogger.Boxes;
public static class DataLoggerConfigurationHandler
{
public static WorkerConfigHandlers Create()
=> new WorkerConfigHandlers()
.On<JsonElement, string>("data-logger:double", (payload, _) =>
{
var raw = payload.TryGetProperty("value", out var valueElement) ? valueElement.ToString() : null;
if (!double.TryParse(raw, out var value))
{
throw new WorkerBridgeException($"Invalid number: {raw}");
}
return ValueTask.FromResult($"your doubled number is : {value * 2}");
});
}
// Program.cs
var bridge = new FlowBoxWorkerBridge(host.Services.GetRequiredService<ILoggerFactory>());
bridge.RegisterHandler($"industream/data-logger/{version}", DataLoggerConfigurationHandler.Create().Build());
await host.Services
.GetRequiredService<FlowMakerClientBuilder>()
.DeclareFlowElement(CsDataLoggerBox.GetRegistrationInfo())
.UseWorkerBridge(bridge)
.Build()
.Run();
Common Pitfalls
Registering the same box version twice
// !!! WRONG !!! : Two handlers for the same box version
bridge.RegisterHandler("myorg/my-box/1.0.0", firstHandler);
bridge.RegisterHandler("myorg/my-box/1.0.0", secondHandler); // InvalidOperationException
// +++ CORRECT +++ : One handler per box version, route events inside it
var router = new WorkerConfigHandlers()
.On<A, RA>("my-box:a", HandleA)
.On<B, RB>("my-box:b", HandleB);
bridge.RegisterHandler("myorg/my-box/1.0.0", router.Build());
[ERROR!error/DUPLICATE REGISTRATION]
RegisterHandlerthrowsInvalidOperationExceptionif a handler is already registered for the box version. Use a singleWorkerConfigHandlersrouter to fan out multiple events for one box.
Blocking the handler
// !!! WRONG !!! : Blocking call inside the handler
.On<JsonElement, string>("my-box:load", (payload, _) =>
{
var data = httpClient.GetString(url).Result; // blocks a thread up to the timeout
return ValueTask.FromResult(data);
})
// +++ CORRECT +++ : Await asynchronously and honor the token
.On<JsonElement, string>("my-box:load", async (payload, ct) =>
{
var data = await httpClient.GetStringAsync(url, ct);
return data;
})
[WARNING!warning/HANDLER TIMEOUT] Handlers run off the Socket.IO receive loop with a default 30-second timeout. A blocking call (
.Result,.Wait(), sync I/O) wastes a thread and eventually returnsTIMEOUT. Useasync/awaitand pass theCancellationTokenthrough.
Forgetting UseWorkerBridge
// !!! WRONG !!! : Bridge built but never attached — it never connects
var bridge = new FlowBoxWorkerBridge(loggerFactory);
bridge.RegisterHandler("myorg/my-box/1.0.0", handler);
await clientBuilder.DeclareFlowElement(registration).Build().Run();
// +++ CORRECT +++ : Attach the bridge to the client
await clientBuilder
.DeclareFlowElement(registration)
.UseWorkerBridge(bridge)
.Build()
.Run();
[ERROR!error/BRIDGE NEVER CONNECTS] Without
UseWorkerBridge(bridge), the client never callsConnect, so the worker never joins the/worker/<workerId>namespace and every request from the UI fails.
Manually implementing :health
// !!! WRONG !!! : Re-implementing the reserved health event
.On<JsonElement, object>(":health", (_, __) => ValueTask.FromResult<object?>(new { status = "ok" }));
// +++ CORRECT +++ : Leave it alone — the bridge answers :health automatically
// (register only your application events)
[NOTE!sync/RESERVED EVENT]
:healthis answered by the bridge before your handler runs. Registering it in aWorkerConfigHandlersrouter has no effect and only adds dead code.