Bloccs

Typed, supervised dataflow on the BEAM.

You describe a graph of processing stages as TOML; bloccs type-checks the wiring, enforces what each stage is allowed to do, and compiles it to a Broadway/GenStage supervision tree.

⚠️ Experimental — v0.1, pre-release. bloccs is early and moving fast: the public API and the manifest format may change between releases, it isn't on Hex yet, and it isn't durable (put Oban or a broker at the edges for work that must survive restarts). Great for exploring and prototyping — pin a version and check the CHANGELOG before you upgrade.

The events example — a webhook processor: ingest → validate → enrich → route, fanning out to persist and notify, dead-lettering unknown event types

Why bloccs?

Plenty of backend work is a graph of stages: ingest → validate → enrich → branch → persist/notify. On the BEAM you'd build that today by hand-wiring a few Broadway pipelines and a supervisor. That works, but nothing checks it:

bloccs makes the graph a declared, checked thing. You write the topology and the per-stage contracts as TOML; bloccs rejects a graph whose edges don't type-match before it runs, refuses an effect a node didn't declare, and generates the Broadway supervision tree — as real, debuggable .ex source — with the retry / timeout / idempotency / back-pressure already wired in. The artifact you review is the manifest, not the generated glue — which is what keeps a human in control when an LLM is doing the typing (see Legible to humans and LLMs).

Why not just write Elixir, or use Broadway directly? You can — and for a single pipeline you should (see How it compares). bloccs earns its keep once you have a graph and want the edges (schemas), the effects (capabilities), and the per-node policy declared and machine-checked rather than maintained by hand. The next section shows the two checks that aren't possible with plain Broadway.

Install

def deps do
[{:bloccs, "~> 0.1"}]
end

bloccs is not on Hex yet (see Status); until the first release, depend on it from git: {:bloccs, github: "Bloccs/bloccs"}. The effect adapters are bring-your-own and opt-in — add {:req, "~> 0.5"} for the real HTTP backend and point the DB backend at your Ecto repo only if you use them (see guides/effect-adapters.md).

Quickstart

With the dep in place, the loop is validate → compile → run over a network manifest. Point the tasks at a .bloccs network — write one (see guides/concepts.md and the manifest reference) or copy networks/ + nodes/ from examples/tour:

mix bloccs.validate networks/hello.bloccs
mix bloccs.compile networks/hello.bloccs
mix bloccs.run networks/hello.bloccs --message '{"name": "ada"}'

mix bloccs.compile emits real, debuggable .ex source under _build/<env>/bloccs_generated/<network>/ — diff it in a PR. For a step-by-step walkthrough that writes a node by hand, see guides/getting-started.md.

Start a new project from scratch

To begin from a runnable example instead, install the bloccs.new generator as a Mix archive and scaffold a project — it wires the dep, a sample node and its schemas, and a one-node network:

mix archive.install hex bloccs # makes `mix bloccs.new` available
mix bloccs.new my_flow
cd my_flow
mix deps.get
mix bloccs.run networks/hello.bloccs --message '{"name": "ada"}'

Typed edges and scoped effects

This is the part you can't get from hand-wired Elixir. Both examples below are real output from the tour example (examples/tour).

1. Mismatched edges are rejected at validation time.examples/tour/networks/pipeline_mismatch.bloccs wires an out-port carrying WordRequest@1 into an in-port that expects Definition@1. Validate it:

$ cd examples/tour
$ mix bloccs.validate networks/pipeline_mismatch.bloccs
networks/pipeline_mismatch.bloccs
- [[edges]] (networks/pipeline_mismatch.bloccs): edge accept.word define.word
schema mismatch: accept.word=WordRequest@1 but define.word=Definition@1

The network never starts. Plain Broadway has no contract between pipelines — this mismatch would surface as a FunctionClauseError (or worse, silently wrong data) at 3am.

2. Undeclared side effects are refused. A node may only touch the world through effects it declared in [effects]. define declares HTTP to exactly one host:

[effects]
http = { allow = ["dictionary.local"], methods = ["GET"] }

Call anything else and the capability struct refuses it at runtime:

# a host the node didn't allow:
bloccs effect denied: :httphost of http://evil.example/leak
not in declared allowlist ["dictionary.local"]
# an effect axis the node never declared at all:
bloccs effect denied: :httpaxis not declared

And use Bloccs.Node warns you at compile time the moment the effect shell reaches for an undeclared axis:

warning: bloccs: effect :http used in effect_shell but not declared in
[effects]. Declared: [].

Typed edges and capability-scoped effects are the two guarantees that make a bloccs graph safer than the same graph hand-wired.

The manifests

Two kinds of TOML manifest:

# nodes/enrich.bloccs
[node]
id = "enrich"
version = "0.1.0"
kind = "transform"
[ports.in]
valid = { schema = "RawEvent@1" }
[ports.out]
enriched = { schema = "EnrichedEvent@1" }
[effects]
http = { allow = ["enrichment.local"], methods = ["GET"] }
[contract]
pure_core = "MyApp.Nodes.Enrich.transform/2"
effect_shell = "MyApp.Nodes.Enrich.execute/2"
retry = { strategy = "exponential", max = 2, on = ["timeout"], base_ms = 50 }
timeout_ms = 3000
idempotency = { key = "id" }

