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 能准确审查发生过什么(钩子、持久化)。

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

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


📚 返回专栏目录

相关推荐
百数平台11 小时前
六大 AI 智能体业务场景:表单、巡检、合同审查落地实践
低代码·ai
wewowilling12 小时前
工作流 .... coze平台--AI--Agent--RAG--Function Calling
python·低代码·agent·零代码·工作流·coze
流星。。方2 天前
老官网改版实录:3步保留URL与收录,渐进式适配移动端
低代码
液态不合群2 天前
AI低代码政务落地:破解信息化低效困局的核心逻辑
人工智能·低代码·数字化·政务
RuoyiOffice2 天前
Springboot + Vue3 + FormCreate表单引擎,直接用AI对话进行单据设计,告别拖拽画表单
spring boot·低代码·vue3·sse·formcreate·ruoyi office·ai表单
SL_staff2 天前
JVS混合架构实践:能力引擎+动态模型如何支撑企业级可持续交付
spring boot·低代码·全栈
葡萄城技术团队2 天前
活字格企业级低代码深度解析:拖拽之外,支撑复杂业务的底层技术逻辑
低代码
wewowilling4 天前
Coze智能体入门 --- AI Agent开发入门--第一章
python·低代码·agent·零代码·coze
Geek-Chow4 天前
05.03 · n8n 源码剖析:Webhook 接入层 Webhook Ingress
低代码