How-to guide

Write a worker

Implement method handlers with the worker SDKs, declare parameters, send progress feedback and read platform-delivered settings.

Distro Current 5 min read Updated

Prerequisites

  • A service and a worker profile (see the [quickstart](../getting-started/quickstart.md))
  • The worker SDK for your language: .NET, Java, Node.js or Python

Note

Kiwi Distro is currently in private preview. Interfaces and behaviour described here can change before general availability. Contact Kiwify Labs for access.

A worker is a process that registers for one service and executes its methods. SDKs exist for .NET, Java, Node.js and Python; they follow the same wire contract (Worker protocol).

Define a method#

Choose your language. The choice applies to every sample on the site and is remembered in your browser.

.NET
[MethodName("charge")]
public class ChargeMethod : MethodHandlerBase
{
    public decimal Amount { get; set; }
    public string Currency { get; set; } = "USD";

    protected override void Execute() => SetResult(new { charged = Amount, currency = Currency });
}
Java
worker.method("charge",
    List.of(
        ParameterSpec.of("amount").type(ParameterSpec.Type.NUMBER).required(),
        ParameterSpec.of("currency").type(ParameterSpec.Type.STRING)
            .withDefault("USD").pattern("^[A-Z]{3}$")),
    ctx -> Map.of("charged", ctx.param("amount"), "currency", ctx.param("currency")));
Node.js
worker.method(
  "charge",
  {
    parameters: [
      { name: "amount", type: "number", required: true },
      { name: "currency", type: "string", default: "USD", pattern: "^[A-Z]{3}$" },
    ],
  },
  (ctx) => ({ charged: ctx.param("amount"), currency: ctx.param("currency") }),
);
Python
worker.method(
    "charge",
    handler,                                  # sync or async; return a value to succeed, raise to fail
    parameters=[
        ParameterSpec("amount",   type="number", required=True),
        ParameterSpec("currency", type="string", default="USD", pattern="^[A-Z]{3}$"),
    ],
)

In Java, Node.js and Python a handler returns a value to succeed and throws to fail. In .NET a handler is a class marked [MethodName]: request arguments are bound to its public properties (matched case-insensitively), and SetResult sets the result. The worker object is created as in the quickstart.

In Java, Node.js and Python a parameter has a name, a type, required, a default and a pattern. type is string, number, boolean, object, array or any. Names bind case-insensitively, defaults apply only when a value is absent, and pattern is checked on strings. If validation fails, the caller receives an error response and your handler is not run.

Class-style handlers#

A handler can also be a class. In .NET this is the only style: handlers are discovered by AddMethodHandlers().

.NET
[MethodName("greet")]
public class GreetMethod : MethodHandlerBase
{
    public string Name { get; set; } = "";

    protected override void Execute() => SetResult(new { message = $"hi {Name}" });
}
Java
public final class GreetHandler extends MethodHandler {
    public String methodName() { return "greet"; }
    public List<ParameterSpec> parameters() {
        return List.of(ParameterSpec.of("name").type(ParameterSpec.Type.STRING).required());
    }
    public Object execute() {
        return Map.of("message", "hi " + requireParam("name"));
    }
}

worker.register(GreetHandler.class);   // needs a public no-arg constructor
Node.js
class GreetHandler extends MethodHandler {
  static override methodName = "greet";
  static override parameters = [{ name: "name", type: "string" as const, required: true }];
  override async execute() {
    return { message: `hi ${this.requireParam("name")}` };
  }
}

worker.register(GreetHandler);
Python
class GreetHandler(MethodHandler):
    method_name = "greet"
    parameters = [ParameterSpec("name", type="string", required=True)]
    async def execute(self):
        return {"message": f"hi {self.require_param('name')}"}

worker.register(GreetHandler)

Send progress#

While a handler runs it can emit feedback, which is stored and can be queried later with kiwi distro list-feedback:

.NET
[MethodName("progress")]
public class ProgressMethod : MethodHandlerBase
{
    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        await Feedback.SendStartNotificationAsync(cancellationToken: cancellationToken);
        for (var pct = 20; pct <= 100; pct += 20)
        {
            await Feedback.SendCompletionPercentageAsync(pct, cancellationToken: cancellationToken);
        }
        await Feedback.SendCompletionNotificationAsync(cancellationToken);

        SetResult(new { done = true });
    }
}
Java
worker.method("progress", ctx -> {
    ctx.feedback().sendStartNotification();
    for (int i = 1; i <= 5; i++) ctx.feedback().sendCompletionPercentage(i * 20);
    ctx.feedback().sendCompletionNotification();
    return Map.of("done", true);
});
Node.js
worker.method("progress", async (ctx) => {
  await ctx.feedback.sendStartNotification();
  for (let i = 1; i <= 5; i++) {
    await ctx.feedback.sendCompletionPercentage(i * 20);
  }
  await ctx.feedback.sendCompletionNotification();
  return { done: true };
});
Python
async def progress(ctx):
    await ctx.feedback.send_start_notification()
    for i in range(1, 6):
        await ctx.feedback.send_completion_percentage(i * 20)
    await ctx.feedback.send_completion_notification()
    return {"done": True}

Feedback types: start notification, periodic status, completion percentage (0 to 100), completion notification, event log, error report and custom (an object). Feedback is fire-and-forget: a failed feedback post never fails the method.

Settings and secrets#

Secrets attached to a service (kiwi distro add-service-secrets) are delivered to the worker at registration as application settings. Read them inside handlers, never at import or start-up, because they do not exist until the worker has registered:

.NET
// inside a MethodHandlerBase handler (or inject IWorkerApplicationSettings elsewhere)
var apiKey = Settings.GetRequired("api-key");
Java
String apiKey = ctx.requireSetting("api-key");
Node.js
const apiKey = ctx.requireSetting("api-key");
Python
api_key = ctx.require_setting("api-key")

Values are a live view, so a rotated secret is picked up after re-registration. In .NET you can also await settings.WhenReady outside a handler.

Choose a delivery mode#

Work is always pulled: the middleware only signals that work exists.

execution_mode How the worker learns of work Use when
eager (default) A hanging GET /worker/messages?wait=N long-poll (long_poll_wait, default 25 s) The worker has no inbound port, or sits behind a load balancer. The balancer's idle timeout must exceed long_poll_wait
lazy One pull at start-up, then a TCP wake-up probe on wakeup_probe_service_port The middleware can reach the worker's ip:port directly
connected A persistent SignalR connection .NET workers; needs session affinity and WebSocket support

Configuration#

Settings resolve, lowest to highest precedence: appsettings.json, KIWI_-prefixed environment variables, --key value command-line arguments, then options passed in code. Required: auth_secret and middleware_url. See Worker and client settings.

Give workers tags ({"os": "linux", "group": "infra"}) so clients can route to a subset of workers with filters.

Concurrency#

Each SDK bounds concurrent requests (.NET defaults to a value derived from the CPU count; Java, Node.js and Python default to 32). See Capacity planning.

Shut down gracefully#

On SIGINT or SIGTERM the worker notifies the middleware, finishes in-flight requests and exits. run_forever() handles this for you.

Expected result

  • A worker registers, handles requests with validated parameters and reports progress