Go & Python clients§
Wiggle's wire protocol is language-neutral gRPC, and step binding is by activity name over the graph — so the same workflow's steps can be served by Java, Go, and Python workers simultaneously. One instance, three languages, no shared SDK semantics beyond the proto.
Both clients are idiomatic, not transliterations of the Java API. Both live in their own repositories with their own examples and test suites:
Go§
Workers are structs; the handler surface is explicit interfaces (Activity, ItemActivity for
forEach bodies) — no reflection magic beyond name matching, no goroutine-local state:
type OrderHandlers struct{}
func (OrderHandlers) Workflow() string { return "order-fulfilment" }
func (OrderHandlers) Validate(ctx wiggle.Context) (wiggle.Context, error) {
ctx["status"] = "VALIDATED"
return ctx, nil // the return REPLACES the context
}
func (OrderHandlers) InStock(ctx wiggle.Context) (bool, error) { // a gate
return ctx["quantity"].(float64) > 0, nil
}
// forEach body: the element is the item's context; base comes in explicitly
func (OrderHandlers) Price(base wiggle.Context, item any) (any, error) {
return price(base, item), nil
}
w := wiggle.NewWorker(client, "worker-go",
wiggle.Register(orderBlueprint), wiggle.Handlers(OrderHandlers{}))
w.Start(ctx)
Python§
Handler classes are plain objects — public methods are steps, matched by case-folded name; the
return annotation picks the step kind (-> bool = gate, -> None = effect, anything else a
task whose return replaces the context):
class OrderHandlers:
workflow = "order-fulfilment"
def validate(self, ctx):
return {**ctx, "status": "VALIDATED"} # replaces the context
def in_stock(self, ctx) -> bool: # a gate
return ctx["quantity"] > 0
def price(self, item): # forEach body: the element IS the context
base = wiggle.step.base() # frozen pre-loop context, read-only
return base["rate"] * item["amount"]
def collect(self, ctx) -> dict: # the explicit combine
return {**wiggle.step.base(), "priced": ctx["charge-items"]}
worker = wiggle.Worker(client, "worker-py")
worker.register(order_blueprint, OrderHandlers())
worker.start()
Shared semantics§
All three clients honour the same contracts, because the contracts live in the graph and the engine, not the SDK:
- a task's return replaces the context (
None/nullleaves it untouched); - fork branches are isolated; the combine is mandatory and explicit;
forEachmaps elements — the element is the item's context; results collect into a list/map/set shaped like the input, handed to the combine;- gates end the instance cleanly on
false; effects never touch the context; - retries, leases, timers, and signals are engine-side and identical everywhere.