The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Bilig WorkPaper forecast approval guard",
"nodes": [
{
"parameters": {},
"id": "4d67376c-8c9c-4e62-8a4d-2d2c601d7278",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "const baseUrl = 'https://bilig.proompteng.ai';\n\nreturn [\n {\n json: {\n requestId: 'deal-1042-renewal',\n account: 'Northstar Systems',\n owner: 'revops',\n sheetName: 'Inputs',\n address: 'B3',\n value: 0.4,\n minExpectedArr: 90000,\n maxTargetGap: 6000,\n baseUrl\n }\n },\n {\n json: {\n requestId: 'deal-1188-expansion',\n account: 'Harbor Analytics',\n owner: 'finance',\n sheetName: 'Inputs',\n address: 'B4',\n value: 6500,\n minExpectedArr: 90000,\n maxTargetGap: 6000,\n baseUrl\n }\n },\n {\n json: {\n requestId: 'deal-1211-risk-review',\n account: 'Cedar Works',\n owner: 'sales-ops',\n sheetName: 'Inputs',\n address: 'B3',\n value: 0.22,\n minExpectedArr: 90000,\n maxTargetGap: 6000,\n baseUrl\n }\n }\n];"
},
"id": "8ff11031-a7bf-40e8-8ad8-b6787950f253",
"name": "Load forecast requests",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
0
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n const value = Number(row.value);\n\n if (!Number.isFinite(value)) {\n throw new Error(`Forecast value must be numeric for ${row.requestId}`);\n }\n\n if (!['B2', 'B3', 'B4', 'B5'].includes(row.address)) {\n throw new Error(`Unsupported forecast input cell ${row.address} for ${row.requestId}`);\n }\n\n return {\n json: {\n ...row,\n value,\n minExpectedArr: Number(row.minExpectedArr),\n maxTargetGap: Number(row.maxTargetGap),\n normalizedAt: new Date().toISOString()\n }\n };\n});"
},
"id": "98033e3d-2e8b-42db-97d8-55ca54a680a0",
"name": "Validate request rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
520,
0
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n\n return {\n json: {\n ...row,\n biligPayload: {\n sheetName: row.sheetName,\n address: row.address,\n value: row.value\n }\n }\n };\n});"
},
"id": "e58be8f1-ad37-4573-82d9-1ff55ef1e3d7",
"name": "Build Bilig request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
780,
0
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.baseUrl.replace(/\\/$/, '') + '/api/workpaper/n8n/forecast' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.biligPayload) }}",
"options": {}
},
"id": "9f955101-f349-4023-ae05-00d6fbc1d9a5",
"name": "Call Bilig WorkPaper",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1060,
0
]
},
{
"parameters": {
"jsCode": "const scenarios = $('Build Bilig request').all();\n\nreturn $input.all().map((item, index) => {\n const proof = item.json;\n const scenario = scenarios[index]?.json;\n\n if (!scenario) {\n throw new Error(`Missing source scenario for Bilig response ${index}`);\n }\n\n if (proof.verified !== true) {\n throw new Error(`Bilig did not verify ${scenario.requestId}: ${JSON.stringify(proof)}`);\n }\n\n if (!proof.checks?.formulasPersisted || !proof.checks?.restoredMatchesAfter || !proof.checks?.computedOutputChanged) {\n throw new Error(`Formula checks failed for ${scenario.requestId}: ${JSON.stringify(proof.checks)}`);\n }\n\n return {\n json: {\n ...scenario,\n proof,\n editedCell: proof.editedCell,\n beforeExpectedArr: proof.before.expectedArr,\n afterExpectedArr: proof.after.expectedArr,\n targetGap: proof.after.targetGap,\n formulaChecks: proof.checks\n }\n };\n});"
},
"id": "cc1b7abe-a792-4d2e-bd4a-5cad59b2a371",
"name": "Verify formula proof",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
0
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n const expectedArrOk = row.afterExpectedArr >= row.minExpectedArr;\n const targetGapOk = Math.abs(row.targetGap) <= row.maxTargetGap;\n const decision = expectedArrOk && targetGapOk ? 'approve' : 'review';\n\n return {\n json: {\n ...row,\n expectedArrOk,\n targetGapOk,\n decision,\n decisionReason: decision === 'approve'\n ? 'Fresh formula output is within policy.'\n : 'Fresh formula output needs owner review before the workflow continues.'\n }\n };\n});"
},
"id": "fbb705f0-4434-4be9-aecb-7fffe4572f6d",
"name": "Apply approval policy",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1600,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "2dd99761-9350-4a2e-9704-5e7a75f6084d",
"leftValue": "={{ $json.decision }}",
"rightValue": "approve",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "0b97d466-0e96-434d-9552-c1e9c81be8ab",
"name": "Approved?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1880,
0
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n\n return {\n json: {\n status: 'approved',\n requestId: row.requestId,\n account: row.account,\n owner: row.owner,\n editedCell: row.editedCell,\n expectedArr: row.afterExpectedArr,\n targetGap: row.targetGap,\n evidence: {\n verified: row.proof.verified,\n formulasPersisted: row.formulaChecks.formulasPersisted,\n restoredMatchesAfter: row.formulaChecks.restoredMatchesAfter,\n computedOutputChanged: row.formulaChecks.computedOutputChanged\n }\n }\n };\n});"
},
"id": "af4569b6-86f9-4f2d-8d17-ecf5f9a84327",
"name": "Build approval record",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2140,
-120
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n\n return {\n json: {\n status: 'needs_review',\n requestId: row.requestId,\n account: row.account,\n owner: row.owner,\n editedCell: row.editedCell,\n expectedArr: row.afterExpectedArr,\n targetGap: row.targetGap,\n failedPolicy: {\n expectedArrOk: row.expectedArrOk,\n targetGapOk: row.targetGapOk\n },\n reviewerNote: `${row.account} needs review because the freshly recalculated workbook output missed policy.`\n }\n };\n});"
},
"id": "8d14755e-60ab-435b-b0ea-1904a427725d",
"name": "Build review record",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2140,
140
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => {\n const row = item.json;\n\n return {\n json: {\n ...row,\n auditSummary: `${row.status}: ${row.account} ${row.editedCell} expected ARR ${row.expectedArr} target gap ${row.targetGap}`,\n nextStep: row.status === 'approved'\n ? 'Send this record to CRM, Slack, or a quote approval system.'\n : 'Route this record to the deal owner before the workflow continues.',\n github: 'https://github.com/proompteng/bilig',\n package: '@bilig/workpaper'\n }\n };\n});"
},
"id": "f25ae7ad-661f-40d2-948a-0f297dcba8b4",
"name": "Write audit summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2420,
0
]
},
{
"parameters": {
"content": "## Sample forecast changes\n\nRun the workflow once as-is. It creates three forecast-change requests so both approval paths execute.\n\nReplace the rows in `Load forecast requests` with CRM, form, or queue data. Keep `requestId`, `account`, `owner`, workbook cell, proposed value, and thresholds together.",
"height": 280,
"width": 520,
"color": 4
},
"id": "1deff2ac-a11b-44d8-a437-cfb5dd62cb6f",
"name": "Sticky - sample forecast changes",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-20,
-500
]
},
{
"parameters": {
"content": "## Reject bad rows before HTTP\n\n`Validate request rows` stops non-numeric values, missing owners, unsupported cells, and invalid thresholds before the WorkPaper service is called.\n\nPut tenant, account, or permission checks here so bad data fails before formula output is trusted.",
"height": 260,
"width": 520,
"color": 5
},
"id": "97f8f640-db26-4af9-ad48-b3eb866d54a9",
"name": "Sticky - validate rows first",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
560,
-500
]
},
{
"parameters": {
"content": "## Recalculate in Bilig\n\n`Build Bilig request` sends one workbook edit: `sheetName`, `address`, and `value`. `Call Bilig WorkPaper` recalculates formulas and returns before/after readback.\n\nFor private data, replace `baseUrl` with your internal Bilig service.",
"height": 300,
"width": 640,
"color": 6
},
"id": "407664f7-c30d-4600-bd8d-5c36cfcb3d43",
"name": "Sticky - Bilig formula readback",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1100,
-500
]
},
{
"parameters": {
"content": "## Do not route unverified numbers\n\n`Verify formula proof` fails unless Bilig reports `verified`, formulas persisted, restored JSON matches, and the computed value changed.\n\n`Apply approval policy` reads only that verified output before choosing approve or review.",
"height": 280,
"width": 620,
"color": 3
},
"id": "62bb2c8b-f9df-48e7-9c8e-282a76a28426",
"name": "Sticky - verify before routing",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1280,
280
]
},
{
"parameters": {
"content": "## Swap in real destinations\n\nThe IF node builds separate approved and review records so downstream systems receive clean payloads.\n\nReplace `Write audit summary` with Slack, Gmail, HubSpot, Airtable, or a queue. Keep the Bilig proof fields in the final record.",
"height": 300,
"width": 620,
"color": 7
},
"id": "f676a33b-d669-4ac2-95e7-201b113060e2",
"name": "Sticky - destination handoff",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1940,
280
]
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Load forecast requests",
"type": "main",
"index": 0
}
]
]
},
"Load forecast requests": {
"main": [
[
{
"node": "Validate request rows",
"type": "main",
"index": 0
}
]
]
},
"Validate request rows": {
"main": [
[
{
"node": "Build Bilig request",
"type": "main",
"index": 0
}
]
]
},
"Build Bilig request": {
"main": [
[
{
"node": "Call Bilig WorkPaper",
"type": "main",
"index": 0
}
]
]
},
"Call Bilig WorkPaper": {
"main": [
[
{
"node": "Verify formula proof",
"type": "main",
"index": 0
}
]
]
},
"Verify formula proof": {
"main": [
[
{
"node": "Apply approval policy",
"type": "main",
"index": 0
}
]
]
},
"Apply approval policy": {
"main": [
[
{
"node": "Approved?",
"type": "main",
"index": 0
}
]
]
},
"Approved?": {
"main": [
[
{
"node": "Build approval record",
"type": "main",
"index": 0
}
],
[
{
"node": "Build review record",
"type": "main",
"index": 0
}
]
]
},
"Build approval record": {
"main": [
[
{
"node": "Write audit summary",
"type": "main",
"index": 0
}
]
]
},
"Build review record": {
"main": [
[
{
"node": "Write audit summary",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "7a7b33b5-0a60-41df-a6c5-3438f10fe04a",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "BiligWorkPaperForecastApprovalGuard",
"tags": []
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Bilig WorkPaper forecast approval guard. Uses httpRequest. Event-driven trigger; 16 nodes.
Source: https://github.com/proompteng/bilig/blob/37f9db9ef1afb2ad05913d23fd1a64a1f3f9959c/examples/n8n-workpaper-formula-readback/bilig-workpaper-forecast-approval-guard.n8n.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow listens for an “Approved” label on a Trello card, reads the AI draft bookkeeping JSON from card comments, and posts the corresponding transaction to Xero. It then adds a Xero deep link b
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
This workflow allows you to import any workflow from a file or another n8n instance and map the credentials easily. A multi-form setup guides you through the entire process At the beginning you have t
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.
[](https://youtu.be/c7yCZhmMjtI)