F-Mesh is a Flow-Based Programming (FBP) framework for Go. You build an app as a graph of small, reusable components. Pipes connect them, and data flows through the graph as signals.
It is inspired by J. Paul Morrison's FBP. The API is small and untyped on purpose: one pipe can carry anything, so you model the flow, not the type graph.
go get github.com/hovsep/fmeshRequires Go 1.27 or later.
A mesh that joins two strings and converts the result to uppercase:
package main
import (
"context"
"fmt"
"strings"
"github.com/hovsep/fmesh"
"github.com/hovsep/fmesh/component"
"github.com/hovsep/fmesh/signal"
)
func must(err error) {
if err != nil {
panic(err)
}
}
func main() {
// Components: named ports plus an activation function
concat, err := component.New("concat",
component.WithInputs("i1", "i2"),
component.WithOutputs("res"),
component.WithActivationFunc(func(_ context.Context, this *component.Component) error {
word1 := this.InputByName("i1").Signals().FirstPayloadOrDefault("")
word2 := this.InputByName("i2").Signals().FirstPayloadOrDefault("")
return this.OutputByName("res").PutSignals(signal.New(word1 + word2))
}))
must(err)
uppercase, err := component.New("uppercase",
component.WithInputs("i1"),
component.WithOutputs("res"),
component.WithActivationFunc(func(_ context.Context, this *component.Component) error {
str := this.InputByName("i1").Signals().FirstPayloadOrDefault("")
return this.OutputByName("res").PutSignals(signal.New(strings.ToUpper(str)))
}))
must(err)
// The mesh holds the components
fm, err := fmesh.New("hello world")
must(err)
must(fm.AddComponents(concat, uppercase))
// A pipe connects an output to an input
must(concat.OutputByName("res").PipeTo(uppercase.InputByName("i1")))
// Seed the inputs, then run until no component has work left
must(concat.InputByName("i1").PutSignals(signal.New("hello ")))
must(concat.InputByName("i2").PutSignals(signal.New("world!")))
_, err = fm.Run(context.Background())
must(err)
result, err := uppercase.OutputByName("res").Signals().FirstPayload()
must(err)
fmt.Printf("Result: %v\n", result) // Result: HELLO WORLD!
}- Components. Build workflows from small, independent, testable blocks.
- Concurrency for free. All components ready in a cycle run at the same time. You write no goroutines, channels or locks.
- Discrete time. Execution runs in cycles, like ticks of a clock. This makes runs easy to reason about.
- Deterministic runs. Same input gives the same output, as long as your activation functions are deterministic.
- Cancellation and deadlines.
Run(ctx)passes the context to every activation function and hook. - Hooks and plugins. Add behavior at mesh, cycle, component and port level.
- Metadata. Tag signals, components and ports with string, bool or number values, then filter and route by them.
- Run report.
Run(ctx)returns aRuntimeInfowith the result of every cycle.
fm.SetupHooks(func(h *fmesh.Hooks) {
h.BeforeRun(func(ctx context.Context, fm *fmesh.FMesh) error {
fmt.Println("Starting mesh...")
return nil
})
h.AfterCycle(func(ctx context.Context, hookCtx *fmesh.CycleContext) error {
fmt.Printf("Cycle #%d complete\n", hookCtx.Cycle.Number())
return nil
})
})Signals carry any. One pipe can hold a string, a struct and an error at once. Go channels cannot
do that. The cost: a type mismatch shows up when you read a payload, not at compile time.
n, err := sig.As[int]() // reports a mismatch; prefer this
n := sig.PayloadOrDefault(0) // hides a mismatch; use only when a fallback is rightA TimeLimit becomes a deadline on the run context. So it reaches the HTTP calls and queries
inside your components.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
component.WithActivationFunc(func(ctx context.Context, this *component.Component) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
...
})
_, err := fm.Run(ctx) // errors.Is(err, fmesh.ErrRunCanceled) after cancel()| Concept | Description |
|---|---|
| Component | The main building block. Has inputs, outputs and an activation function. |
| Port | An input or output of a component. A component can have any number of them. |
| Pipe | Connects an output port to an input port. |
| Signal | A data packet. Carries any payload plus optional metadata. |
| Cycle | One tick of execution. Every ready component activates. |
Each one surprises people once. The wiki covers them in full.
1. A component activates only when an input port has signals. There are no source components.
You start a mesh by putting signals on inputs. If you forget, the run does nothing and returns nil.
_ = producer.InputByName("start").PutSignals(signal.New("go")) // without this, nothing happens2. Order is by name. Signals keep arrival order within a port. Upstream components drain in component-name order. A component's ports are traversed in port-name order. So if a component reads all inputs at once, port names decide the order.
3. Payloads are shared, so treat them as read-only. Fan-out sends the same *Signal to every
destination, and those destinations run concurrently. Never mutate a received map, slice or
pointer; build new signals instead. Run your mesh tests with -race.
4. Cancellation is cooperative. Run(ctx) stops at the next cycle boundary. Go cannot stop a
running goroutine, so an activation function that ignores its context blocks the mesh until it returns.
More: 401. Scheduling rules and 603. Caveats.
- Data pipelines: ETL, data processing, format conversion
- Workflow automation: multi-step business processes
- Computational graphs: scientific computing, simulations
- Game logic: entity systems, behavior trees
- Batch event processing: a bounded set of events, processed to the end
- Prototyping dataflow designs
F-Mesh is not a classical FBP system and not a streaming engine. A mesh runs to completion over the data it was given.
- Not suited to long-running components
- No wall-clock events (timers, tickers)
- Components run in discrete cycles, not in real time
- No backpressure: a mesh holds its signals in memory for the whole run
For real-time streaming, use a classical FBP system or a message queue. Known trade-offs (partial output before a wait, shared payloads, lost work when one component fails) are listed in 603. Caveats.
- Wiki: full guide. Source is in
docs/wiki; edit it by PR. - Examples: working programs.
- API reference
- Flow-Based Programming by J. Paul Morrison
F-Mesh is pre-production. Minor versions may contain breaking changes until this notice is removed. Pin an exact version if that matters to you. CHANGELOG.md lists every breaking change.
Contributions are welcome. Read the contributing guide first.
MIT. See LICENSE.
Made by @hovsep
