Business processes are graphs, not call stacks. Plugfy makes the graph a language construct: declarative, parallel, observable and deterministic.
A module (.plug) is one namespace: types, functions, a decision, and flows — all in one file. Flows import the same libraries and call the same functions:
namespace shop.pricing import System.Collections // List/Map + filter/map/fold/sum/any/all/find import System.Flow // FlowResult<T>, run/toJson/toDot
Each step names a value and the action that produces it. Steps that do not depend on each other belong to the same wave — and pure waves run in parallel, automatically:
flow profile(xs: List<int>) -> Stats { total = sum(xs) // wave 0 — pure, runs in parallel avg = mean(xs) // wave 0 hi = maxOf(xs) // wave 0 => Stats(total: total, avg: avg, hi: hi) // wave 1 — joins the results }
The guarantees:
FlowResult(parallel) == FlowResult(sequential) — byte-identical, pinned by a CI gate.FlowResult.nodes) is identical regardless of which step finished first.Ordinary control flow (if / while / let / var) inside a flow is a sequential barrier — everything before it finishes, the region runs alone, everything after waits. Parallelism is something you can read from the source.
A flow's execution is a structured result, not a log to grep. FlowResult<T> carries the final value plus a per-step trace, exportable as JSON or Graphviz:
$ plugfy explain stats.flow # the inferred parallel waves $ plugfy flow-graph stats.flow --dot # the dependency graph, ready for Graphviz
A failing step short-circuits its dependents and is captured in the flow's result — with the step name, the error value, and the trace of everything that did run. Error handling is part of the graph, not an afterthought: you decide per flow whether a failure aborts, falls back, or is delegated upward as a value.
The flow surface is growing into Conduit — one node grammar for rich orchestration. Planned additions (all additive, no new kernel opcodes):
namespace Acme.Orders import Acme.Inventory as Inv import Acme.Payments as Pay @retry(max: 3) @tag("fulfillment") flow Fulfill(in order: Order, in dryRun: bool = false) -> (out shipment: Shipment) { validate <- Orders.validate(order: order) -> ValidOrder reserve <- Inv.reserve(lines: validate.lines) -> Reservation // reserve and charge share @timeout(30s) charge <- Pay.charge(amount: validate.total) -> Receipt // no data -> run in PARALLEL }
pure marker and explicit after edges.select / sequence / any / for … parallel), a pluggable @join joiner, wave-scoped cooperative cancellation with @timeout, and the closed FlowEvent stream — real-time structured execution events, of which FlowResult<T> is the terminal fold.Stream<T> edges and durable event-log replay.See the roadmap for status.