Type-safe async workflow orchestration for language models. Zero dependencies, 100% test coverage.
Project description
ClearFlow
Type-safe async workflow orchestration for language models. Explicit routing, immutable state, zero dependencies.
Why ClearFlow?
- Predictable control flow – explicit routes, no hidden magic
- Immutable, typed state – frozen state passed via
NodeResult - One exit rule – exactly one termination route enforced
- Tiny surface area – one file, three concepts:
Node,NodeResult,Flow - 100% test coverage – every line tested
- Zero runtime deps – bring your own clients (OpenAI, Anthropic, etc.)
Installation
pip install clearflow
60-second Quickstart
from typing import TypedDict
from clearflow import Flow, Node, NodeResult
# 1) Define typed state
class ChatState(TypedDict):
messages: list[dict[str, str]]
# 2) Define a node
class ChatNode(Node[ChatState]):
async def exec(self, state: ChatState) -> NodeResult[ChatState]:
# Call your LLM here
# reply = await llm.chat(state["messages"])
reply = {"role": "assistant", "content": "Hello!"}
new_state: ChatState = {"messages": [*state["messages"], reply]}
return NodeResult(new_state, outcome="success")
# 3) Build flow with explicit routing
chat = ChatNode()
flow = (
Flow[ChatState]("ChatBot")
.start_with(chat)
.route(chat, "success", None) # terminate on success
.build()
)
# 4) Run it
async def main() -> None:
result = await flow({"messages": [{"role": "user", "content": "Hi"}]})
print(result.state["messages"][-1]["content"]) # "Hello!"
import asyncio
asyncio.run(main())
Core Concepts
Node[T]
A unit that transforms state of type T.
prep(state: T) -> T– optional pre-work/validationexec(state: T) -> NodeResult[T]– required; return new state + outcomepost(result: NodeResult[T]) -> NodeResult[T]– optional cleanup/logging
Nodes are async and pure (no shared mutable state).
NodeResult[T]
Holds the new state and an outcome string used for routing.
Flow[T]
A fluent builder that wires nodes together with explicit routing:
Flow[T]("Name")
.start_with(a)
.route(a, "ok", b)
.route(b, "done", None) # exactly one termination
.build() # -> returns a Node[T] you can await
Routing: next node is (from_node.name, outcome). If no name set, uses class name.
Nested flows: a built flow is itself a Node[T] – compose flows within flows.
Example: Multi-step Pipeline
from typing import TypedDict
from clearflow import Flow, Node, NodeResult
class State(TypedDict):
value: int
class Validate(Node[State]):
async def exec(self, s: State) -> NodeResult[State]:
return NodeResult(s, "valid" if s["value"] >= 0 else "invalid")
class Process(Node[State]):
async def exec(self, s: State) -> NodeResult[State]:
return NodeResult({"value": s["value"] * 2}, "success")
class Output(Node[State]):
async def exec(self, s: State) -> NodeResult[State]:
print("Final:", s["value"])
return NodeResult(s, "done")
flow = (
Flow[State]("Pipeline")
.start_with(Validate())
.route(Validate(), "valid", Process())
.route(Validate(), "invalid", Output()) # route invalid to output
.route(Process(), "success", Output())
.route(Output(), "done", None) # single termination point
.build()
)
await flow({"value": 21}) # Final: 42
See more: Chat example | Structured output
Testing Example
Nodes are easy to test in isolation because they are pure functions over typed state:
import pytest
from clearflow import Node, NodeResult
class N(Node[int]):
async def exec(self, x: int) -> NodeResult[int]:
return NodeResult(x + 1, "ok")
@pytest.mark.asyncio
async def test_n() -> None:
res = await N()(0)
assert res.state == 1 and res.outcome == "ok"
When to Use ClearFlow
- LLM workflows where you need explicit control
- Systems requiring clear error handling paths
- Projects with strict dependency requirements
- Applications where debugging matters
ClearFlow vs PocketFlow
| Aspect | ClearFlow | PocketFlow |
|---|---|---|
| State | Immutable, passed via NodeResult |
Shared store (mutable dict) |
| Routing | Explicit (node, outcome) routes |
Graph with labeled edges |
| Termination | Exactly one None route enforced |
Multiple exit patterns |
| Type safety | Full Python 3.13+ generics | Dynamic |
| Lines | 166 | 100 |
Both are minimalist. ClearFlow emphasizes type safety and explicit control. PocketFlow emphasizes brevity and shared state.
Recipes
- Guardrails: validate node routes
"invalid"→ termination - Retries: node returns
"retry"outcome → routes back to itself - Sub-flows: build child flow, use as node in parent
- Parallel: multiple validate nodes → single process node
Development
# Install uv (if not already installed)
pip install --user uv # or: pipx install uv
# Clone and set up development environment
git clone https://github.com/consent-ai/ClearFlow.git
cd ClearFlow
uv sync --group dev # Creates venv and installs deps automatically
./quality-check.sh # Run all checks
Contributing
See CONTRIBUTING.md
License
Acknowledgments
Inspired by PocketFlow's Node-Flow-State pattern.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file clearflow-0.0.5.tar.gz.
File metadata
- Download URL: clearflow-0.0.5.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b23b3685d9f4909ecf3321380573337af3161eb9a81feca1ef14508836eed525
|
|
| MD5 |
1b11ea3d3a37a1f5d8f09f1fe27be0d8
|
|
| BLAKE2b-256 |
d298a8187da864a8e1fb1d6e2874c4d0623a56fb7aef77126e648db1aca4db05
|
Provenance
The following attestation bundles were made for clearflow-0.0.5.tar.gz:
Publisher:
release.yml on consent-ai/ClearFlow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clearflow-0.0.5.tar.gz -
Subject digest:
b23b3685d9f4909ecf3321380573337af3161eb9a81feca1ef14508836eed525 - Sigstore transparency entry: 403116508
- Sigstore integration time:
-
Permalink:
consent-ai/ClearFlow@22878b4eddb35d87b7b8e95549f1441d0f1cdb78 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/consent-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22878b4eddb35d87b7b8e95549f1441d0f1cdb78 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file clearflow-0.0.5-py3-none-any.whl.
File metadata
- Download URL: clearflow-0.0.5-py3-none-any.whl
- Upload date:
- Size: 6.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0d65e624f2a364fa4654f7029985422b48cc4dc6385b0127c36db848995d4cac
|
|
| MD5 |
e64f5c4cb7dbc50db3cecc6337793806
|
|
| BLAKE2b-256 |
98a19ca35dabecacaf084fb8d09ba349285a4ff5b2cc48b1ae8e9f40926165d1
|
Provenance
The following attestation bundles were made for clearflow-0.0.5-py3-none-any.whl:
Publisher:
release.yml on consent-ai/ClearFlow
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
clearflow-0.0.5-py3-none-any.whl -
Subject digest:
0d65e624f2a364fa4654f7029985422b48cc4dc6385b0127c36db848995d4cac - Sigstore transparency entry: 403116525
- Sigstore integration time:
-
Permalink:
consent-ai/ClearFlow@22878b4eddb35d87b7b8e95549f1441d0f1cdb78 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/consent-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@22878b4eddb35d87b7b8e95549f1441d0f1cdb78 -
Trigger Event:
workflow_dispatch
-
Statement type: