本文是专栏「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 能准确审查发生过什么(钩子、持久化)。
这就是整个设计的全部:图即数据,一个引擎负责遍历它,密钥只有一个存放之处,每次运行都有一份记录。
下一篇:《后续步骤:练习题、源码入口与未覆盖内容》,本系列的最后一篇。
📚 返回专栏目录