Workflows is a Rust library crate (not a hosted service; the crate name on crates.io/GitHub is tinyflows) that models an automation as a WorkflowGraph: a directed graph of typed nodes and edges. You build or generate that graph, it gets structurally validated, compiled into an opaque CompiledWorkflow, and lowered — once per run — onto tinyagents, a state-graph execution engine, via engine::run.
model::WorkflowGraph -> validate -> compiler::compile -> engine::run
(typed graph) (structural) (validated handle) (lowers onto
tinyagents,
drives to done)
Run state is a single JSON value shaped like { "run": { "trigger": … }, "nodes": { "": { "items": [ … ] } } }. A merge reducer folds each node's output under its own id, so independent branches never collide — which is what keeps parallel fan-out deterministic.
The node catalog
Kind
What it does
trigger
Entry node that starts the workflow (exactly one per graph); firing mode is host-driven
agent
Runs an LLM agent turn, with optional chat-model / memory / tool / output-parser sub-ports
tool_call
Invokes one specific integration action deterministically, no LLM involved
http_request
Outbound HTTP request
code
Sandboxed user code (JavaScript or Python)
output_parser
Parses/validates an upstream agent's output into a structured shape
sub_workflow
Runs another workflow as a nested sub-graph and returns its output
condition
Two-way IF, emits on true/false
switch
Multi-way branch keyed by an expression result
merge
Fan-in barrier — waits for every wired predecessor before running
split_out
Fan-out — emits one item per element of a list
transform
Pure, expression-based field mapping over the run state
Data flows between nodes as arrays of items shaped { json, binary?, paired_item? } — closer to n8n's item-based model than a plain function-composition DAG — and node config can reference the run scope with =-prefixed expressions like =item.name.
The part I actually want to talk about: host-agnosticism
Every place this engine would normally have to make a decision about who it's talking to — which LLM, which integration provider, how HTTP requests actually go out, where state gets persisted — is instead a Rust trait the embedding application implements:
LlmProvider
ToolInvoker
HttpClient
CodeRunner
StateStore
The crate itself never hard-codes a vendor and never makes a real network call on its own. For testing (and for the hello_workflow example below), a mock cargo feature ships deterministic in-memory implementations of all five traits.
This matters for two reasons beyond the obvious "don't vendor-lock the library" one. First, credentials never touch the crate at all — connections are opaque connection_ref values that only the host resolves into real secrets, so a bug in Workflows can't leak a token it never had. Second, it means the exact same compiled workflow can run against mocks in CI and against real capabilities in production, with no branching logic in the workflow definition itself.
Human-in-the-loop as a primitive, not a callback you bolt on
A node can be marked requires_approval. When the engine hits one, the run pauses and shows up in RunOutcome::pending_approvals — the host can surface that however it wants (a UI diff, a Slack message, whatever), and engine::resume picks the run back up once a human approves it.
We built this because "the agent drafts it, a person approves it, then it actually sends" is the whole trust model of the product feature this powers — it needed to be a real pause-and-resume in the execution model, not something the host application fakes by re-running the workflow from the top.
Reliability, per node
on_error policy: stop, continue, or route a failure to a dedicated error output port, so you can build an actual recovery sub-graph instead of just failing the run.
Bounded retry.
Observability via tracing, plus a RunObserver hook and Run/ExecutionStep records if you want to build your own run inspector on top.
Try it
[dependencies]
tinyflows = "0.1"
use serde_json::{Value, json};
use tinyflows::caps::mock::mock_capabilities;
use tinyflows::compiler::compile;
use tinyflows::engine::run;
use tinyflows::model::{Edge, Node, NodeKind, WorkflowGraph};
[tokio::main(flavor = "current_thread")]
async fn main() {
let graph = WorkflowGraph {
nodes: vec![
Node {
id: "t".into(),
kind: NodeKind::Trigger,
type_version: 1,
name: "start".into(),
config: Value::Null,
ports: vec![],
position: None,
},
Node {
id: "greet".into(),
kind: NodeKind::Transform,
type_version: 1,
name: "greet".into(),
config: json!({ "set": { "greeting": "=item.name" } }),
ports: vec![],
position: None,
},
],
edges: vec![Edge {
from_node: "t".into(),
from_port: "main".into(),
to_node: "greet".into(),
to_port: "main".into(),
}],
..Default::default()
};
let compiled = compile(&graph).expect("compile");
let outcome = run(&compiled, json!({ "name": "Ada" }), &mock_capabilities())
.await
.expect("run");
println!("{}", serde_json::to_string_pretty(&outcome.output).unwrap());
}
That's the hello_workflow example in the repo — cargo run --example hello_workflow --features mock.
What's done, and what honestly isn't
Done (Phase A): the full node catalog, structural validation, per-run compilation, item-based data flow with =-expressions, linear/conditional/parallel-fan-out/merge-barrier routing, per-node error handling, human-in-the-loop approval gating, tracing observability, opaque credential references, and a versioned wire format (schema_version + per-node type_version) with a migrate framework for upgrades. It's #![forbid(unsafe_code)], MSRV 1.85, Rust 2024 edition, and runs end-to-end against the mock capabilities behind a reference-workflow e2e suite.
Not yet, and we'd rather say so than let you find out the hard way:
A real jq/jaq-style expression engine — right now a minimal dotted-path evaluator handles the =item.name style expressions, as an interim.
Retry backoff timing and per-node timeouts.
Durable, checkpointed super-step replay — resume currently re-executes deterministically rather than resuming from a saved mid-run checkpoint.
Visual and agent-first authoring tools (that's a host-side concern — it's what the OpenHuman product layer does on top of this).
The OpenHuman host integration itself — the piece that actually wires this engine to real LLMs and real integrations — is Phase B and lives in a separate repo that isn't public yet.
License
GPL-3.0-or-later. Worth knowing up front if you're evaluating it for something you plan to ship closed-source.
If you build workflow/automation engines, or you're the kind of person who has opinions about where the "capability trait" boundary should sit in a system like this, I'd genuinely like to hear where you think we got it wrong: Workflows on GitHub.
Top comments (0)