From 0aa402f89ce2b012b1350d1cce9b06174ee26d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:47:09 +0800 Subject: [PATCH] Add files via upload --- docs/workflow-graph.md | 176 +++++++++++++++++++++++++++++++++++--- docs/workflow-graph_en.md | 176 +++++++++++++++++++++++++++++++++++--- 2 files changed, 326 insertions(+), 26 deletions(-) diff --git a/docs/workflow-graph.md b/docs/workflow-graph.md index 28832bdb..f2e6abdd 100644 --- a/docs/workflow-graph.md +++ b/docs/workflow-graph.md @@ -27,9 +27,10 @@ | 选中元素 | 单击节点或连线,右侧显示 **节点属性** | | 删除选中 | 点击 **删除选中** 删除当前节点或连线 | | 自动布局 | 点击 **自动布局** 整理节点位置 | +| 试运行 | 点击 **试运行** 使用安全 dry-run 验证数据流;工具、Agent、审批不会真实执行 | | 删除流程 | 点击 **删除** 删除整个流程定义 | -**建议:** 每个流程至少包含 **1 个开始节点** 和 **1 个输出节点**;开始节点不应有入边,输出节点不应有出边。 +**硬性规则:** 每个流程至少包含 **1 个开始节点** 和 **1 个输出节点**;开始节点不能有入边,输出 / 结束节点不能有出边。保存时前端和后端都会执行严格校验。 --- @@ -45,6 +46,7 @@ | `lastOutput` | `{{previous.xxx}}` | **上一个刚执行完** 的节点的输出 | | `outputs` | `{{outputs.xxx}}` | 全局 **命名变量池**(由节点的「输出变量名」写入) | | `nodeOutputs` | `{{节点ID.xxx}}` | 指定节点 ID 的完整输出对象 | +| `metrics` | 运行详情中查看 | 节点耗时、工具调用数、可收集到的 token / cost 等指标 | ### 3.1 `previous` 是什么? @@ -69,6 +71,15 @@ Agent B 的 `{{previous.output}}` = Agent A 的输出。 Agent B 的 `{{previous.output}}` = **条件节点** 的输出(`true` / `false`),**不是** Agent A 的结果。 +如果一个节点有 **多个上游节点** 同时连入,`previous` 会先按该节点的 **汇聚策略** 生成: + +| 汇聚策略 | 含义 | 适合场景 | +|----------|------|----------| +| `all_merge` | 合并所有上游输出,`previous.output` 为数组 | 默认推荐,综合多路结果 | +| `last_by_canvas` | 按画布顺序取最后一个上游输出 | 明确只采用一路结果 | +| `first_non_empty` | 取第一个非空输出 | 多路兜底 | +| `fail_fast` | 任一上游失败则中止当前节点 | 关键链路、审批前置、安全检查 | + ### 3.2 `outputs` 是什么? `outputs` 是引擎在运行过程中维护的 **命名变量注册表**。 @@ -135,21 +146,55 @@ outputs["你填的变量名"] = 节点输出内容 | `{{previous.matched}}` | 上一条件节点的匹配结果(`true` / `false`) | | `{{outputs.变量名}}` | 某节点注册过的命名输出 | | `{{节点ID.output}}` | 指定节点 ID 的 `output` 字段 | +| `{{previous.kind}}` | 上一节点输出类型,如 `agent` / `tool` / `condition` | +| `{{previous.status}}` | 上一节点状态,如 `completed` / `failed` / `simulated` | + +节点输出会保留兼容字段(如 `output`、`matched`),同时带有结构化字段: + +```json +{ + "kind": "agent", + "node_id": "node-2", + "node_type": "agent", + "status": "completed", + "output": "..." +} +``` ### 4.3 条件表达式 -条件节点和连线条件支持简单比较: +条件节点和连线条件支持比较、文本匹配、正则、逻辑组合与安全 JSONPath/JQ 路径读取: ```text {{outputs.agent_result1}} != "" {{previous.output}} == "ok" -{{outputs.count}} == "100" +{{outputs.count}} >= 100 +{{previous.output}} contains "success" +{{previous.output}} matches "^ok" +{{outputs.risk_score}} >= 8 && {{previous.output}} != "" +jsonpath({{previous.output}}, "$.status") == "ok" +jq({{outputs.scan}}, ".severity") == "high" ``` 规则: -- 使用 `==` 或 `!=` 做字符串比较(两侧会自动去掉首尾空格和引号) +- 支持 `==`、`!=`、`>`、`>=`、`<`、`<=` +- 支持 `contains` 子串匹配与 `matches` 正则匹配 +- 支持简单 `&&` / `||` +- 支持 `jsonpath(value, "$.path")` 与 `jq(value, ".path")` 的**安全路径子集**,仅做字段读取,不执行任意脚本 +- 比较两侧会自动去掉首尾空格和引号 - 无比较符时,非空且不为 `false` / `0` / `null` 视为真 +- 保存时会静态校验表达式格式、JSONPath/JQ 路径和正则语法 + +### 4.4 嵌套字段绑定 + +节点的字段绑定除 `output`、`message` 等普通字段外,也支持 JSONPath/JQ 风格路径: + +| 绑定配置 | 含义 | +|----------|------| +| `from=previous, field=$.status` | 从上一节点输出对象读取 `status` | +| `from=outputs, field=$.scan.severity` | 从命名输出中读取嵌套字段 | +| `from=node-1, field=.output.items[0]` | 从指定节点输出读取数组元素 | --- @@ -175,6 +220,7 @@ outputs["你填的变量名"] = 节点输出内容 | 输入来源 | 上游数据的模板表达式 | `{{previous.output}}` | | 节点指令 | 本节点要完成的任务描述 | 空 | | 输出变量名 | 写入 `outputs` 的键名 | `agent_result` | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | **消息拼装规则:** @@ -186,6 +232,7 @@ Agent 节点执行后: - `previous.output` 更新为本节点响应文本 - 若配置了 **输出变量名**,同时写入 `outputs[输出变量名]` +- Agent 子图在 Eino 中拆为 `prepare → execute → finalize`,便于 trace 与后续局部 checkpoint ### 5.3 工具(tool) @@ -196,6 +243,7 @@ Agent 节点执行后: | MCP 工具 | 工具名称(必填) | — | | 参数模板 | JSON,支持 `{{...}}` 模板 | `{}` | | 超时秒数 | 可选 | 空 | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | 示例参数模板: @@ -212,6 +260,7 @@ Agent 节点执行后: | 字段 | 说明 | 默认值 | |------|------|--------| | 条件表达式 | 支持 `{{...}}` 与 `==` / `!=` | `{{previous.output}} != ""` | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | **分支规则:** @@ -229,12 +278,21 @@ Agent 节点执行后: ### 5.5 审批(hitl) -人工确认检查点(当前为记录模式,自动标记 `approved: true` 并继续)。 +人工确认检查点。流程运行到该节点前会通过 Eino interrupt/checkpoint 暂停,等待 API 或监控面板审批后恢复。 | 字段 | 说明 | 默认值 | |------|------|--------| | 审批提示 | 支持模板 | `请审批该步骤是否继续执行` | +| 提示字段绑定 | 留空审批提示时,从绑定字段读取说明 | `previous.output` | | 审批方 | `human` / `audit_agent` | `human` | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | + +HITL 等待信息会记录: + +- `checkpointId` +- interrupt `beforeNodes` +- resume target / address / path +- resume payload schema(`approved`、`comment`) ### 5.6 输出(output) @@ -244,6 +302,8 @@ Agent 节点执行后: |------|------|--------| | 输出变量名 | 必填,最终结果的键名 | `result` | | 变量来源 | 模板表达式,决定写入的值 | `{{previous.output}}` | +| 固定输出值 | 可选,填写后覆盖变量来源 | 空 | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | **注意:** 输出节点是流程的「出口」,不应再有出边。 @@ -254,6 +314,7 @@ Agent 节点执行后: | 字段 | 说明 | 默认值 | |------|------|--------| | 结束摘要模板 | 支持 `{{outputs.xxx}}` | `{{outputs.result}}` | +| 汇聚策略 | 多上游进入本节点时如何生成 `previous` | `all_merge` | --- @@ -353,7 +414,80 @@ workflow_policy: auto --- -## 九、保存前校验规则 +## 九、调试、试运行与复盘 + +### 9.1 安全试运行(dry-run) + +画布工具栏点击 **试运行**,输入一条测试消息即可模拟执行流程。 + +dry-run 的安全边界: + +- `start` / `condition` / `output` / `end` 会按真实逻辑计算 +- `tool` 不会真实调用 MCP,只返回 `[dry-run] tool call skipped` +- `agent` 不会真实调用模型,只返回 `[dry-run] agent execution skipped` +- `hitl` 不会暂停,只模拟通过 + +相关 API: + +```http +POST /api/workflows/dry-run +``` + +请求体: + +```json +{ + "graph": { "nodes": [], "edges": [], "config": {} }, + "inputs": { "message": "ping" } +} +``` + +响应包含: + +- `outputs` +- `nodeOutputs` +- `trace` +- `metrics` +- `replayScript` + +### 9.2 运行详情与 replay + +运行后可查询完整节点执行轨迹: + +```http +GET /api/workflows/runs/{runId} +``` + +返回 `run` 与 `nodeRuns`,每个节点记录包含: + +- input 快照 +- output 快照 +- status / error +- started_at / finished_at +- `duration_ms` + +复盘接口: + +```http +GET /api/workflows/runs/{runId}/replay +``` + +该接口只根据已保存的 `nodeRuns` 生成步骤,不会重新执行工具或 Agent。 + +### 9.3 指标(metrics) + +工作流会尽量累计: + +- `node_count` +- `duration_ms` +- `tool_call_count` +- Agent progress 中可收集到的 `prompt_tokens` / `completion_tokens` / `total_tokens` / `cost` + +token 与成本是否存在取决于底层模型/Agent 事件是否上报 usage。 + +--- + +## 十、保存前校验规则 保存时系统会自动检查: @@ -363,13 +497,20 @@ workflow_policy: auto | 必须有输出节点 | 至少 1 个 `output`,且填写输出变量名 | | 连线合法 | 源/目标节点存在,不能自环 | | 开始节点无入边 | 开始节点不能被指向 | -| 输出节点无出边 | 输出节点后不应再连线 | -| 工具节点 | 必须选择 MCP 工具 | -| 条件节点 | 必须填写表达式;建议 1~2 条出边(是/否) | +| 输出 / 结束节点无出边 | 输出 / 结束节点后不应再连线 | +| 非开始节点必须有入边 | 避免孤岛节点 | +| 非输出 / 结束节点必须有出边 | 避免执行到死路 | +| 无环路 | Workflow 编排必须是 DAG | +| 可达性 | 所有节点必须能从开始节点到达,并能最终到达 output/end | +| 工具节点 | 必须选择 MCP 工具;参数 JSON 必须合法;超时必须为正整数 | +| Agent 节点 | 必须填写节点指令或输入绑定;必须填写输出变量名 | +| 条件节点 | 必须填写表达式;需要 1~2 条出边;分支必须标记是/否且不能重复 | +| 连线条件 | 表达式、正则、JSONPath/JQ 路径必须通过静态校验 | +| 汇聚策略 | 必须是 `all_merge` / `last_by_canvas` / `first_non_empty` / `fail_fast` | --- -## 十、排错指南 +## 十一、排错指南 | 现象 | 可能原因 | 处理建议 | |------|----------|----------| @@ -379,25 +520,34 @@ workflow_policy: auto | 流程无最终输出 | 未命中输出节点所在分支 | 检查条件分支连线;确保至少一条路径到达 **输出** 节点 | | 角色对话未跑流程 | 角色未绑定或未启用 | 确认 `workflow_id`、`workflow_policy: auto`、流程 `enabled: true` | | 工具节点失败 | 参数 JSON 不合法或工具未启用 | 检查参数模板;在 MCP 中启用对应工具 | +| 保存失败提示分支非法 | 条件节点出边未标记是/否或重复 | 选中连线,设置条件分支为 `true` 或 `false` | +| 多上游结果不符合预期 | 汇聚策略不合适 | 根据场景改为 `all_merge` / `first_non_empty` / `last_by_canvas` / `fail_fast` | +| 嵌套字段取不到 | JSONPath/JQ 路径不符合安全子集 | 使用 `$.a.b[0]` 或 `.a.b[0]`,不要用通配符/递归/表达式 | --- -## 十一、最佳实践 +## 十二、最佳实践 1. **命名规范**:为每个需要被引用的节点设置有意义的输出变量名,如 `scan_result`、`parsed_targets`,避免都叫 `agent_result`。 2. **跨节点传参优先用 `outputs`**:只要中间可能插入条件、工具、审批节点,就应用命名变量。 3. **`previous` 仅用于直连**:A → B 且无中间节点时,`{{previous.output}}` 最简洁。 4. **条件判断引用源数据**:判断 Agent 输出时用 `{{outputs.xxx}}`,不要用 `{{previous.output}}`(除非条件紧跟在目标 Agent 之后)。 5. **每条路径都要有出口**:确保「是」「否」分支最终都能到达 **输出** 节点(或你期望的终点)。 -6. **保存前跑一遍**:用简单指令(如固定字符串输出)验证数据传递,再替换为真实业务逻辑。 +6. **多上游节点显式选择汇聚策略**:综合结果用 `all_merge`,兜底用 `first_non_empty`,关键链路用 `fail_fast`。 +7. **嵌套 JSON 用 JSONPath/JQ 安全路径**:例如 `jsonpath({{previous.output}}, "$.status") == "ok"`。 +8. **保存前先 dry-run**:用简单消息验证数据传递和分支,再绑定角色真实执行。 --- -## 十二、相关代码位置(开发者参考) +## 十三、相关代码位置(开发者参考) | 模块 | 路径 | |------|------| | 执行引擎 | `internal/workflow/runner.go` | +| Eino 编译 / checkpoint / HITL | `internal/workflow/eino_compile.go` | +| 图校验 | `internal/workflow/validation.go` | +| 表达式 / JSONPath / 汇聚 | `internal/workflow/expression.go`、`jsonpath.go`、`join.go` | +| dry-run / replay 数据 | `internal/workflow/dry_run.go`、`internal/handler/workflow_run.go` | | 画布前端 | `web/static/js/workflows.js` | | 流程 API | `internal/handler/workflow.go` | | 角色绑定 | `internal/config/config.go`(`workflow_id` 字段) | diff --git a/docs/workflow-graph_en.md b/docs/workflow-graph_en.md index b4a63627..a4b4223c 100644 --- a/docs/workflow-graph_en.md +++ b/docs/workflow-graph_en.md @@ -27,9 +27,10 @@ Saved workflows can be bound to a role under **Role Management**. When `workflow | Select | Click a node or edge; properties appear in the right panel | | Delete selected | Remove the current node or edge | | Auto layout | Rearrange node positions | +| Dry run | Safely simulate data flow; Tool, Agent, and HITL nodes are not executed for real | | Delete workflow | Remove the entire workflow definition | -**Requirements:** Every workflow needs at least **one Start node** and **one Output node**. Start nodes must not have incoming edges; Output nodes must not have outgoing edges. +**Hard requirements:** Every workflow needs at least **one Start node** and **one Output node**. Start nodes must not have incoming edges; Output / End nodes must not have outgoing edges. Both frontend and backend run strict validation before save. --- @@ -45,6 +46,7 @@ During a run, the engine keeps internal state. Template expressions `{{...}}` re | `lastOutput` | `{{previous.xxx}}` | Output of the **most recently executed** node | | `outputs` | `{{outputs.xxx}}` | Global **named variable pool** (written by nodes with an output key) | | `nodeOutputs` | `{{nodeId.xxx}}` | Full output object of a specific node ID | +| `metrics` | available in run details | Node duration, tool call count, and usage/cost metrics when reported | ### 3.1 What is `previous`? @@ -69,6 +71,15 @@ Start → Agent A → Condition → Agent B For Agent B, `{{previous.output}}` = the **condition node** output (`true` / `false`), **not** Agent A’s result. +If a node has **multiple upstream nodes**, `previous` is built by that node’s **join strategy** first: + +| Join strategy | Meaning | Use case | +|---------------|---------|----------| +| `all_merge` | Merge all upstream outputs; `previous.output` is an array | Default; aggregate multiple results | +| `last_by_canvas` | Use the last upstream output by canvas order | Explicitly use one branch | +| `first_non_empty` | Use the first non-empty output | Fallback chains | +| `fail_fast` | Stop the node if any upstream failed | Critical gates, approval prechecks, safety checks | + ### 3.2 What is `outputs`? `outputs` is a **named variable registry** maintained by the engine during execution. @@ -135,21 +146,55 @@ Allowed characters in paths: letters, digits, underscore, dot, hyphen. Examples: | `{{previous.matched}}` | Match result of the previous condition node (`true` / `false`) | | `{{outputs.variable_name}}` | Named output registered by a node | | `{{nodeId.output}}` | `output` field of the node with that ID | +| `{{previous.kind}}` | Previous node output kind, e.g. `agent` / `tool` / `condition` | +| `{{previous.status}}` | Previous node status, e.g. `completed` / `failed` / `simulated` | + +Node outputs keep compatibility fields such as `output` and `matched`, and also include a structured envelope: + +```json +{ + "kind": "agent", + "node_id": "node-2", + "node_type": "agent", + "status": "completed", + "output": "..." +} +``` ### 4.3 Condition expressions -Condition nodes and edge conditions support simple comparisons: +Condition nodes and edge conditions support comparisons, text matching, regex, logical operators, and safe JSONPath/JQ path reads: ```text {{outputs.agent_result1}} != "" {{previous.output}} == "ok" -{{outputs.count}} == "100" +{{outputs.count}} >= 100 +{{previous.output}} contains "success" +{{previous.output}} matches "^ok" +{{outputs.risk_score}} >= 8 && {{previous.output}} != "" +jsonpath({{previous.output}}, "$.status") == "ok" +jq({{outputs.scan}}, ".severity") == "high" ``` Rules: -- Use `==` or `!=` for string comparison (leading/trailing spaces and quotes are trimmed) +- Operators: `==`, `!=`, `>`, `>=`, `<`, `<=` +- `contains` checks substrings; `matches` checks regular expressions +- Simple `&&` / `||` is supported +- `jsonpath(value, "$.path")` and `jq(value, ".path")` support a **safe path-only subset**; no arbitrary script execution +- Leading/trailing spaces and quotes are trimmed before comparison - Without a comparator, non-empty values that are not `false`, `0`, or `null` are treated as true +- Expressions, regexes, and JSONPath/JQ paths are statically validated before save + +### 4.4 Nested field binding + +Field bindings can read ordinary fields such as `output` or `message`, and also JSONPath/JQ-style paths: + +| Binding | Meaning | +|---------|---------| +| `from=previous, field=$.status` | Read `status` from previous output | +| `from=outputs, field=$.scan.severity` | Read a nested field from named outputs | +| `from=node-1, field=.output.items[0]` | Read an array element from a specific node output | --- @@ -175,6 +220,7 @@ Runs an LLM Agent task. Supports multiple modes. | Input source | Template for upstream data | `{{previous.output}}` | | Node instruction | Task description for this node | empty | | Output variable name | Key written into `outputs` | `agent_result` | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | **Message assembly:** @@ -186,6 +232,7 @@ After execution: - `previous.output` becomes this node’s response text - If **Output variable name** is set, the value is also stored in `outputs[variable_name]` +- In the Eino graph, the Agent node is split into `prepare → execute → finalize` for clearer trace and future checkpointing ### 5.3 Tool @@ -196,6 +243,7 @@ Calls an enabled MCP tool. | MCP tool | Tool name (required) | — | | Argument template | JSON with `{{...}}` templates | `{}` | | Timeout (seconds) | Optional | empty | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | Example argument template: @@ -212,6 +260,7 @@ Evaluates an expression and outputs `matched` (`true` / `false`). | Field | Description | Default | |-------|-------------|---------| | Expression | Supports `{{...}}` and `==` / `!=` | `{{previous.output}} != ""` | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | **Branching rules:** @@ -229,12 +278,21 @@ Edge condition examples (select an edge, configure in the right panel): ### 5.5 HITL (human-in-the-loop) -Human approval checkpoint (currently record-only; marks `approved: true` and continues). +Human approval checkpoint. The run pauses before this node through Eino interrupt/checkpoint and resumes after approval via API or the monitor panel. | Field | Description | Default | |-------|-------------|---------| | Prompt | Supports templates | `Please approve before continuing` | +| Prompt binding | If prompt text is empty, read approval text from a bound field | `previous.output` | | Reviewer | `human` / `audit_agent` | `human` | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | + +Pending HITL metadata records: + +- `checkpointId` +- interrupt `beforeNodes` +- resume target / address / path +- resume payload schema (`approved`, `comment`) ### 5.6 Output @@ -244,6 +302,8 @@ Writes the final workflow result into `outputs` for summary and chat display. |-------|-------------|---------| | Output variable name | Required key for the final result | `result` | | Variable source | Template deciding what to write | `{{previous.output}}` | +| Static output value | Optional; overrides variable source when set | empty | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | **Note:** Output nodes are workflow exits and must not have outgoing edges. @@ -254,6 +314,7 @@ Optional node for an end summary template (less common in role-bound flows). | Field | Description | Default | |-------|-------------|---------| | Result template | Supports `{{outputs.xxx}}` | `{{outputs.result}}` | +| Join strategy | How to build `previous` when multiple upstreams enter this node | `all_merge` | --- @@ -353,7 +414,80 @@ If no Output node is reached or no branch matches, `outputs` may be empty and th --- -## 9. Validation before save +## 9. Debugging, dry-run, and replay + +### 9.1 Safe dry-run + +Click **Dry run** on the canvas toolbar and enter a test message to simulate the workflow. + +Dry-run safety rules: + +- `start` / `condition` / `output` / `end` use real logic +- `tool` does not call MCP; it returns `[dry-run] tool call skipped` +- `agent` does not call the model; it returns `[dry-run] agent execution skipped` +- `hitl` does not pause; it simulates approval + +API: + +```http +POST /api/workflows/dry-run +``` + +Request: + +```json +{ + "graph": { "nodes": [], "edges": [], "config": {} }, + "inputs": { "message": "ping" } +} +``` + +Response includes: + +- `outputs` +- `nodeOutputs` +- `trace` +- `metrics` +- `replayScript` + +### 9.2 Run details and replay + +Query full node execution traces after a run: + +```http +GET /api/workflows/runs/{runId} +``` + +The response contains `run` and `nodeRuns`. Each node run records: + +- input snapshot +- output snapshot +- status / error +- started_at / finished_at +- `duration_ms` + +Replay API: + +```http +GET /api/workflows/runs/{runId}/replay +``` + +This generates replay steps from saved `nodeRuns`; it does not re-execute tools or Agents. + +### 9.3 Metrics + +The workflow accumulates, when available: + +- `node_count` +- `duration_ms` +- `tool_call_count` +- Agent progress usage such as `prompt_tokens` / `completion_tokens` / `total_tokens` / `cost` + +Token and cost metrics depend on whether the underlying model/Agent events report usage. + +--- + +## 10. Validation before save On save, the system checks: @@ -363,13 +497,20 @@ On save, the system checks: | Output node required | At least one `output` node with an output variable name | | Valid edges | Source and target exist; no self-loops | | Start has no incoming edges | Start must not be targeted | -| Output has no outgoing edges | Nothing after Output | -| Tool nodes | MCP tool must be selected | -| Condition nodes | Expression required; ideally 1–2 outgoing edges (yes/no) | +| Output / End has no outgoing edges | Nothing after Output / End | +| Non-start nodes must have incoming edges | Prevent orphan nodes | +| Non-output/end nodes must have outgoing edges | Prevent dead ends | +| No cycles | Workflow orchestration must be a DAG | +| Reachability | Every node must be reachable from Start and eventually reach output/end | +| Tool nodes | MCP tool required; argument JSON must be valid; timeout must be a positive integer | +| Agent nodes | Must have node instruction or input binding; output variable name required | +| Condition nodes | Expression required; 1–2 outgoing edges; branches must be yes/no and unique | +| Edge conditions | Expressions, regexes, and JSONPath/JQ paths must pass static validation | +| Join strategy | Must be `all_merge` / `last_by_canvas` / `first_non_empty` / `fail_fast` | --- -## 10. Troubleshooting +## 11. Troubleshooting | Symptom | Likely cause | Fix | |---------|--------------|-----| @@ -379,25 +520,34 @@ On save, the system checks: | No final output | Output node branch not reached | Verify condition wiring; ensure every path reaches an **Output** node | | Role chat does not run workflow | Role not bound or disabled | Check `workflow_id`, `workflow_policy: auto`, workflow `enabled: true` | | Tool node fails | Invalid JSON in arguments or tool disabled | Fix argument template; enable the tool in MCP settings | +| Save fails with invalid branch | Condition outgoing edges are not marked yes/no, or are duplicated | Select the edge and set branch to `true` or `false` | +| Multi-upstream result is unexpected | Join strategy does not match the workflow | Switch between `all_merge`, `first_non_empty`, `last_by_canvas`, and `fail_fast` | +| Nested field is empty | JSONPath/JQ path is outside the safe subset | Use `$.a.b[0]` or `.a.b[0]`; avoid wildcards, recursion, or expressions | --- -## 11. Best practices +## 12. Best practices 1. **Meaningful names**: Use descriptive output variable names (`scan_result`, `parsed_targets`) instead of reusing `agent_result` everywhere. 2. **Prefer `outputs` for cross-node data**: If a condition, tool, or HITL node might sit in between, use named variables. 3. **Use `previous` only for direct links**: `A → B` with nothing in between is the ideal case for `{{previous.output}}`. 4. **Conditions should reference source data**: When testing Agent output, use `{{outputs.xxx}}` unless the condition immediately follows that Agent. 5. **Every path needs an exit**: Ensure both yes and no branches eventually reach an **Output** node (or your intended end). -6. **Validate with a simple run**: Use fixed-string outputs to verify data flow before swapping in real business logic. +6. **Choose join strategy explicitly for multi-upstream nodes**: Use `all_merge` for aggregation, `first_non_empty` for fallback, and `fail_fast` for critical gates. +7. **Use JSONPath/JQ safe paths for nested JSON**: e.g. `jsonpath({{previous.output}}, "$.status") == "ok"`. +8. **Dry-run before real execution**: Validate data flow and branches with a simple message before binding the workflow to a role. --- -## 12. Code references (for developers) +## 13. Code references (for developers) | Module | Path | |--------|------| | Execution engine | `internal/workflow/runner.go` | +| Eino compile / checkpoint / HITL | `internal/workflow/eino_compile.go` | +| Graph validation | `internal/workflow/validation.go` | +| Expressions / JSONPath / joins | `internal/workflow/expression.go`, `jsonpath.go`, `join.go` | +| Dry-run / replay data | `internal/workflow/dry_run.go`, `internal/handler/workflow_run.go` | | Canvas UI | `web/static/js/workflows.js` | | Workflow API | `internal/handler/workflow.go` | | Role binding | `internal/config/config.go` (`workflow_id` field) |