Each node's implementation is split in two: a pure core (no IO, no clock, no randomness — easy to test, deterministic) and an effect shell that touches the world, and only through declared effects.

How it works

What the compiler emits from those manifests, and how each node behaves once it's running.

Flow primitives

A node's behavior is its ports + contract, not a fixed "type". From those, the common dataflow shapes fall out — most are just what the effect shell returns, the rest a small manifest block:

primitivehow
transformpure_core computes; shell emits one message
filtershell returns :drop — consume, emit nothing
split / fan-outshell returns {:emit, [{port, payload}, …]}, or one out-port wired to many in-ports
route / brancha router shell picks which out-port to emit on
merge (fan-in)several edges into one in-port
aggregate / window[batch]pure_core reduces the batch (count or time window)
join[join] — correlate two+ typed in-ports by a key
throttle / delay[rate] / [delay]

The bloccs notation: hexagon glyphs for each primitive — node, source, sink, split, merge, filter, batch, join, throttle, delay, subgraph — where a node that declares an effect carries a badge

Conditional logic lives in node code (a returned port, a :drop), never in edge predicates — which keeps the topology declarative and machine-checkable. The primitives reference catalogues each one (glyph, what it does, how to declare it); see also guides/concepts.md and the manifest reference.

The generated runtime

From file to running system: the .bloccs manifest is the source of truth, the diagram is a view of it, and the Broadway/OTP supervision tree is the compiled output

mix bloccs.compile reads the manifests and emits a Broadway-based supervision tree — one stage per node, a router for edges, a producer for each inbound port. Every declared contract is wired into that runtime, not just parsed:

Effects: mock by default, real adapters opt-in

Nodes call a backend-agnostic facade (Bloccs.Effects.HTTP.post/3, DB.insert/3, …). The mock backends are the default (tests stay hermetic); the real ones are selected purely in config, with no node changes:

config :bloccs, :effect_backends,
http: Bloccs.Effects.HTTP.Req, # real HTTP via Req (add :req to your deps)
db: Bloccs.Effects.DB.Ecto # real SQL via your Ecto repo
config :bloccs, Bloccs.Effects.DB.Ecto, repo: MyApp.Repo, returning: [:id]

bloccs forces neither req nor Ecto on you — both are bring-your-own. Write your own backend by implementing the Bloccs.Effects.HTTP / DB behaviour. See guides/effect-adapters.md. A runnable example hitting real HTTP + SQLite (zero external services) lives in examples/real_backend.

CLI (Mix tasks)

Legible to humans and LLMs

A bloccs network is a small declarative artifact: the topology is a TOML file, and each node is a manifest plus two functions (a pure core and an effect shell). The thing you review stays small even when an assistant writes most of the implementation.

So you can hand the repetitive parts to an assistant and stay in control of the shape of the system, instead of drowning in generated code you have to audit line by line.

Examples

A graded ladder under examples/ — see examples/README.md:

How it compares

bloccs generates Broadway — it isn't a competitor to it. Reach for Broadway directly when you have a single pipeline; reach for bloccs when you have a graph of typed stages and want the edges (schemas), effects (capabilities), and per-node policy (retry / timeout / idempotency / back-pressure) declared and checked, with the topology as a reviewable artifact. It is not durable — for work that must survive restarts, put Oban or a broker at the edges. Full comparison against Broadway, Oban, Reactor, Temporal, LangGraph, and plain GenServers: guides/comparison.md.

Prior art

Flow-based programming on the BEAM isn't new. Anton Mishchuk's lineage — FlowexALFStrom / Kraken — spent years establishing that the actor model and GenStage make the BEAM a natural home for FBP, and Kraken reached the same "graph-as-data + declarative adapters" idea bloccs builds on. bloccs owes that groundwork.

The difference is the guarantees, not the shape:

What bloccs is not: a runtime DSL (the manifest is compiled ahead of time, not interpreted), a data-ingestion pipeline (that's Broadway — which bloccs generates), a visual builder (the file is canonical; a canvas is a view), or durable (put Oban or a broker at the edges for work that must survive restarts).

Status

v0.1 — pre-release. Not yet on Hex; APIs may change before the first release. The full parse → validate → compile → run loop works end to end, and everything the manifests can declare is honored at runtime:

See guides/ARCHITECTURE.md for the compile pipeline. The deferred items are listed under Out of scope.

A note on the name. In the design docs you'll see bloccs called an IR for Agentic Computation Graphs (ACGs) — the north star is typed, supervised graphs with LLM/tool nodes as first-class stages. Today the engine is the general-purpose, agent-agnostic core: typed dataflow on the BEAM. The "agentic" layer is a roadmap direction, not a v0.1 claim — every example here is ordinary backend dataflow.

Out of scope for v0.1

Phoenix LiveView canvas (v0.3+) · MCP server (v0.2) · Pro / encrypted packages (private repo, post-v0.1) · polyglot pure_core · cyclic networks (DAG-only; feedback loops are a deadlock-safety design still on the roadmap) · escript-packaged binary · full Dialyzer-level static effect proof · durable persistence.

License

MIT. Pro extensions ship later in a separate private repo (modeled on Oban Pro).