| Crates.io | flowlang |
| lib.rs | flowlang |
| version | 0.3.27 |
| created_at | 2022-06-28 14:54:52.071438+00 |
| updated_at | 2025-07-03 10:15:55.484379+00 |
| description | A dataflow oriented programming meta-language in JSON supporting functions written in rust, python, javascript, java, and flow. |
| homepage | https://github.com/mraiser/flow |
| repository | https://github.com/mraiser/flow |
| max_upload_size | |
| id | 614890 |
| size | 632,016 |
NOTE: Support for back-end commands written in Java and Javascript is kind of broken for now. Contact me if you'd like to help fix it.
Flowlang is a Rust implementation of the Flow language, a dataflow-oriented programming language designed for visual "flow" diagrams. The crate's primary purpose is to execute Flow programs (defined in JSON) and provide a unified function-call interface across multiple programming languages, including Rust, Python, JavaScript, and Java. In essence, Flowlang acts as an interpreter and runtime for Flow programs, allowing developers to construct programs as dataflow graphs and run them seamlessly. This addresses the problem of orchestrating complex logic in a visual, data-driven manner, and integrating code written in different languages into one workflow.
Its multi-language support and inherent dataflow paradigm make Flowlang particularly well-suited for building and orchestrating Large Language Model (LLM) based tools and agents. Developers can seamlessly integrate Python scripts for LLM interactions, Rust for performance-critical tasks, and JavaScript for other utilities, all within a unified visual workflow. The flowmcp binary further enhances this by providing direct support for the Model Control Protocol.
A Flow program is represented as a directed graph of operations ("commands") where data flows along connections between nodes. The Flow language is loosely based on Prograph, a visual dataflow language. Using an IDE like Newbound, a developer draws a diagram of how data moves through functions and conditions; Flowlang then executes this diagram by passing data through the graph. Each node (operation) processes inputs and produces outputs that feed into other nodes. The Flowlang crate essentially interprets the JSON representation of such a diagram, allowing it to run as a program.
One of Flowlang's distinctive features is multi-language support. It provides a unified functional API so that "Flow commands" (nodes in the flow graph) can be implemented not only in Flow's own visual language but also in Rust, Python, JavaScript, or Java. This means developers can write certain nodes as native Rust functions, or as Python/JS scripts, etc., and integrate them into the dataflow. The Flowlang runtime handles calling out to the correct language runtime and feeding data in/out, which simplifies building heterogeneous systems. All these languages maintain state between calls, so for example the Python interpreter or JavaScript engine isn't re-initialized on every use, enabling persistent stateful behavior across multiple calls.
Relation to ndata: The Flowlang crate is built on top of the companion crate ndata, which provides the dynamic data structures used to represent and pass data between flow nodes. ndata defines types like Data, DataObject, and DataArray that behave similarly to loosely-typed JSON values or Python objects. These can hold numbers, strings, booleans, nested objects/arrays, etc., and are used as the universal data container in Flowlang. Crucially, ndata implements an internal heap with reference counting and garbage collection. This allows Flowlang to create and pass around dynamic data (e.g., the input and output parameters to commands) without worrying about Rust's strict ownership rules---much like a garbage-collected language. In practice, every input or output in a flow is a DataObject (a JSON-like map of keys to Data values) that can be freely shared across threads and languages. The Flowlang runtime leverages ndata so that data flows smoothly through the graph, regardless of which language produced or consumes it. This design choice makes Flowlang thread-safe by design as ndata's objects use internal reference counts and locks so they can be sent between threads without explicit Arc wrappers. In summary, Flowlang's core functionality is enabling dataflow programming (especially visual programming via Newbound) and seamless multi-language function integration, built atop a dynamic data model provided by ndata. This empowers rapid prototyping and cross-language development by abstracting away memory management and language interop complexities.
With the rise of Large Language Models (LLMs), the need for robust and flexible tooling to orchestrate LLM interactions, chain prompts, manage state, and integrate with various APIs has become paramount. Flowlang, with its inherent strengths, is exceptionally positioned as the best vehicle for rolling your own LLM tools and agents, especially with the introduction of Model Control Protocol (MCP) support via the flowmcp binary.
What is Model Control Protocol (MCP)? MCP provides a standardized way for applications to communicate with and control AI models or agents. It involves sending structured requests (often JSON-RPC) to a model endpoint and receiving structured responses. This allows for complex interactions beyond simple prompt-response, including managing context, controlling model parameters, and invoking specific agent capabilities.
Introducing flowmcp**:** The flowmcp binary in Flowlang is a dedicated executable that implements an MCP server. It listens for JSON-RPC messages over stdin, processes them using the Flowlang engine, and sends responses back via stdout. This allows external systems or interfaces to interact with Flowlang-defined workflows as if they were language models or intelligent agents.
Why Flowlang is Ideal for LLM Tooling:
Seamless Multi-Language Integration:
Visual Dataflow Programming for Complex Chains:
Flexible Data Handling with ndata**:**
State Management:
Rapid Prototyping and Iteration:
Exposing LLM Tools as Services:
Example Use Case: A Research Agent Flow Imagine an LLM agent that takes a research query, searches the web, summarizes relevant articles, and generates a report. In Flowlang:
By leveraging Flowlang and flowmcp, developers can build powerful, modular, and maintainable LLM-powered applications with greater ease and clarity than traditional scripting approaches.
Despite being implemented in Rust, Flowlang adopts many techniques more common in dynamic or functional languages. Key Rust technologies and design choices include:
Dynamic Data with Manual GC: Flowlang uses the ndata crate to manage data dynamically. ndata internally uses a global heap and manual garbage collection---unusual for Rust, but deliberate here to allow more flexibility. All DataObject and DataArray instances carry their own reference counts, and memory is only freed when a GC function is explicitly called. This means Flowlang can store cyclic or cross-scope data (e.g., global state or interconnected node outputs) without immediate ownership issues. The trade-off is that the programmer (or the runtime) must periodically invoke DataStore::gc() (which calls NData::gc()) to clean up unused values. This design restores some of the "garbage-collected language" convenience inside Rust's safe environment, at the cost of forgoing Rust's usual compile-time memory guarantees. It's a conscious choice to make Flowlang suitable for rapid prototyping and multi-language interop. In practice, when writing Rust code that uses Flowlang, do not wrap Flow data in additional Arc or Mutex---ndata already handles thread-safe reference counting internally. A common mistake is to put Data or DataObject inside an Arc; this is unnecessary and could lead to memory never being freed (as ndata's GC would not see the data as collectable). Instead, rely on Flowlang/ndata's own memory model and simply call the GC when appropriate (for example, after a batch of flow executions, call DataStore::gc() to reclaim heap storage).
Thread-Safety and Concurrency Model: Flowlang's concurrency model is built around the idea that flows can run in parallel, but individual flow executions are single-threaded by default. The Flow interpreter uses an event-loop style algorithm to evaluate the dataflow graph (detailed in the next section) and does not spawn multiple threads for parallel nodes---instead, it processes nodes whose inputs are ready in sequence. However, because ndata data structures are thread-safe, it is possible to run multiple Flow commands (functions) concurrently in different threads or tasks. For example, two separate Command::execute calls can happen on different threads---the underlying data passing (using DataObject) is protected by atomic reference counts and locks, so data races will not occur. In short, Flowlang itself doesn't automatically parallelize a single flow, but it allows multi-threaded use. The thread safety is achieved without heavy use of Mutex thanks to the internal design of ndata: references to data are coordinated by a custom thread-safe reference counter (SharedMutex in ndata) so that cloning a DataObject just bumps a count and different threads can read/write through it safely. This simplifies concurrent scenarios---manual copying or guarding of flow inputs/outputs to share them is not needed. The Flowlang interpreter loop also uses only safe Rust (no unsafe for concurrency), leaning on the atomic refcounts for synchronization. There is no explicit use of Rust async/await in Flowlang; flows are generally run to completion synchronously via Command::execute. If asynchronous behavior is needed (e.g., waiting on I/O), implement that inside a node (for instance, a Rust node can use tokio internally, or a JavaScript node can await a promise in the embedded engine).
FFI and Language Embedding: Under the hood, Flowlang leverages Rust's FFI capabilities to integrate other language runtimes:
All these integrations highlight Rust's ability to host multiple runtimes simultaneously. Flowlang uses conditional compilation (feature flags) to keep these optional---by default, only pure Flow and Rust are supported, and one compiles with --features=javascript_runtime or others to include JS, Python, or Java support. This modular design keeps the base crate lightweight and lets users opt-in only to the needed language engines.
In summary, Flowlang's architecture is an interesting blend: it sacrifices some of Rust's usual strictness (using a global heap and dynamic typing) to gain flexibility, while still leveraging Rust's strengths in FFI, speed, and safety for multi-language support. The concurrency model is cooperative and data-driven---multiple languages run in the same event loop and thread, unless they are explicitly threaded out. The design emphasizes that data is the primary carrier of state (fitting a dataflow paradigm), and everything from memory management to multi-language calls is built to make passing around DataObject instances simple and safe.
Flowlang is designed to be used as a library within a larger Rust workspace, which acts as the main application host. The recommended project structure involves a top-level binary crate (e.g., newbound) and multiple sub-project library crates (e.g., newbound-core, cmd) that contain the actual flowlang libraries.
As a Binary (CLI Tool): The crate comes with three binaries: flow (the main interpreter), flowb (the builder for Rust/Python commands), and flowmcp (for Model Control Protocol interactions). Obtain these by cloning the GitHub repo and building:
git clone https://github.com/mraiser/flow.git
cd flow
cargo build # builds the flow, flowb, and flowmcp binaries
# (Optionally, copy or symlink the binaries to a directory in your PATH)
sudo ln -s $(pwd)/target/debug/flow /usr/local/bin/flow
sudo ln -s $(pwd)/target/debug/flowb /usr/local/bin/flowb
sudo ln -s $(pwd)/target/debug/flowmcp /usr/local/bin/flowmcp
This compiles the latest code. (For a release build, use cargo build --release and adjust the paths accordingly.) Once built, the flow CLI can execute Flow libraries. By default, it looks for a data directory in the current working directory which contains the flow libraries (JSON files). The repository itself includes a data/ folder with an example library called "testflow".
To run a flow from the command line with flow, use:
flow <library> <control> <command> <<< '<json-input>'
For example, to execute the test_add command in the testflow library:
cd path/to/flow # directory containing 'data' folder
flow testflow testflow test_add <<< '{"a": 300, "b": 120}'
This launches the Flow interpreter, loads the testflow library, and runs the function named test_add with the provided JSON input. The result is printed to stdout as JSON.
To use flowmcp for Model Control Protocol interactions: The flowmcp binary starts a server that listens for JSON-RPC requests on stdin and sends responses to stdout.
# Run flowmcp (it will wait for JSON-RPC requests on stdin)
./target/debug/flowmcp
An external application would then pipe JSON-RPC requests like the following to flowmcp's stdin:
{"jsonrpc": "2.0", "method": "testflow.testflow.test_add", "params": {"a": 5, "b": 7}, "id": 1}
And flowmcp would respond on stdout:
{"jsonrpc":"2.0","result":{"result":12},"id":1}
As a Library in a Rust Workspace:
Workspace Setup: Structure your project as a Cargo workspace. Your top-level Cargo.toml should define the main binary and list all sub-project crates as members.
# In /my_project/Cargo.toml
[package]
name = "my_project_bin"
# ...
[workspace]
members = [
"core-libs",
"command-libs",
"ffi-lib"
]
Build Script (build.rs): Your top-level binary crate must have a build.rs file to ensure the linker can find any FFI sub-projects.
// In /my_project/build.rs
use std::env;
use std::path::PathBuf;
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let profile = env::var("PROFILE").unwrap();
let deps_path = manifest_dir.join("target").join(profile).join("deps");
println!("cargo:rustc-link-search=native={}", deps_path.display());
}
Sub-Project Configuration: Use the "root" field in each library's /data/library_name/meta.json to assign it to a sub-project crate. For example: {"root": "core-libs", ...}
Main Application Logic: Your main.rs initializes the system and calls the single, auto-generated function to register all commands.
// In /my_project/src/main.rs
mod generated_initializer;
fn main() {
// Initialize the Flow runtime once.
flowlang::init("data");
// Register all commands from all sub-projects.
generated_initializer::initialize_all_commands();
// ... your application logic ...
}
Build Process: Run flowb (or flowb rebuild) from your project root. The build script will automatically:
HTTP Service Usage: Flowlang has a built-in mini HTTP server that can expose flow commands as web endpoints.
flow flowlang http listen <<< '{"socket_address": "127.0.0.1:7878", ...}'
This command starts an HTTP listener. Any Flow command can then be invoked via an HTTP GET request.
Enabling Language Runtimes: If flows include commands written in other languages, compilation must include the corresponding features:
Internally, Flowlang represents a flow as a collection of interconnected components.
Modules Organization: The crate is divided into several modules: datastore, command, code, case, primitives, rustcmd, pycmd, jscmd, javacmd, mcp, buildrust, and various utility modules.
Flow Definition Data Structures: When a flow library (JSON) is loaded, it is parsed into a set of in-memory structs:
Loading and Parsing Flows: init("data") reads the library JSON files from the data directory and builds the in-memory Case structures, registering each as an executable Command.
Interpreter Execution Algorithm: The heart of Flowlang is Code::execute, which runs a Case. It uses a two-phase event loop:
Example: Suppose a flow is needed to compute a * b + c. This can be done by writing a Rust function or assembling a Flow visually. A simplified JSON for such a flow would define inputs a, b, and c, two primitive operations (multiply, add), and connections to wire them together correctly.
Best Practices & Caveats:
Memory Management: Run DataStore::gc() periodically in long-running services to prevent memory bloat, as ndata's garbage collection is manual.
No External Sync Needed: Do not wrap ndata types (DataObject, DataArray, etc.) in Arc or Mutex. They are already internally thread-safe.
Global State: Use DataStore::globals() for state that needs to persist across flow invocations.
Using Multi-Language Commands (especially for LLMs): When writing flow commands in other languages, ensure the initialization steps are followed.
Performance Considerations: Flowlang is optimized for flexibility. For performance-critical logic, implement it as a single native Rust command rather than a large graph of many small operations. Be mindful of the FFI overhead when crossing language boundaries frequently.
Debugging Flows: Set RUST_BACKTRACE=1 for backtraces on panics. Use eprintln! for logging in flowmcp to avoid interfering with JSON-RPC output on stdout.
In conclusion, Flowlang offers a compelling way to design systems by wiring together dataflow components. It stands out by bridging multiple languages in one runtime and by providing Rust developers a dynamic, visual scripting layer. With the addition of flowmcp and its strong suitability for LLM tooling, Flowlang is an increasingly powerful platform for building sophisticated, modern applications.