06 · n8n 源码剖析:完整走查——Ada 的订单,从头到尾

本文是专栏「n8n 工作流引擎剖析」第 06 章(完整走查),承接上一篇《生命周期钩子、持久化与推送》。本篇把贯穿示例完整地重新跑一遍,全程深入,把十一篇深度剖析缝合成一条连续的故事线。
你在这里: 读完本文,每一个概念、每一个组件都已经被你看着它亲自完成了自己的工作,而且是在同一个连贯的故事里,带着每一跳上真实传递的数据。

缩写:HTTP (Hypertext Transfer Protocol,超文本传输协议)、JSON (JavaScript Object Notation,JavaScript 对象表示法)、API (Application Programming Interface,应用程序编程接口)、DB (Database,数据库)、ID (Identifier,标识符)、UI (User Interface,用户界面)、V8 (Google's JavaScript engine,谷歌的 JavaScript 引擎)、WS (WebSocket,WebSocket 协议)、REST(Representational State Transfer,表征状态转移)。

诚实声明。 下文出现的函数名、字段名、消息种类、日志格式字符串和默认值,均来自提交 c1463d0e 的源码。但具体的 ID、时间戳和取值 (执行号 1042、任务号 77、sk_live_...)都是示意性的。我并没有为写这篇指南而真的跑起一个 n8n 实例;唯一被我实际执行过的代码,是 examples/mini-engine.mjs 里的那个玩具引擎。


登场角色(提醒一下)

编辑器 · 触发器注册中心 · Webhook 接入层 · 工作流运行器 · 伸缩队列 · 执行引擎 · 节点 · 表达式引擎 · 凭据 · 任务运行器 · 钩子/持久化/推送------详见 03-concept-map。

第 A 部分 ------ 订单到来之前:搭建与发布(S1--S2)

环节 传递了什么 内部发生了什么
A1 浏览器 → 服务端 PATCH /rest/workflows/wf123,内容 { nodes:[4], connections:{...} } 工作流以 JSON 形式存储(WorkflowEntity,外加一条历史版本)。不含任何密钥。
A2 浏览器 → 服务端 凭据字段 { name:"X-Api-Key", value:"sk_live_..." } Credentials.setData → Cipher.encryptV2 → 记录 { id:cred42, type:httpHeaderAuth, data:<密文> }
A3 浏览器 → 服务端 POST /rest/workflows/wf123/activate,内容 { versionId, expectedChecksum },外加 push-ref 请求头 @ProjectScope('workflow:publish'),写锁检查 → WorkflowService.activateWorkflow
A4 服务端 → 注册中心 ActiveWorkflowManager.add('wf123','activate') 加载已发布版本;new Workflow({...active:true});acquireIsolate;getNodeWebhooks → {POST,'orders'};storeWebhook 插入记录 (orders, POST) → wf123 / "Order Webhook";releaseIsolate;触发器计数 = 1

第 A 部分结束后的状态------这是让实时接入得以工作的唯一要素:

复制代码
webhook_entity:  webhookPath=orders  method=POST  workflowId=wf123  node="Order Webhook"

第 B 部分 ------ 订单到来(S3--S6)

bash 复制代码
curl -i -X POST https://n8n.example.com/webhook/orders \
  -H 'Content-Type: application/json' \
  -d '{"customer":"ACME","amount":100}'

数据库 Redis / Bull 主进程:WorkflowRunner Webhook 节点 webhook() 主进程:Express + LiveWebhooks 商店 数据库 Redis / Bull 主进程:WorkflowRunner Webhook 节点 webhook() 主进程:Express + LiveWebhooks 商店 #mermaid-svg-hVzagrS9JpPzlMM2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hVzagrS9JpPzlMM2 .error-icon{fill:#552222;}#mermaid-svg-hVzagrS9JpPzlMM2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hVzagrS9JpPzlMM2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hVzagrS9JpPzlMM2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hVzagrS9JpPzlMM2 .marker.cross{stroke:#333333;}#mermaid-svg-hVzagrS9JpPzlMM2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hVzagrS9JpPzlMM2 p{margin:0;}#mermaid-svg-hVzagrS9JpPzlMM2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hVzagrS9JpPzlMM2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hVzagrS9JpPzlMM2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-hVzagrS9JpPzlMM2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-hVzagrS9JpPzlMM2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-hVzagrS9JpPzlMM2 .sequenceNumber{fill:white;}#mermaid-svg-hVzagrS9JpPzlMM2 #sequencenumber{fill:#333;}#mermaid-svg-hVzagrS9JpPzlMM2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-hVzagrS9JpPzlMM2 .messageText{fill:#333;stroke:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hVzagrS9JpPzlMM2 .labelText,#mermaid-svg-hVzagrS9JpPzlMM2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .loopText,#mermaid-svg-hVzagrS9JpPzlMM2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-hVzagrS9JpPzlMM2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-hVzagrS9JpPzlMM2 .noteText,#mermaid-svg-hVzagrS9JpPzlMM2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-hVzagrS9JpPzlMM2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hVzagrS9JpPzlMM2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hVzagrS9JpPzlMM2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-hVzagrS9JpPzlMM2 .actorPopupMenu{position:absolute;}#mermaid-svg-hVzagrS9JpPzlMM2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-hVzagrS9JpPzlMM2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-hVzagrS9JpPzlMM2 .actor-man circle,#mermaid-svg-hVzagrS9JpPzlMM2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-hVzagrS9JpPzlMM2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /webhook/orders {"customer":"ACME","amount":100} 1 findWebhook(POST, orders) 缓存 → 数据行 2 加载 wf123 的已发布版本 3 runWebhook(ctx) --- 认证通过,无需表达式 4 workflowData \[ {json:{headers,params,query,body}} ] 5 run(executionMode:'webhook', executionData:{stack:Order Webhook + item}) 6 credentialsPermissionChecker.check ✓ 7 INSERT 执行记录(状态 new) + 负载 → id 1042 8 addJob({executionId:1042, workflowId:wf123,...}, priority 100) 9 "1042" 10 200 {"message":"Workflow was started"} 11

图注:S3--S6------执行记录一旦存在、任务一旦入队,就立刻回复调用方。

带注释的数据流转:

环节 数据
B1 商店 → 接入层 原始 HTTP 请求;Express 把 /webhook/*path 路由到 createWebhookHandlerFor(liveWebhooks,'webhook');日志:Received webhook "POST" for path "orders"
B2 接入层 → 数据库/缓存 findCachedStaticWebhook('POST','orders') → 键 webhook:POST-orders
B3 接入层 → Webhook 节点 一个 WebhookContext,带着 req、res、节点参数(httpMethod: POST、path: orders、responseMode 默认 onReceived)
B4 Webhook 节点 → 接入层 { webhookResponse: undefined, workflowData: [[ { json: { headers:{...}, params:{}, query:{}, body:{customer:"ACME",amount:100} } } ]] }
B5 接入层 → 运行器 IWorkflowExecutionDataProcess { executionMode:'webhook', executionData: IRunExecutionData{ executionData.nodeExecutionStack:[{node:"Order Webhook", data:{main:[[item0]]}, source:null}] }, workflowData, projectId, ... }
B6 运行器 → 数据库 executionPersistence.create({ mode:'webhook', status:'new', finished:false, workflowId, data, workflowData }) → 得到 1042
B7 运行器 → Redis JobData { workflowId:'wf123', executionId:'1042', loadStaticData:false, projectId, poolName };不含任何数据项
B8 接入层 → 商店 HTTP/1.1 200 {"message":"Workflow was started"}------在 run() 返回 id 之后 才发出(这样自定义响应里就可以用上 $execution.id)

第 C 部分 ------ 工作进程运行这张图(S7--S12)

数据库 任务运行器(JS) 凭据(Cipher) 表达式引擎(V8 隔离实例) 节点(Set、HTTP、Code) 工作进程:JobProcessor + WorkflowExecute 数据库 任务运行器(JS) 凭据(Cipher) 表达式引擎(V8 隔离实例) 节点(Set、HTTP、Code) 工作进程:JobProcessor + WorkflowExecute #mermaid-svg-1NIBeNMG0llpWxO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1NIBeNMG0llpWxO7 .error-icon{fill:#552222;}#mermaid-svg-1NIBeNMG0llpWxO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1NIBeNMG0llpWxO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1NIBeNMG0llpWxO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1NIBeNMG0llpWxO7 .marker.cross{stroke:#333333;}#mermaid-svg-1NIBeNMG0llpWxO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1NIBeNMG0llpWxO7 p{margin:0;}#mermaid-svg-1NIBeNMG0llpWxO7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1NIBeNMG0llpWxO7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1NIBeNMG0llpWxO7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1NIBeNMG0llpWxO7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1NIBeNMG0llpWxO7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1NIBeNMG0llpWxO7 .sequenceNumber{fill:white;}#mermaid-svg-1NIBeNMG0llpWxO7 #sequencenumber{fill:#333;}#mermaid-svg-1NIBeNMG0llpWxO7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1NIBeNMG0llpWxO7 .messageText{fill:#333;stroke:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1NIBeNMG0llpWxO7 .labelText,#mermaid-svg-1NIBeNMG0llpWxO7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .loopText,#mermaid-svg-1NIBeNMG0llpWxO7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1NIBeNMG0llpWxO7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1NIBeNMG0llpWxO7 .noteText,#mermaid-svg-1NIBeNMG0llpWxO7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1NIBeNMG0llpWxO7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1NIBeNMG0llpWxO7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1NIBeNMG0llpWxO7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1NIBeNMG0llpWxO7 .actorPopupMenu{position:absolute;}#mermaid-svg-1NIBeNMG0llpWxO7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1NIBeNMG0llpWxO7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1NIBeNMG0llpWxO7 .actor-man circle,#mermaid-svg-1NIBeNMG0llpWxO7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1NIBeNMG0llpWxO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} new Workflow(...) · 工作进程钩子 · acquireIsolate · workflowExecuteAfter findSingleExecution(1042, includeData) · setRunning 1 弹出 "Order Webhook" → 转发 item0 2 弹出 "Add VAT" → SetV2.execute 3 getNodeParameter → "={{ $json.body.amount * 1.2 }}" 4 120 5 弹出 "Create Invoice" → HttpRequestV3.execute 6 getCredentials('httpHeaderAuth') → 解密 7 {name:'X-Api-Key', value:'sk_live_...'} 8 authenticate() 设置请求头 POST 计费 API →{invoiceId:'INV-1001'} 9 弹出 "Summarize" → Code.execute 10 startJob('javascript', {code,...}) ⇄ 数据请求/响应 11 {json:{summary:'Invoice INV-1001'}} 12 钩子 workflowExecuteAfter → 状态 success + 完整 runData 13

图注:S7--S12 在工作进程内部------一个循环,四个节点,三个专职帮手(表达式隔离实例、加密组件、任务运行器)。

引擎的状态,逐次迭代

(摘自引擎深度剖析;unshift/shift 给出的是深度优先的顺序。)

复制代码
起始   stack=[Order Webhook]                                   runData={}
第0轮  弹出 Order Webhook   → out=[item0]                      runData={Order Webhook:[t0]}       stack=[Add VAT]
第1轮  弹出 Add VAT         → out=[item0 + total:120]          runData+={Add VAT:[t0]}            stack=[Create Invoice]
第2轮  弹出 Create Invoice  → out=[{..., invoiceId:'INV-1001'}]  runData+={Create Invoice:[t0]}     stack=[Summarize]
第3轮  弹出 Summarize       → out=[{summary:'Invoice INV-1001'}] runData+={Summarize:[t0]}        stack=[]  → 循环结束

每次迭代之间,引擎会:(1)给输入打上 pairedItem 标记,(2)触发 nodeExecuteBefore/After,(3)对每条输出非空的连接调用 addNodeToBeExecuted。

每个节点上的数据

节点 输入数据项(json) 输出数据项(json)
Order Webhook {headers,params,query,body:{customer:"ACME",amount:100}} 相同(原样转发)
Add VAT 相同 {...同上, total: 120}------来自表达式的赋值
Create Invoice {..., total:120} {invoiceId:"INV-1001", ...}------这是HTTP 响应体 ;请求头里的密钥不在这里
Summarize {invoiceId:"INV-1001", ...} {summary:"Invoice INV-1001"}------在任务运行器里算出来的

存储下来的产物(精简版 resultData.runData)

json 复制代码
{
  "Order Webhook":  [ { "startTime": 1790000000000, "executionTime": 1, "executionStatus": "success",
                        "source": [], "data": { "main": [[ { "json": { "body": { "customer": "ACME", "amount": 100 } }, "pairedItem": { "item": 0 } } ]] } } ],
  "Add VAT":        [ { "executionStatus": "success", "source": [ { "previousNode": "Order Webhook" } ],
                        "data": { "main": [[ { "json": { "body": { "...": "..." }, "total": 120 }, "pairedItem": { "item": 0 } } ]] } } ],
  "Create Invoice": [ { "executionStatus": "success", "source": [ { "previousNode": "Add VAT" } ],
                        "data": { "main": [[ { "json": { "invoiceId": "INV-1001" }, "pairedItem": { "item": 0 } } ]] } } ],
  "Summarize":      [ { "executionStatus": "success", "source": [ { "previousNode": "Create Invoice" } ],
                        "data": { "main": [[ { "json": { "summary": "Invoice INV-1001" }, "pairedItem": { "item": 0 } } ]] } } ]
}

(结构来自 ITaskData;具体取值为示意性数据。)

debug 级别的日志(来自源码的真实格式字符串)

复制代码
[webhook 进程]  Received webhook "POST" for path "orders"
[webhook 进程]  Started execution of workflow "Order intake" from webhook with execution ID 1042
[工作进程]      Worker started execution 1042 (job 77)
[工作进程]      Start executing node "Add VAT"
[工作进程]      Running node "Add VAT" started
[工作进程]      Running node "Add VAT" finished successfully
   ......(Create Invoice、Summarize 也各有同样的三行)......
[主进程]        Execution 1042 (job 77) finished

第 D 部分 ------ 返回路径(S12)

Ada 的浏览器 数据库 主进程:ScalingService Redis (global:progress) 工作进程 Ada 的浏览器 数据库 主进程:ScalingService Redis (global:progress) 工作进程 #mermaid-svg-q2EEAABLE51S7NdL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q2EEAABLE51S7NdL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q2EEAABLE51S7NdL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q2EEAABLE51S7NdL .error-icon{fill:#552222;}#mermaid-svg-q2EEAABLE51S7NdL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q2EEAABLE51S7NdL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q2EEAABLE51S7NdL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q2EEAABLE51S7NdL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q2EEAABLE51S7NdL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q2EEAABLE51S7NdL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q2EEAABLE51S7NdL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q2EEAABLE51S7NdL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q2EEAABLE51S7NdL .marker.cross{stroke:#333333;}#mermaid-svg-q2EEAABLE51S7NdL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q2EEAABLE51S7NdL p{margin:0;}#mermaid-svg-q2EEAABLE51S7NdL .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q2EEAABLE51S7NdL text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-q2EEAABLE51S7NdL .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-q2EEAABLE51S7NdL .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-q2EEAABLE51S7NdL .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-q2EEAABLE51S7NdL .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-q2EEAABLE51S7NdL #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-q2EEAABLE51S7NdL .sequenceNumber{fill:white;}#mermaid-svg-q2EEAABLE51S7NdL #sequencenumber{fill:#333;}#mermaid-svg-q2EEAABLE51S7NdL #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-q2EEAABLE51S7NdL .messageText{fill:#333;stroke:none;}#mermaid-svg-q2EEAABLE51S7NdL .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q2EEAABLE51S7NdL .labelText,#mermaid-svg-q2EEAABLE51S7NdL .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-q2EEAABLE51S7NdL .loopText,#mermaid-svg-q2EEAABLE51S7NdL .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-q2EEAABLE51S7NdL .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-q2EEAABLE51S7NdL .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-q2EEAABLE51S7NdL .noteText,#mermaid-svg-q2EEAABLE51S7NdL .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-q2EEAABLE51S7NdL .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q2EEAABLE51S7NdL .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q2EEAABLE51S7NdL .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q2EEAABLE51S7NdL .actorPopupMenu{position:absolute;}#mermaid-svg-q2EEAABLE51S7NdL .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-q2EEAABLE51S7NdL .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q2EEAABLE51S7NdL .actor-man circle,#mermaid-svg-q2EEAABLE51S7NdL line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-q2EEAABLE51S7NdL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 没有发生推送: 生产运行没有 pushRef 保存最终执行记录 (status success, finished true, runData) job.progress({kind:'job-finished', version:2, executionId:1042, status:'success', startedAt, stoppedAt, ...}) global:progress → 解析响应 Promise, 存入 jobResults1042 主进程一侧的钩子: 保留记录(成功会被保存) (REST) GET executions → 第 1042 行 + 负载

图注:工作进程负责写结果;那条任务消息只是一个通知------主进程读取结果走的是数据库,不是 Redis。

第 E 部分 ------ 自己动手跑一遍这个形状

bash 复制代码
cd n8n-guide/examples && node mini-engine.mjs

输出(已验证真实跑过):

复制代码
ran Order Webhook   -> [{"body":{"customer":"ACME","amount":100}}]
ran Add VAT         -> [{"body":{"customer":"ACME","amount":100},"total":120}]
ran Create Invoice  -> [{"body":{...},"total":120,"invoiceId":"INV-1001","_called":"https://billing.example.com/invoices"}]
ran Summarize       -> [{"summary":"Invoice INV-1001 for 120"}]

这个玩具实现用的是同样的四个动词------弹出、运行、记录、把子节点入队 ------建立在同样的数据模型之上(nodes、connections、数据项、一个栈、runData)。它省略了一切让 n8n 之所以成为 n8n 的东西:多输入等待、重试/onError、表达式与凭据、pairedItem 溯源链、钩子、持久化、取消与队列。

一个失败变体(已追踪的边缘流程)

如果计费 API 返回了 500 会怎样? (见引擎深度剖析的"失败行为"一节。)
#mermaid-svg-xFszLhLxLl4blKc8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xFszLhLxLl4blKc8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xFszLhLxLl4blKc8 .error-icon{fill:#552222;}#mermaid-svg-xFszLhLxLl4blKc8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xFszLhLxLl4blKc8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xFszLhLxLl4blKc8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xFszLhLxLl4blKc8 .marker.cross{stroke:#333333;}#mermaid-svg-xFszLhLxLl4blKc8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xFszLhLxLl4blKc8 p{margin:0;}#mermaid-svg-xFszLhLxLl4blKc8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xFszLhLxLl4blKc8 .cluster-label text{fill:#333;}#mermaid-svg-xFszLhLxLl4blKc8 .cluster-label span{color:#333;}#mermaid-svg-xFszLhLxLl4blKc8 .cluster-label span p{background-color:transparent;}#mermaid-svg-xFszLhLxLl4blKc8 .label text,#mermaid-svg-xFszLhLxLl4blKc8 span{fill:#333;color:#333;}#mermaid-svg-xFszLhLxLl4blKc8 .node rect,#mermaid-svg-xFszLhLxLl4blKc8 .node circle,#mermaid-svg-xFszLhLxLl4blKc8 .node ellipse,#mermaid-svg-xFszLhLxLl4blKc8 .node polygon,#mermaid-svg-xFszLhLxLl4blKc8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xFszLhLxLl4blKc8 .rough-node .label text,#mermaid-svg-xFszLhLxLl4blKc8 .node .label text,#mermaid-svg-xFszLhLxLl4blKc8 .image-shape .label,#mermaid-svg-xFszLhLxLl4blKc8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-xFszLhLxLl4blKc8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xFszLhLxLl4blKc8 .rough-node .label,#mermaid-svg-xFszLhLxLl4blKc8 .node .label,#mermaid-svg-xFszLhLxLl4blKc8 .image-shape .label,#mermaid-svg-xFszLhLxLl4blKc8 .icon-shape .label{text-align:center;}#mermaid-svg-xFszLhLxLl4blKc8 .node.clickable{cursor:pointer;}#mermaid-svg-xFszLhLxLl4blKc8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xFszLhLxLl4blKc8 .arrowheadPath{fill:#333333;}#mermaid-svg-xFszLhLxLl4blKc8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xFszLhLxLl4blKc8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xFszLhLxLl4blKc8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xFszLhLxLl4blKc8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xFszLhLxLl4blKc8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xFszLhLxLl4blKc8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xFszLhLxLl4blKc8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xFszLhLxLl4blKc8 .cluster text{fill:#333;}#mermaid-svg-xFszLhLxLl4blKc8 .cluster span{color:#333;}#mermaid-svg-xFszLhLxLl4blKc8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xFszLhLxLl4blKc8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xFszLhLxLl4blKc8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-xFszLhLxLl4blKc8 .icon-shape,#mermaid-svg-xFszLhLxLl4blKc8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xFszLhLxLl4blKc8 .icon-shape p,#mermaid-svg-xFszLhLxLl4blKc8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xFszLhLxLl4blKc8 .icon-shape .label rect,#mermaid-svg-xFszLhLxLl4blKc8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xFszLhLxLl4blKc8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xFszLhLxLl4blKc8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xFszLhLxLl4blKc8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否(默认)
是
stopWorkflow
continueRegularOutput
continueErrorOutput
Create Invoice: HTTP 500
requestWithAuthentication

抛出 NodeApiError
retryOnFail?
handleNodeExecutionError
sleep waitBetweenTries;

再试一次(最多 2--5 次)
onError
taskData.error 已保存;

节点推回栈;

循环 break
输入数据项照常

传给 Summarize
错误数据项送到输出 2
钩子 workflowExecuteAfter:

status error

  • resultData.error

  • lastNodeExecuted
    保存(默认会保存错误)

  • 按配置触发

executeErrorWorkflow

图注:一个节点失败,三种可能的路由------默认行为是停止运行,但会保留目前为止已经算出的一切。

回顾------问题是怎么被解决的

第 1 阶段里的痛点是胶水代码:毫无可见性、密钥散落各处、只有开发者能改、扩容全靠自己硬凑。在这次走查里:

  • 业务流程是 Ada 画出来的一张 JSON 图 ,而不是代码(工作流、节点、连接)。
  • 外部世界通过一条注册过的 webhook 记录 进入系统,变成最初的数据项 (触发器、接入层)。
  • 这次运行是一条持久化的执行 记录,它的全部状态都活在一个可序列化的对象里,所以一个队列 才能只凭一个 ID 就把它交给某个工作进程 (运行器、伸缩队列、引擎)。
  • 具体的值由表达式 在沙箱中算出;Ada 自定义的逻辑跑在一个任务运行器里------从来不在服务端自己的内存中执行。
  • 凭据始终保持加密状态,只为一次请求解密一次,从不进入运行记录。
  • 每个节点的输入和输出都被记录 下来,所以 Ada 能准确审查发生过什么(钩子、持久化)。

这就是整个设计的全部:图即数据,一个引擎负责遍历它,密钥只有一个存放之处,每次运行都有一份记录。

下一篇:《后续步骤:练习题、源码入口与未覆盖内容》,本系列的最后一篇。


📚 返回专栏目录

相关推荐
SL_staff17 小时前
私有化低代码不是减配版开发:JVS双模式建模如何重建IT交付信任
java·低代码·spring cloud
Java小卷2 天前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
细水长流6453 天前
异常工单自动聚类:同类问题一屏看清
低代码
驰骋工作流3 天前
生产环境版本锁定与升级 SOP:从 Ver 号对账到回滚预案驰骋BPM驰骋低代码低代码工作流引擎
低代码
QianhangQianping4 天前
WebSocket实时推送+ HTTP 定时轮询:一块大屏两套取数怎么不打架
前端·websocket·网络协议·http·低代码·架构·数据可视化
SL_staff4 天前
3分钟完成业务规则调整:告别代码修改,实现拖拽式逻辑配置
java·低代码·全栈
驰骋工作流6 天前
流程时效考核的表结构:从待办时限到 WF_CH 考核结果驰骋BPM驰骋低代码低代码工作流引擎
android·低代码·rxjava
三号路口7 天前
组件库的版本锁定:升级前先冻住
低代码