brecabral
From Pipes to Graphs: Could Elixir Make Control Flow More Composable?
Recently, while studying LangGraph, and particularly the evolution from LangChain’s linear chains toward graph-based workflows, I started wondering: why couldn’t Elixir’s pipe model evolve from linear pipelines into a more general graph-based control-flow abstraction?
Elixir already has one of the most readable ways to express sequential transformations through |>. But when we need to branch, choose the next function dynamically, or revisit an earlier step, we generally move that control flow outside the pipeline.
I started exploring whether we could express these relationships while preserving the simplicity of pipes.
I know this has been discussed before
While researching, I found previous proposals to extend Elixir’s pipe operators, including comments from José Valim explaining that keeping the pipe simple is an intentional design decision.
I understand and respect that reasoning, and I don’t want to be the person bringing yet another pipe proposal without acknowledging the previous discussions. ![]()
Still, I think recent developments in software architecture make this question worth revisiting from a slightly different perspective.
Why now?
I believe we’re moving toward a software ecosystem where LLMs increasingly become components of existing applications.
Unlike traditional linear pipelines, agentic workflows frequently involve cycles:
- Execute an LLM.
- Decide whether to call a tool or produce a response.
- Execute the selected tool.
- Return to the LLM with the result.
- Repeat until a final response is produced.
LangGraph addresses this using explicit graph structures.
But Elixir already has several characteristics that seem particularly suitable for these systems: the BEAM, OTP, concurrency, fault tolerance, message passing, and supervision.
What if a more expressive pipeline syntax could complement those capabilities?
I suspect that making graph-based control flow as natural as sequential piping could become a significant advantage for Elixir in AI applications.
The experiment
I created a small Repository exploring two concepts:
- Branching: choose which function receives the current value.
- Revisiting nodes: return to a named execution step, creating cycles.
The working prototype already demonstrates conditional branching using custom operators:
start
|> llm(tools)
<~> [fn args -> use_tool(args, tools) end, &reply/1]
The LLM function returns a tagged value indicating which branch should execute. The cycle still relies on ordinary recursion, so this is not yet a graph execution engine. The next step would be making that cycle explicit in the composition itself.
For example, imagine something like this:
start
|> node(:decision, &decide/1)
<~> [
tool: {&use_tool/1, next: :decision},
done: {&prepare_reply/1, next: :exit}
]
|> node(:reply, &reply/1)
This is pseudocode, not valid Elixir.
Here, a tool call could return to the named :decision node, while :exit would leave the cycle and continue through the remaining pipeline.
The exact operators are not the important part. I’m interested in whether the underlying abstraction could be made simple and composable.
Beyond LLMs
Although LangGraph inspired this experiment, I don’t think the abstraction should be designed specifically for AI.
My intuition is that graphs are a natural mathematical extension of functional composition.
A pipeline describes a sequence of transformations. A graph generalizes that relationship by allowing multiple possible successors and cycles.
Ideally, the design should emerge from graph theory and functional programming principles, with LLM agents being just one application.
I also wonder whether Elixir is particularly well positioned to explore this because its existing pipe syntax already provides a readable foundation for composing transformations.
Existing work in the Elixir ecosystem
I’m aware of projects such as Reactor, which already provide graph-based workflow orchestration in Elixir.
However, what I’m particularly interested in exploring is whether branching and cyclic control flow could become as natural to express as sequential composition with |>.
Rather than introducing another workflow engine from scratch, I’m curious about whether a small, general-purpose abstraction could emerge and potentially establish a common pattern across the ecosystem.
Questions for the community
There are three questions I’d particularly like to discuss:
-
Could branching and return flows be expressed more simply, potentially establishing a common pattern in the Elixir ecosystem, even without changing the language syntax?
-
Would extending the pipe concept toward more general graph-based control flow really introduce that much complexity? I understand the argument for keeping pipes simple, but I’m curious about the actual trade-offs and limitations.
-
Are there existing discussions, projects, or experiments exploring something similar? I’d be interested in learning what has already been attempted and what challenges were encountered.
Open to collaboration
I’ve started experimenting with these ideas in a small repository, but this is still an early exploration.
I’m not a senior Elixir developer, and I certainly don’t claim to have found the right abstraction or syntax. There may be important language design constraints I’m overlooking.
If anyone finds the idea interesting and would like to contribute, experiment with the syntax, or even collaborate on building a library around it, I’d be happy to participate.
Building something along the lines of a LangGraph for Elixir, but with an API that feels natural to functional programming and Elixir’s pipe-oriented style, seems like an interesting project to explore.
Even if the conclusion is that this should remain a library rather than become part of the language, I think the experiment could still be worthwhile.
I’d appreciate any feedback, references, criticism, or suggestions.
Trending in AI / LLMs
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #ai
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #security
- #metaprogramming










Showing Posts 1 to 4- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
MrDoops
Runic is more or less what you describe - it’s a project I’ve been working on for years and is very usable now but I’ll market it more once its ready for a
0.1.0release.I modeled nested branching trees as data with tuples and lists nesting for parent/child dependencies.
So you’d model out the programs as Runic components then connect them together by name: Runic Cheatsheet — Runic v0.1.0-alpha.11
Because functions are just data, and workflows are composed of many functions as components you can build them at runtime and support low code graph GUIs or in more cases now for agents to dynamically compose workflows at runtime.
brecabral
Thanks for sharing Runic! I wasn’t aware of it when I started experimenting with this idea.
The ability to compose workflows dynamically is particularly interesting.
I think the main difference in what I’m exploring is the syntax and ergonomics: whether branching and transitions back to named nodes could be expressed as naturally as sequential composition with
|>, without explicitly constructing edges.I’m also curious: does Runic currently support cyclic workflows, where execution can return to an earlier node based on a condition?
I’ll take a closer look at the project. Thanks again for pointing me to it!
brecabral
I also came across LangEx, which seems like a well-designed LangGraph-style library for Elixir.
It already covers conditional branching, although I haven’t found the kind of explicit back-transition syntax I was searching.
But maybe keeping the loop inside a recursive function is actually the simpler approach, preserving Elixir’s readability rather than introducing additional control-flow syntax.
That’s something I’m reconsidering as I explore these existing projects.
MrDoops
Loops inside Runic are done via pattern matching and taking an output from the DAG (no cycles via dataflow edges) and running it through the root (top of the graph) again. Then a conditional can pattern match on it like you would a recursive function in normal elixir. DAGs have a lot of good properties like knowing it will terminate and preventing infinite loops when its a dataflow context so its a useful model to have when a loop can always just be controlled from the outside if you want to do that. A good pattern is to have a Genserver keep a Runic Workflow in its state and manage the loop statefully.
I’ve modeled a lot of LLM workflows in Runic mostly using ReqLLM with Runic and it can do anything like LangGraph or LangChain might for composing agents with tools, or more agents, routing, and conventional actions. Usually modeling the conversation in a
Runic.accumulatorand using Rules with conditions for the accumulator state to do control flow based on the conversation for things like turns.E.g. mermaid diagram of a LLM workflow
The useful concept is that you have common data contracts around conversation state, tool calls, llms, etc and then various Runic components using the common data contracts can be connected to each-other dynamically depending on what the situation requires (single agent, multi-agent, agent + tools, compaction, context retrieval, memory sources, etc). I’d keep an eye on the next version of Jido Action if you’re looking for a solution like this.