Orchid flow
A lightweight, async-first workflow orchestration (minimal lang-graph alternative) library for LLM applications. orchid_flow simplifies building multi-step agent workflows with type-safe state management, flexible routing, and built-in observability.
Features
- Async-First: Built on
asynciofor high-performance concurrent execution - Type-Safe: Leverages Pydantic for configuration and state validation
- Flexible Routing: Static transitions, conditional routing, and parallel execution
- Worker Support: Offload CPU-bound nodes to background processes
- Pluggable Storage: In-memory, Redis, or custom context stores
- FastAPI Integration: Quick REST API generation for workflows
- Observability: Built-in callback system for logging and monitoring
Installation
pip install orchid_flow
Quick Start
import asyncio
from pydantic import BaseModel
from typing import Optional
from orchid_flow import Workflow, Node, NodeContext
from orchid_flow.types import AgentRequest, AgentResp, UIOutput, UserInput
class CounterState(BaseModel):
count: int = 0
async def increment(ctx: NodeContext) -> AgentResp:
state = ctx.state
state.count += 1
return AgentResp(
ui_output=UIOutput(text=f"Count: {state.count}")
)
workflow = Workflow(
name="counter",
initial_state=CounterState(),
entry_node="start",
nodes=[
Node(name="start", func=increment, output_node=None)
]
)
async def main():
result = await workflow.run(
AgentRequest(
conversation_id="session-123",
user_input=UserInput(text="increment")
)
)
print(result.ui_output.text) # Count: 1
asyncio.run(main())
Core Concepts
Nodes
Nodes are async functions that receive a NodeContext. Return AgentResp to respond to the user, or None to proceed silently to the next node.
async def my_node(ctx: NodeContext) -> AgentResp | None:
user_text = ctx.user_input.text
user_text = ctx.user_input.text
if user_text:
return AgentResp(ui_output=UIOutput(text=f"You said: {user_text}"))
return None
- Each node can get a config with pydantic type
- Nodes can also be sync functions that run in a separate worker process
from pydantic import BaseModel
class MyConfig(BaseModel):
max_retries: int = 3
timeout: float = 30.0
def sync_node(ctx: NodeContext, config: MyConfig) -> AgentResp | None:
ctx.add_log("info", f"Max retries: {config.max_retries}")
...
Routing
The output_node parameter supports multiple routing strategies:
| Type | Behavior |
|---|---|
str |
Direct transition to named node |
list[str] |
Parallel execution of multiple nodes |
Callable |
Dynamic routing based on context |
def router(ctx: NodeContext) -> str:
if ctx.state.intent == "refund":
return ["refund_node","send_email"]
return "general_node"
# all following nodes are valid:
Node(name="classify", func=classify, output_node="process")
Node(name="classify", func=classify, output_node=["send_email", "update_db"])
Node(name="classify", func=classify, output_node=router)
State
Define conversation state with Pydantic models. State persists across turns within a conversation.
from orchid_flow.context import ConversationState
class SupportState(ConversationState):
intent: Optional[str] = None
attempt_count: int = 0
customer_id: Optional[str] = None
support_workflow = Workflow(
name="support",
initial_state=SupportState(),
...
)
async def handle_request(ctx: NodeContext) -> AgentResp:
state = ctx.state
state.attempt_count += 1
...
Callbacks
Observe workflow lifecycle with callbacks for logging, metrics, or side effects:
from orchid_flow import Callback, CallbackEvent
async def log_start(ctx: NodeContext, e: CallbackEvent):
print(f"[START] {ctx.node_name}")
async def log_end(ctx: NodeContext, e: CallbackEvent):
elapsed = e.data["elapsed"]
print(f"[END] {ctx.node_name} - {elapsed:.2f}s")
async def log_error(ctx: NodeContext, e: CallbackEvent):
print(f"[ERROR] {ctx.node_name}: {e.error}")
async def on_state_change(ctx: NodeContext, e: CallbackEvent):
print(f"[STATE_CHANGE] {ctx.node_name}: {e.data}")
workflow = Workflow(
...,
callbacks=[
Callback(on="node_start", fn=log_start),
Callback(on="node_end", fn=log_end),
Callback(on="node_err", fn=log_error),
Callback(on="set_state", fn=on_state_change),
]
)
Complete Example: Customer Support Bot
import asyncio
from pydantic import BaseModel
from typing import Optional
from orchid_flow import (
Workflow, Node, NodeContext, Callback, CallbackEvent,
AgentRequest, AgentResp, UIOutput, UserInput
)
from orchid_flow.context import ConversationState
class SupportState(ConversationState):
intent: Optional[str] = None
attempt_count: int = 0
customer_id: Optional[str] = None
class RefundConfig(BaseModel):
max_refund_amount: float = 50.0
require_reason: bool = True
async def classify_intent(ctx: NodeContext) -> AgentResp:
state = ctx.state
user_text = ctx.user_input.text or ""
if "refund" in user_text.lower():
state.intent = "refund"
elif "human" in user_text.lower():
state.intent = "human_agent"
else:
state.intent = "general"
return None
def intent_router(ctx: NodeContext) -> str:
state = ctx.state
if state.intent == "refund":
return "refund_node"
elif state.intent == "human_agent":
return "transfer_node"
return "general_node"
def handle_refund(ctx: NodeContext, config: RefundConfig) -> AgentResp:
ctx.add_log("info", "Processing refund request")
return AgentResp(
ui_output=UIOutput(
text=f"Processing refund (Limit: ${config.max_refund_amount}). Confirm?",
answer_type="select",
select_options=["Yes, Refund", "Cancel"],
)
)
async def transfer_to_human(ctx: NodeContext) -> AgentResp:
return AgentResp(
ui_output=UIOutput(text="Transferring to human agent...")
)
async def handle_general(ctx: NodeContext) -> AgentResp:
return AgentResp(
ui_output=UIOutput(text="How can I help you today?")
)
async def on_set_state(ctx: NodeContext, e: CallbackEvent):
print(f"State updated: {e.data}")
support_workflow = Workflow(
name="customer_support",
initial_state=SupportState(),
entry_node="entry_point",
callbacks=[
Callback(on="set_state", fn=on_set_state),
],
nodes=[
Node(
name="entry_point",
func=classify_intent,
output_node=intent_router,
run_in_worker=False,
),
Node(
name="refund_node",
func=handle_refund,
config=RefundConfig(max_refund_amount=100.0),
output_node=None,
run_in_worker=True,
),
Node(
name="transfer_node",
func=transfer_to_human,
output_node=None,
),
Node(
name="general_node",
func=handle_general,
output_node=None,
),
],
)
app: FastAPI = make_fastapi_app(
workflows=[support_workflow],
)
Workflow
| Parameter | Type | Description |
|---|---|---|
name |
str |
Workflow identifier |
initial_state |
BaseModel |
Initial state instance |
entry_node |
str |
Starting node name |
nodes |
list[Node] |
Node definitions |
callbacks |
list[Callback] |
Event handlers (optional) |
n_worker |
int |
Worker processes (default: 4) |
node_timeout |
float |
Node timeout in seconds (optional) |
context_store |
ContextStore |
Context persistence (optional) |
Node
| Parameter | Type | Description |
|---|---|---|
name |
str |
Node identifier |
func |
Callable |
Node function (async or sync) |
config |
BaseModel |
Config injected as second argument |
output_node |
str | list | Callable |
Next node(s) or router |
run_in_worker |
bool |
Run in background process |
NodeContext
| Property/Method | Description |
|---|---|
conversation_id |
Unique conversation identifier |
node_name |
Current node name |
node_counter |
Execution counter |
conversation_history |
List of conversation turns |
user_input |
Current user input |
state |
Conversation state (typed) |
logs |
List of log entries |
add_log(level, message) |
Add a log entry |
Types
| Type | Description |
|---|---|
AgentRequest |
Incoming request with conversation_id and user_input |
AgentResp |
Response with ui_output |
UserInput |
User input with text, select, number, date |
UIOutput |
UI output with text, answer_type, select_options |
Turn |
Conversation turn (user/bot) |
Log |
Log entry with level and message |
Callback Events
| Event | Description |
|---|---|
node_start |
Fired when a node begins execution |
node_end |
Fired when a node completes successfully |
node_err |
Fired when a node raises an exception |
set_state |
Fired when state is modified |
License
MIT
Metadata
Release files for orchid_flow 0.1.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| orchid_flow-0.1.6.tar.gz | 13.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| orchid_flow-0.1.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 27.0 kB
Release files / orchid_flow-0.1.6.tar.gz
| Download URL | orchid_flow-0.1.6.tar.gz |
|---|---|
| Size | 13.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1c0208c47fee0e1762069beb254e2d9481f6209b7b3851484a10057c95fd263e
|
|
BLAKE2b-256 checksum How to use checksums |
6f55a6697cf080d73825daca2a6e059ddaa3a750c51aa3e765df6da9d226275b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / orchid_flow-0.1.6-py3-none-any.whl
| Download URL | orchid_flow-0.1.6-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c502e7c21dac76ab1ac8981b931c55e5d66db5444f022b91ce74d32e29d3c311
|
|
BLAKE2b-256 checksum How to use checksums |
e4c0da47f66a00733c06025938843a13e5bc3cf73c0fce71bc2110ebc9870840
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.8 {"installer":{"name":"uv","version":"0.10.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"EndeavourOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for orchid_flow 0.1.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.