一个功能齐全的 LocalFlow 最小示例节点仓库,演示了节点开发的关键交互和最佳实践。
本仓库适合以下场景:
- 🎓 学习 LocalFlow 节点开发 — 从零理解节点的完整生命周期
- 🧪 验证节点加载流程 — 确认 LocalFlow 能正确发现、加载和执行节点
- 📋 作为开发模板 — 基于此仓库快速创建自己的节点仓库
- 将本仓库克隆到 LocalFlow 的节点目录:
# 克隆到 LocalFlow 的用户节点目录
git clone https://github.com/localflow-app/localflow-sample-node.git-
在 LocalFlow 中通过节点浏览器添加
demo_node节点到工作流。 -
配置「问候语」参数(或使用默认值
Hello LocalFlow!),运行工作流。 -
观察节点执行过程:控制台输出日志 + UI 进度条更新 + 输出数据传递。
localflow-sample-node/
├── demo_node/ # 示例节点目录
│ ├── node.json # 节点元数据配置
│ └── node.py # 节点执行脚本
├── manifest.json # 仓库清单(LocalFlow 发现节点的入口)
├── LICENSE # Apache 2.0 许可证
└── README.md # 本文档
核心规则:每个节点是一个独立子目录,目录名即
node_type(蛇形命名 snake_case),目录内至少包含node.json和node.py两个文件。
manifest.json 是 LocalFlow 发现节点的入口,位于仓库根目录:
{
"repo_name": "localflow-sample-node",
"repo_url": "https://github.com/localflow-app/localflow-sample-node",
"snapshot_version": "1.0.0",
"snapshot_commit": "",
"nodes": [
"demo_node"
]
}| 字段 | 说明 |
|---|---|
repo_name |
仓库名称,用于在 LocalFlow 中标识 |
repo_url |
仓库的远程地址 |
snapshot_version |
快照版本号 |
snapshot_commit |
快照对应的 Git commit SHA(由 LocalFlow 自动填充) |
nodes |
所有节点 node_type 的列表,必须与节点目录名一致 |
min_app_version |
❌ 可选,LocalFlow 最低版本要求,低于此版本不加载该仓库 |
manifest.json 的作用:
- 发现节点:LocalFlow 启动时读取
manifest.json的nodes列表,然后逐个加载对应目录 - 版本管理:
snapshot_version+snapshot_commit用于检测远程仓库是否有新版本 - 仓库元信息:
repo_name和repo_url用于 UI 显示和 GitHub 交互
⚠️ 新增节点后务必更新此文件,否则 LocalFlow 无法发现新节点。
demo_node/node.json 定义了节点的身份、配置项和依赖:
{
"node_type": "demo_node",
"name": "Demo 节点",
"description": "一个用于验证 localflow 节点加载流程的示例节点",
"category": "示例",
"version": "1.0.0",
"entry_file": "node.py",
"dependencies": [],
"config_schema": {
"greeting": {
"type": "string",
"label": "问候语",
"default": "Hello LocalFlow!",
"placeholder": "输入问候语文本",
"description": "节点执行时输出的问候内容"
}
},
"input_schema": {},
"output_schema": {
"demo_output": {
"type": "string",
"description": "问候语输出"
}
},
"metadata": {
"node_kind": "demo"
},
"examples": [
{
"title": "默认问候",
"config": {
"greeting": "Hello LocalFlow!"
}
}
],
"input_example": {},
"output_example": {
"demo_output": "Hello LocalFlow!"
}
}字段说明:
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
node_type |
string | ✅ | 节点唯一标识,必须与目录名一致,使用蛇形命名 |
name |
string | ✅ | 节点显示名称 |
description |
string | ✅ | 节点功能描述 |
category |
string | ✅ | 节点分类,用于节点浏览器分组 |
version |
string | ✅ | 语义化版本号(SemVer) |
entry_file |
string | ❌ | 入口文件名,默认 "node.py" |
dependencies |
string[] | ❌ | pip 依赖包列表,如 ["requests", "pandas"] |
config_schema |
object | ❌ | 配置项定义,LocalFlow 会据此自动生成 UI 表单 |
metadata |
object | ❌ | 附加元数据。系统使用 metadata.node_kind 标识特殊节点类型 |
input_schema |
object | ❌ | 输入变量 schema,用于工作流引擎校验上游输出 |
output_schema |
object | ❌ | 输出变量 schema |
examples |
array | ❌ | 使用示例列表,每项含 title 和 config,显示在属性面板 |
input_example |
object | ❌ | 输入样例数据 |
output_example |
object | ❌ | 输出样例数据 |
config_schema 配置项类型:
| 类型 | 说明 | 额外字段 |
|---|---|---|
string |
字符串输入 | placeholder 输入框占位文本 |
int |
整数输入 | — |
float |
浮点数输入 | — |
bool |
布尔开关 | — |
enum |
枚举选择 | 必须提供 options 数组 |
每个配置项支持以下子字段:
| 子字段 | 类型 | 说明 |
|---|---|---|
label |
string | 显示标签(必填) |
type |
string | 字段类型(必填) |
default |
any | 默认值 |
description |
string | 字段说明文字,UI 中显示为帮助提示 |
placeholder |
string | 输入框占位文本(仅 string 类型) |
以上 input_schema、output_schema、metadata、examples、input_example、output_example 为可选字段,适用于需要与工作流引擎深度集成的节点:
input_schema/output_schema:定义节点的输入输出接口。AI 服务(ai_chat_service.py)读取这些 schema 以理解节点的数据协议,自动生成正确的连线代码metadata:附加元数据。系统通过metadata.node_kind识别特殊节点类型。例如 Playwright 脚本节点的node_kind: "playwright_script"会被节点属性面板和注册表用于分支判断(node_base.py:169、node_registry.py:171、node_properties.py:272)examples:使用示例列表,每项包含title和config,显示在节点属性面板的"使用示例"区域(node_properties.py:320-321)input_example/output_example:样例数据,供调试和 AI 节点生成参考
对于绝大多数节点,config_schema 是 node.json 中的静态 JSON。但少数特殊节点类型(如 Playwright 脚本节点)采用 动态 config_schema 模式:
- 用户在 UI 中输入一段 Python 脚本,其中包含
{{url}}、{{limit}}等占位符 - 系统通过
extract_playwright_params()提取占位符列表 build_playwright_config_schema()基于占位符动态构建 schema,自动生成对应的配置表单字段
# 伪代码:动态 schema 生成逻辑
param_names = ["url", "limit"] # 从脚本中提取
for name in param_names:
schema[name] = {
"type": "string",
"label": name.replace("_", " ").title(),
"default": "",
"placeholder": f"填写 {name}",
}此模式在注册时通过 node_registry.py:171-172 替换静态 schema 实现。对于普通自定义节点,使用静态 JSON 即可,无需关注此模式。
demo_node/node.py 是节点的核心执行脚本:
def execute(self, input_data):
"""Demo 节点 - 输出问候语"""
greeting = self.config.get("greeting", "Hello LocalFlow!")
print(f"[Demo] 收到输入数据: {list(input_data.keys())}")
report_progress(50, "正在生成问候语...")
print(f"[Demo] 生成问候语: {greeting}")
report_progress(100, "完成")
return {**input_data, "demo_output": greeting}这段简短的代码涵盖了 LocalFlow 节点的 全部关键交互:
| 交互 | 代码 | 说明 |
|---|---|---|
| 读取配置 | self.config.get("greeting", ...) |
从 config_schema 获取用户在 UI 中设置的值 |
| 接收输入 | input_data 参数 |
获取上游节点传递的数据字典 |
| 进度报告 | report_progress(50, "...") |
向 UI 报告执行进度 |
| 传递输出 | return {**input_data, ...} |
将结果传递给下游节点 |
self.config 是一个字典,包含用户在 UI 中为 config_schema 定义的参数所设置的运行时值。
# 读取配置,提供默认值作为兜底
greeting = self.config.get("greeting", "Hello LocalFlow!")要点:
- 键名与
node.json中config_schema的键名一一对应 - 始终使用
.get(key, default)而非[key],避免 KeyError - 默认值应与
config_schema中的default字段保持一致
input_data 是一个字典,包含上游所有节点输出的合并数据。
# 查看上游传递了哪些数据
print(f"收到输入数据: {list(input_data.keys())}")
# 读取特定上游输出
some_value = input_data.get("upstream_key", None)要点:
- 如果节点是工作流的第一个节点,
input_data为空字典{} - 上游节点的输出键名由上游节点的返回值决定
- 使用
.get(key, default)安全访问,避免因上游数据缺失而崩溃
execute 方法必须返回一个字典,该字典将合并到数据流中传递给下游节点。
# ✅ 推荐:展开 input_data,确保上游数据继续向下传递
return {**input_data, "demo_output": greeting}
# ❌ 不推荐:丢弃上游数据
return {"demo_output": greeting}要点:
- 使用
{**input_data, "new_key": value}模式,确保上游数据不丢失 - 输出键名建议使用具有描述性的名称(如
demo_output、file_path),避免与上游键冲突 - 如果新键与上游键重名,新值会覆盖上游值
report_progress() 由 LocalFlow 运行时自动注入,无需 import,可直接在 execute 中调用。
# 报告进度:百分比 + 描述信息
report_progress(50, "正在生成问候语...")
report_progress(100, "完成")要点:
percent范围 0–100,超出会自动截断message为可选参数,在 UI 上显示当前步骤描述- 适用于耗时操作(循环处理、网络请求、文件读写等)
- 如果不调用,UI 显示默认的旋转动画指示器
- 不影响执行性能,可放心在循环中使用
对于需要隔离子环境、大型第三方依赖(如 Playwright、Selenium)或长时间运行的节点,execute() 可以不直接执行逻辑,而是生成一个独立的 Python 脚本并通过 subprocess.run() 启动子进程执行。
- 节点依赖需要独立安装(如
playwright、selenium) - 执行可能阻塞 UI 或需要超时控制
- 需要隔离的进程环境(环境变量、工作目录)
LocalFlow 内置了标准的子进程执行模板(node_base.py:214-248),生成的脚本框架会自动处理通信。核心机制:
┌─────────────────────────────────────────┐
│ NodeShim(父进程端) │
│ 1. 嵌入 NODE_CONFIG 到脚本 │
│ 2. 将 input_data 写入 stdin │
│ 3. 启动 subprocess.run(capture_output) │
│ 4. 从 stdout 解析 ###JSON_OUTPUT### │
│ 5. 从 ###PROGRESS## 解析进度并转发 UI │
└──────────────┬──────────────────────────┘
│ subprocess.run()
▼
┌─────────────────────────────────────────┐
│ 子进程脚本 │
│ - 读取 stdin → input_data │
│ - 实例化 NodeShim → self.config │
│ - 调用 execute(shim, input_data) │
│ - print("###JSON_OUTPUT###") │
│ - print(json.dumps(output_data)) │
│ - print("###JSON_OUTPUT_END###") │
└─────────────────────────────────────────┘
在子进程模式下,节点作者可使用的通信机制:
| 功能 | 方式 | 说明 |
|---|---|---|
| 输出数据 | print("###JSON_OUTPUT###") + JSON + print("###JSON_OUTPUT_END###") |
由 NodeShim 模板自动生成,无需手动编写 |
| 进度报告 | print("###PROGRESS##{...}") 或直接调用 report_progress() |
模板中已定义 report_progress() 函数,内部使用此标记 |
| 错误退出 | sys.exit(1) |
父进程捕获非零返回码,将 stderr 作为错误信息 |
def execute(self, input_data):
import json, subprocess, sys, os
from pathlib import Path
# 1. 生成运行时脚本
runtime_script = '''#!/usr/bin/env python
import json, sys
NODE_CONFIG = {config_json}
def report_progress(pct, msg=""):
print(f"###PROGRESS##{{json.dumps({{...}})}}")
{user_logic}
def main():
input_data = json.loads(sys.stdin.read())
class NodeShim:
def __init__(self, cfg): self.config = cfg
output = execute(NodeShim(NODE_CONFIG), input_data)
print("###JSON_OUTPUT###")
print(json.dumps(output))
print("###JSON_OUTPUT_END###")
if __name__ == "__main__":
main()
'''
script_path = Path.cwd() / "runtime_script.py"
script_path.write_text(runtime_script, encoding="utf-8")
# 2. 子进程执行
proc = subprocess.run(
[sys.executable, str(script_path)],
input=json.dumps(input_data),
capture_output=True, text=True, timeout=120,
cwd=os.getcwd()
)
# 3. 解析结果
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip())
start = proc.stdout.find("###JSON_OUTPUT###")
end = proc.stdout.find("###JSON_OUTPUT_END###")
payload = json.loads(proc.stdout[start+17:end].strip())
return {**input_data, **payload}💡 Playwright 节点 是子进程模式的典型实现。它的
execute()由build_playwright_inline_wrapper_source()(playwright_node_utils.py:131)自动生成,除了标准通信外还额外注入了LF_INPUT_DATA、LF_DOWNLOAD_DIR、LF_ARTIFACTS_DIR等运行时变量,并对 Playwright 浏览器进行了 monkey-patch(自动拦截下载事件)。
# ✅ 好的做法
return {**input_data, "my_result": result}
# ❌ 不好的做法 — 下游节点将无法访问上游数据
return {"my_result": result}# ✅ 带默认值,避免 KeyError
value = self.config.get("key", "default")
data = input_data.get("key", None)
# ❌ 直接索引,可能抛出异常
value = self.config["key"]
data = input_data["key"]def execute(self, input_data):
items = input_data.get("items", [])
results = []
for i, item in enumerate(items):
results.append(process(item))
report_progress(int((i + 1) / len(items) * 100), f"处理中 {i+1}/{len(items)}")
return {**input_data, "results": results}如果节点使用了第三方库,必须在 node.json 的 dependencies 中声明:
{
"dependencies": ["requests", "beautifulsoup4"]
}LocalFlow 会自动安装声明的依赖,无需用户手动操作。
print() 的输出会显示在 LocalFlow 的控制台中,适合输出调试信息:
print(f"[MyNode] 开始处理,共 {len(items)} 条数据")注意:如果节点使用 子进程执行模式,print() 的输出会被 subprocess.run(capture_output=True) 捕获,不会实时显示在控制台。子进程结束后,stdout 会通过 result['script_stdout'] 字段透传给父进程进行统一输出。如果需要实时日志,使用 report_progress() 或 sys.stderr.write()。
建议在节点中捕获异常并返回错误信息,而非让异常直接崩溃:
def execute(self, input_data):
try:
result = do_something()
return {**input_data, "success": True, "result": result}
except Exception as e:
return {**input_data, "success": False, "error": str(e)}为 input_schema 和 output_schema 填写准确的类型和描述有助于:
- AI 服务理解节点的数据接口,生成正确的工作流连线
- 工作流引擎在运行前校验上下游数据兼容性
- UI 显示输出数据的结构提示
{
"output_schema": {
"result": { "type": "string", "description": "处理结果" },
"count": { "type": "integer", "description": "数量" }
}
}基于本仓库创建你自己的节点仓库只需三步:
your-node-repo/
├── your_node/ # 你的节点目录
│ ├── node.json # 修改为你的节点元数据
│ └── node.py # 修改为你的节点逻辑
├── manifest.json # 修改 repo_name、repo_url 和 nodes
└── LICENSE
manifest.json:
{
"repo_name": "your-node-repo",
"repo_url": "https://github.com/your-username/your-node-repo",
"snapshot_version": "1.0.0",
"snapshot_commit": "",
"nodes": [
"your_node"
]
}your_node/node.json:
{
"node_type": "your_node",
"name": "你的节点",
"description": "节点的功能描述",
"category": "分类名称",
"version": "1.0.0",
"entry_file": "node.py",
"dependencies": [],
"config_schema": {}
}your_node/node.py:
def execute(self, input_data):
result = "your logic here"
return {**input_data, "output_key": result}将节点目录放入 LocalFlow 的节点目录,重启应用即可在节点浏览器中看到新节点。
本项目采用 Apache License 2.0 开源许可证。
A minimal, fully functional LocalFlow sample node repository that demonstrates key interactions and best practices for node development.
This repository is ideal for:
- 🎓 Learning LocalFlow node development — Understand the complete node lifecycle from scratch
- 🧪 Verifying node loading — Confirm LocalFlow can correctly discover, load, and execute nodes
- 📋 Using as a development template — Quickly create your own node repository based on this one
- Quick Start
- Repository Structure
- File-by-File Walkthrough
- Key Interactions Explained
- Advanced: Subprocess Execution
- Best Practices
- Creating Your Own Node Repository
- License
- Clone this repository into LocalFlow's node directory:
git clone https://github.com/localflow-app/localflow-sample-node.git-
Add the
demo_nodeto your workflow via the LocalFlow node browser. -
Configure the "Greeting" parameter (or use the default
Hello LocalFlow!) and run the workflow. -
Observe the node execution: console log output + UI progress bar updates + output data passing.
localflow-sample-node/
├── demo_node/ # Sample node directory
│ ├── node.json # Node metadata configuration
│ └── node.py # Node execution script
├── manifest.json # Repository manifest (entry point for LocalFlow node discovery)
├── LICENSE # Apache 2.0 License
└── README.md # This document
Core rule: Each node is an independent subdirectory. The directory name is the
node_type(snake_case), and must contain at leastnode.jsonandnode.py.
manifest.json is the entry point for LocalFlow to discover nodes, located at the repository root:
{
"repo_name": "localflow-sample-node",
"repo_url": "https://github.com/localflow-app/localflow-sample-node",
"snapshot_version": "1.0.0",
"snapshot_commit": "",
"nodes": [
"demo_node"
]
}| Field | Description |
|---|---|
repo_name |
Repository name, used for identification in LocalFlow |
repo_url |
Remote URL of the repository |
snapshot_version |
Snapshot version number |
snapshot_commit |
Git commit SHA for the snapshot (auto-filled by LocalFlow) |
nodes |
List of all node node_type values, must match node directory names |
min_app_version |
❌ Optional. Minimum LocalFlow version required; the repo won't load if the app is older |
Purpose of manifest.json:
- Node discovery: LocalFlow reads the
nodeslist frommanifest.jsonon startup, then loads each node directory - Version management:
snapshot_version+snapshot_commitare used to detect new versions in the remote repository - Repository metadata:
repo_nameandrepo_urlare used for UI display and GitHub interactions
⚠️ Always update this file when adding new nodes, otherwise LocalFlow won't discover them.
demo_node/node.json defines the node's identity, configuration, and dependencies:
{
"node_type": "demo_node",
"name": "Demo 节点",
"description": "一个用于验证 localflow 节点加载流程的示例节点",
"category": "示例",
"version": "1.0.0",
"entry_file": "node.py",
"dependencies": [],
"config_schema": {
"greeting": {
"type": "string",
"label": "问候语",
"default": "Hello LocalFlow!",
"placeholder": "输入问候语文本",
"description": "节点执行时输出的问候内容"
}
},
"input_schema": {},
"output_schema": {
"demo_output": {
"type": "string",
"description": "问候语输出"
}
},
"metadata": {
"node_kind": "demo"
},
"examples": [
{
"title": "默认问候",
"config": {
"greeting": "Hello LocalFlow!"
}
}
],
"input_example": {},
"output_example": {
"demo_output": "Hello LocalFlow!"
}
}Field Reference:
| Field | Type | Required | Description |
|---|---|---|---|
node_type |
string | ✅ | Unique node identifier, must match directory name, snake_case |
name |
string | ✅ | Display name for the node |
description |
string | ✅ | Node functionality description |
category |
string | ✅ | Node category for grouping in the node browser |
version |
string | ✅ | Semantic version (SemVer) |
entry_file |
string | ❌ | Entry file name, defaults to "node.py" |
dependencies |
string[] | ❌ | pip dependency list, e.g. ["requests", "pandas"] |
config_schema |
object | ❌ | Configuration definition; LocalFlow auto-generates UI forms from this |
metadata |
object | ❌ | Additional metadata. The system uses metadata.node_kind to identify special node types |
input_schema |
object | ❌ | Input variable schema for upstream validation and AI auto-wiring |
output_schema |
object | ❌ | Output variable schema |
examples |
array | ❌ | Usage examples list, each with title and config, shown in the properties panel |
input_example |
object | ❌ | Sample input data |
output_example |
object | ❌ | Sample output data |
config_schema Parameter Types:
| Type | Description | Extra Fields |
|---|---|---|
string |
String input | placeholder input hint text |
int |
Integer input | — |
float |
Float input | — |
bool |
Boolean toggle | — |
enum |
Enum selection | Must provide options array |
Each parameter supports these sub-fields:
| Sub-field | Type | Description |
|---|---|---|
label |
string | Display label (required) |
type |
string | Field type (required) |
default |
any | Default value |
description |
string | Field description, shown as a help tooltip in the UI |
placeholder |
string | Input placeholder text (string type only) |
The optional fields input_schema, output_schema, metadata, examples, input_example, and output_example are for nodes that need deep integration with the workflow engine:
input_schema/output_schema: Define the node's data interface. The AI service (ai_chat_service.py) reads these schemas to understand the node's data protocol and auto-generate correct wiring codemetadata: Additional metadata. The system usesmetadata.node_kindto identify special node types. For example, Playwright script nodes usenode_kind: "playwright_script"which triggers branching logic in the node properties panel and registry (node_base.py:169,node_registry.py:171,node_properties.py:272)examples: Usage examples, each withtitleandconfig, displayed in the "Examples" section of the node properties panel (node_properties.py:320-321)input_example/output_example: Sample data for debugging and AI-generated node reference
For the vast majority of nodes, config_schema is a static JSON in node.json. However, some special node types (like Playwright script nodes) use a dynamic config_schema pattern:
- The user enters a Python script in the UI containing
{{url}},{{limit}}placeholders - The system extracts placeholder names via
extract_playwright_params() build_playwright_config_schema()dynamically builds a schema from the placeholders, auto-generating corresponding form fields
# Pseudocode: dynamic schema generation
param_names = ["url", "limit"] # extracted from script
for name in param_names:
schema[name] = {
"type": "string",
"label": name.replace("_", " ").title(),
"default": "",
"placeholder": f"Enter {name}",
}This pattern is implemented via node_registry.py:171-172, which replaces the static schema at registration time. For regular custom nodes, static JSON is sufficient — no need to worry about this pattern.
demo_node/node.py is the core execution script:
def execute(self, input_data):
"""Demo 节点 - 输出问候语"""
greeting = self.config.get("greeting", "Hello LocalFlow!")
print(f"[Demo] 收到输入数据: {list(input_data.keys())}")
report_progress(50, "正在生成问候语...")
print(f"[Demo] 生成问候语: {greeting}")
report_progress(100, "完成")
return {**input_data, "demo_output": greeting}This short code covers all key interactions of a LocalFlow node:
| Interaction | Code | Description |
|---|---|---|
| Read config | self.config.get("greeting", ...) |
Get runtime values set by the user in the UI via config_schema |
| Receive input | input_data parameter |
Get the data dictionary passed from upstream nodes |
| Report progress | report_progress(50, "...") |
Report execution progress to the UI |
| Pass output | return {**input_data, ...} |
Pass results to downstream nodes |
self.config is a dictionary containing the runtime values set by the user for the parameters defined in config_schema.
# Read config with a fallback default value
greeting = self.config.get("greeting", "Hello LocalFlow!")Key points:
- Keys correspond one-to-one with
config_schemakeys innode.json - Always use
.get(key, default)instead of[key]to avoid KeyError - Default values should be consistent with the
defaultfield inconfig_schema
input_data is a dictionary containing the merged output data from all upstream nodes.
# Check what data upstream has passed
print(f"Received input data: {list(input_data.keys())}")
# Read a specific upstream output
some_value = input_data.get("upstream_key", None)Key points:
- If the node is the first in the workflow,
input_datais an empty dictionary{} - Upstream output key names are determined by the upstream node's return value
- Use
.get(key, default)for safe access to avoid crashes from missing upstream data
The execute method must return a dictionary, which is merged into the data flow and passed to downstream nodes.
# ✅ Recommended: spread input_data to ensure upstream data continues downstream
return {**input_data, "demo_output": greeting}
# ❌ Not recommended: discards upstream data
return {"demo_output": greeting}Key points:
- Use the
{**input_data, "new_key": value}pattern to ensure no upstream data is lost - Use descriptive output key names (e.g.
demo_output,file_path) to avoid conflicts with upstream keys - If a new key has the same name as an upstream key, the new value overwrites the upstream value
report_progress() is automatically injected by the LocalFlow runtime. No import is needed — just call it directly in execute.
# Report progress: percentage + description
report_progress(50, "Generating greeting...")
report_progress(100, "Done")Key points:
percentrange is 0–100; values outside this range are automatically clampedmessageis optional; displayed in the UI as the current step description- Use for long-running operations (loops, network requests, file I/O, etc.)
- If not called, the UI shows a default spinner indicator
- Does not affect execution performance; safe to use in loops
For nodes that need isolated environments, large third-party dependencies (e.g., Playwright, Selenium), or long-running operations, execute() can launch a child process via subprocess.run() instead of running logic directly in-process.
- Node dependencies need separate installation (e.g.,
playwright,selenium) - Execution may block the UI or needs timeout control
- An isolated process environment (env vars, working directory) is required
LocalFlow has a built-in standard subprocess execution template (node_base.py:214-248). The generated script framework handles communication automatically:
┌─────────────────────────────────────────┐
│ NodeShim (parent process side) │
│ 1. Embed NODE_CONFIG into the script │
│ 2. Write input_data to stdin │
│ 3. Launch subprocess.run(capture_output)│
│ 4. Parse ###JSON_OUTPUT### from stdout │
│ 5. Parse ###PROGRESS## markers → UI │
└──────────────┬──────────────────────────┘
│ subprocess.run()
▼
┌─────────────────────────────────────────┐
│ Child process script │
│ - Read stdin → input_data │
│ - Instantiate NodeShim → self.config │
│ - Call execute(shim, input_data) │
│ - print("###JSON_OUTPUT###") │
│ - print(json.dumps(output_data)) │
│ - print("###JSON_OUTPUT_END###") │
└─────────────────────────────────────────┘
Communication mechanisms available in subprocess mode:
| Feature | Method | Description |
|---|---|---|
| Output data | print("###JSON_OUTPUT###") + JSON + print("###JSON_OUTPUT_END###") |
Auto-generated by NodeShim template; no manual coding needed |
| Progress reporting | print("###PROGRESS##{...}") or call report_progress() directly |
The report_progress() function is already defined in the template |
| Error exit | sys.exit(1) |
Parent captures non-zero return code, uses stderr as error message |
def execute(self, input_data):
import json, subprocess, sys, os
from pathlib import Path
# 1. Generate runtime script
runtime_script = '''#!/usr/bin/env python
import json, sys
NODE_CONFIG = {config_json}
def report_progress(pct, msg=""):
print(f"###PROGRESS##{{json.dumps({{...}})}}")
{user_logic}
def main():
input_data = json.loads(sys.stdin.read())
class NodeShim:
def __init__(self, cfg): self.config = cfg
output = execute(NodeShim(NODE_CONFIG), input_data)
print("###JSON_OUTPUT###")
print(json.dumps(output))
print("###JSON_OUTPUT_END###")
if __name__ == "__main__":
main()
'''
script_path = Path.cwd() / "runtime_script.py"
script_path.write_text(runtime_script, encoding="utf-8")
# 2. Execute child process
proc = subprocess.run(
[sys.executable, str(script_path)],
input=json.dumps(input_data),
capture_output=True, text=True, timeout=120,
cwd=os.getcwd()
)
# 3. Parse results
if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip())
start = proc.stdout.find("###JSON_OUTPUT###")
end = proc.stdout.find("###JSON_OUTPUT_END###")
payload = json.loads(proc.stdout[start+17:end].strip())
return {**input_data, **payload}💡 Playwright node is a typical subprocess mode implementation. Its
execute()is auto-generated bybuild_playwright_inline_wrapper_source()(playwright_node_utils.py:131). Beyond standard communication, it injects runtime variables (LF_INPUT_DATA,LF_DOWNLOAD_DIR,LF_ARTIFACTS_DIR) and monkey-patches Playwright browsers to auto-handle downloads.
# ✅ Good practice
return {**input_data, "my_result": result}
# ❌ Bad practice — downstream nodes cannot access upstream data
return {"my_result": result}# ✅ With default values, avoiding KeyError
value = self.config.get("key", "default")
data = input_data.get("key", None)
# ❌ Direct indexing, may raise exceptions
value = self.config["key"]
data = input_data["key"]def execute(self, input_data):
items = input_data.get("items", [])
results = []
for i, item in enumerate(items):
results.append(process(item))
report_progress(int((i + 1) / len(items) * 100), f"Processing {i+1}/{len(items)}")
return {**input_data, "results": results}If your node uses third-party libraries, you must declare them in node.json:
{
"dependencies": ["requests", "beautifulsoup4"]
}LocalFlow will automatically install declared dependencies — no manual action required from users.
print() output is displayed in the LocalFlow console, suitable for debug information:
print(f"[MyNode] Starting processing, {len(items)} items total")Note: If your node uses subprocess execution mode, print() output is captured by subprocess.run(capture_output=True) and won't appear in real time. After the child process finishes, stdout is forwarded through the result['script_stdout'] field for unified output. For real-time logging, use report_progress() or sys.stderr.write().
It's recommended to catch exceptions and return error information rather than letting exceptions crash the node:
def execute(self, input_data):
try:
result = do_something()
return {**input_data, "success": True, "result": result}
except Exception as e:
return {**input_data, "success": False, "error": str(e)}Providing accurate types and descriptions for input_schema and output_schema helps:
- The AI service understand your node's data interface and generate correct workflow wiring
- The workflow engine validate upstream/downstream data compatibility before execution
- The UI display structure hints for output data
{
"output_schema": {
"result": { "type": "string", "description": "Processing result" },
"count": { "type": "integer", "description": "Item count" }
}
}Create your own node repository based on this one in three steps:
your-node-repo/
├── your_node/ # Your node directory
│ ├── node.json # Modify with your node metadata
│ └── node.py # Modify with your node logic
├── manifest.json # Update repo_name, repo_url, and nodes
└── LICENSE
manifest.json:
{
"repo_name": "your-node-repo",
"repo_url": "https://github.com/your-username/your-node-repo",
"snapshot_version": "1.0.0",
"snapshot_commit": "",
"nodes": [
"your_node"
]
}your_node/node.json:
{
"node_type": "your_node",
"name": "Your Node",
"description": "Description of your node's functionality",
"category": "Category Name",
"version": "1.0.0",
"entry_file": "node.py",
"dependencies": [],
"config_schema": {}
}your_node/node.py:
def execute(self, input_data):
result = "your logic here"
return {**input_data, "output_key": result}Place the node directory into LocalFlow's node directory and restart the application. Your node will appear in the node browser.
This project is licensed under the Apache License 2.0.