brecabral

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. :slight_smile:

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:

  1. Execute an LLM.
  2. Decide whether to call a tool or produce a response.
  3. Execute the selected tool.
  4. Return to the LLM with the result.
  5. 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:

  1. Could branching and return flows be expressed more simply, potentially establishing a common pattern in the Elixir ecosystem, even without changing the language syntax?

  2. 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.

  3. 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.

Showing Posts 1 to 4

MrDoops

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.0 release.

I modeled nested branching trees as data with tuples and lists nesting for parent/child dependencies.

workflow = Runic.workflow(
  name: :pipeline,
  steps: [
    {Runic.step(fn x -> x + 1 end, name: :add),
     [Runic.step(fn x -> x * 2 end, name: :double),
      Runic.step(fn x -> x * 3 end, name: :triple)]}
  ]
)

So you’d model out the programs as Runic components then connect them together by name: Runic Cheatsheet — Runic v0.1.0-alpha.11

workflow = Workflow.new()
  |> Workflow.add(step1)                       # Add to root
  |> Workflow.add(step2, to: :step1_name)      # Add as child of named component
  |> Workflow.add(step3, to: step2)            # Add as child of component struct
  |> Workflow.add(join_step, to: [:a, :b])     # Join multiple parents

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

brecabral OP

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

brecabral OP

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

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.accumulator and 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.

— All posts loaded —

Where Next? Top

Trending in AI / LLMs Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
AstonJ
AI tools and services seem to be the new JS framework - with new ones popping up every 5 minutes - hence it might be worth posting one of...
New
preciz
I like to find performance optimizations and hidden bugs in Elixir codebases with coding agents. If I just ask them directly to find tho...
New
spasm-myelixlabs
Hey everyone, We’re a tiny dev team, and we wanted to share something we’ve been building and dogfooding internally for months: Synapse ...
New
caleb-bb
So I’ve got this RAG project called Cake that I’ve been working on a good long while. For this topic, the RAG part is less important than...
New
nathanl
Anthropic: Launching an opt-in vulnerability-finding service for open-source software We’re launching OSS Scanner, an opt-in vulnerabil...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews