This workflow follows the Execute Workflow Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
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": "AP Invoice \u2014 02 Pre-Posting Gate",
"nodes": [
{
"parameters": {},
"id": "913d96cb-8a47-4f0a-9af1-c6003a60ca1d",
"name": "When Called by Orchestrator",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
-208,
240
]
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\n\ntry {\n const lines = item.journalLines;\n\n if (!Array.isArray(lines) || lines.length === 0) {\n throw new Error('journalLines is empty or missing');\n }\n\n const skipChecks = item.options?.skipChecks ?? [];\n if (skipChecks.includes('balance')) {\n return [{ json: { ...item, passed: true, failedCheck: null, reason: null } }];\n }\n\n let totalDebits = 0;\n let totalCredits = 0;\n\n for (const line of lines) {\n const amount = parseFloat(line.amount);\n if (isNaN(amount) || amount < 0) {\n throw new Error('Invalid amount on account ' + line.accountCode + ': ' + line.amount);\n }\n if (line.type === 'debit') {\n totalDebits += amount;\n } else if (line.type === 'credit') {\n totalCredits += amount;\n } else {\n throw new Error('Invalid line type on account ' + line.accountCode + ': must be debit or credit, got: ' + line.type);\n }\n }\n\n // Round to 2dp before comparing \u2014 avoids floating-point drift (e.g. 0.1 + 0.2 !== 0.3)\n const roundedDebits = parseFloat(totalDebits.toFixed(2));\n const roundedCredits = parseFloat(totalCredits.toFixed(2));\n\n if (roundedDebits !== roundedCredits) {\n return [{\n json: {\n ...item,\n passed: false,\n failedCheck: 'balance',\n reason: 'Debits (' + roundedDebits.toFixed(2) + ') do not equal credits (' + roundedCredits.toFixed(2) + '). Difference: ' + Math.abs(roundedDebits - roundedCredits).toFixed(2)\n }\n }];\n }\n\n return [{ json: { ...item, passed: true, failedCheck: null, reason: null } }];\n\n} catch (err) {\n return [{\n json: {\n ...item,\n passed: false,\n failedCheck: 'balance',\n reason: 'Balance check error: ' + err.message\n }\n }];\n}"
},
"id": "d18d1aa6-fd48-4e30-98f3-55c15a794af8",
"name": "Check 1 \u2014 Balance",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
64,
240
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-balance-01",
"leftValue": "={{ $json.passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "e8eb0362-e77b-4145-bcda-e056afcfcb61",
"name": "Balance Pass?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
320,
240
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-bal-01",
"name": "result",
"value": "FAIL",
"type": "string"
},
{
"id": "assign-bal-02",
"name": "failedCheck",
"value": "={{ $json.failedCheck }}",
"type": "string"
},
{
"id": "assign-bal-03",
"name": "reason",
"value": "={{ $json.reason }}",
"type": "string"
}
]
},
"options": {}
},
"id": "bddbc953-74dc-4f3a-b17c-f35ed723d7a9",
"name": "Return: Balance Failed",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
320,
480
]
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nconst lines = item.journalLines;\nconst coa = item.chartOfAccounts;\n\nif (!Array.isArray(coa) || coa.length === 0) {\n throw new Error('chartOfAccounts is empty or missing \u2014 caller must pass the live COA fetched at workflow start');\n}\n\nconst skipChecks = item.options?.skipChecks ?? [];\nif (skipChecks.includes('coa_validity')) {\n return [{ json: { ...item, passed: true, failedCheck: null, reason: null } }];\n}\n\ntry {\n // Build lookup: match by code (AcctNum) first, fall back to id\n // QB sandbox accounts often have no AcctNum \u2014 id is always present\n const coaByCode = {};\n const coaById = {};\n for (const account of coa) {\n if (account.code) coaByCode[account.code] = account;\n if (account.id) coaById[account.id] = account;\n }\n\n const notFound = [];\n const inactive = [];\n\n for (const line of lines) {\n const account = coaByCode[line.accountCode] ?? coaById[line.accountCode];\n if (!account) {\n notFound.push(line.accountCode);\n } else if (account.active === false) {\n inactive.push(`${line.accountCode} (${account.name})`);\n }\n }\n\n if (notFound.length > 0 || inactive.length > 0) {\n const reasons = [];\n if (notFound.length > 0) reasons.push('Not in COA: ' + notFound.join(', '));\n if (inactive.length > 0) reasons.push('Inactive accounts: ' + inactive.join(', '));\n return [{ json: { ...item, passed: false, failedCheck: 'coa_validity', reason: reasons.join('. ') } }];\n }\n\n return [{ json: { ...item, passed: true, failedCheck: null, reason: null } }];\n\n} catch (err) {\n return [{ json: { ...item, passed: false, failedCheck: 'coa_validity', reason: 'COA validity check error: ' + err.message } }];\n}"
},
"id": "6a910d68-306f-4839-a41d-04fb914ab19f",
"name": "Check 2 \u2014 COA Validity",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
608,
224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "cond-coa-01",
"leftValue": "={{ $json.passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c5b4f9a2-96bb-4523-98af-9ba190d1525e",
"name": "COA Pass?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
848,
224
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-coa-01",
"name": "result",
"value": "FAIL",
"type": "string"
},
{
"id": "assign-coa-02",
"name": "failedCheck",
"value": "={{ $json.failedCheck }}",
"type": "string"
},
{
"id": "assign-coa-03",
"name": "reason",
"value": "={{ $json.reason }}",
"type": "string"
}
]
},
"options": {}
},
"id": "79985ee1-7c57-45f0-9bf3-043d93318d3c",
"name": "Return: COA Failed",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
848,
480
]
},
{
"parameters": {
"url": "=https://sandbox-quickbooks.api.intuit.com/v3/company/{{ $('When Called by Orchestrator').item.json.qbRealmId }}/preferences",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "quickBooksOAuth2Api",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "minorversion",
"value": "65"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {}
}
}
},
"id": "8458c152-cbbc-4d6b-86eb-d36ce777ddc0",
"name": "Fetch QB Period Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1120,
208
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"credentials": {
"quickBooksOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "cond-period-01",
"leftValue": "={{ $json.passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "3c04dbc2-a44e-4c85-8b2d-aba82c38261f",
"name": "Period Open?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1552,
208
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-per-01",
"name": "result",
"value": "FAIL",
"type": "string"
},
{
"id": "assign-per-02",
"name": "failedCheck",
"value": "={{ $('Check 3 \u2014 Period Status').item.json.failedCheck }}",
"type": "string"
},
{
"id": "assign-per-03",
"name": "reason",
"value": "={{ $('Check 3 \u2014 Period Status').item.json.reason }}",
"type": "string"
}
]
},
"options": {}
},
"id": "e77b667c-33de-4d0b-bec6-f1c2e42fe27e",
"name": "Return: Period Failed",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1536,
480
]
},
{
"parameters": {
"jsCode": "const triggerInput = $('When Called by Orchestrator').first().json;\nconst lines = triggerInput.journalLines;\nconst credentialScope = triggerInput.credentialScope;\nconst skipChecks = triggerInput.options?.skipChecks ?? [];\n\nif (skipChecks.includes('user_auth')) {\n return [{ json: { passed: true, failedCheck: null, reason: null } }];\n}\n\ntry {\n if (!Array.isArray(credentialScope)) {\n throw new Error('credentialScope must be an array of authorised account codes or IDs');\n }\n\n const unauthorised = [];\n for (const line of lines) {\n // Match by accountCode (AcctNum) or accountId \u2014 QB sandbox often has no AcctNum\n const code = line.accountCode;\n const id = line.accountId;\n const authorised = credentialScope.includes(code) || credentialScope.includes(id);\n if (!authorised) {\n unauthorised.push(`${code ?? id} (${line.type})`);\n }\n }\n\n if (unauthorised.length > 0) {\n return [{\n json: {\n passed: false,\n failedCheck: 'user_auth',\n reason: `Credential not authorised for: ${unauthorised.join(', ')}. Expand credentialScope or route to an authorised approver.`\n }\n }];\n }\n\n return [{ json: { passed: true, failedCheck: null, reason: null } }];\n\n} catch (err) {\n return [{\n json: {\n passed: false,\n failedCheck: 'user_auth',\n reason: 'Auth check error: ' + err.message\n }\n }];\n}"
},
"id": "17b9156e-171f-4912-a3b0-3b6b0424c559",
"name": "Check 4 \u2014 Auth",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1840,
192
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-auth-01",
"leftValue": "={{ $json.passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "82cefe31-1368-47e6-a135-eda7d7986cbc",
"name": "Auth Pass?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2064,
192
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-auth-01",
"name": "result",
"value": "FAIL",
"type": "string"
},
{
"id": "assign-auth-02",
"name": "failedCheck",
"value": "={{ $json.failedCheck }}",
"type": "string"
},
{
"id": "assign-auth-03",
"name": "reason",
"value": "={{ $json.reason }}",
"type": "string"
}
]
},
"options": {}
},
"id": "b812db26-454b-4ea3-91cb-02170f9d4dbb",
"name": "Return: Auth Failed",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2064,
480
]
},
{
"parameters": {
"jsCode": "const triggerInput = $('When Called by Orchestrator').first().json;\n\ntry {\n const auditLogEntryId = triggerInput.auditLogEntryId;\n const skipChecks = triggerInput.options?.skipChecks ?? [];\n\n if (skipChecks.includes('audit_log')) {\n return [{ json: { passed: true, failedCheck: null, reason: null } }];\n }\n\n if (!auditLogEntryId) {\n return [{\n json: {\n passed: false,\n failedCheck: 'audit_log',\n reason: 'auditLogEntryId is missing. The caller must write a PENDING audit log entry before calling this gate \u2014 the ID proves the log was created.'\n }\n }];\n }\n\n // UUID v4 format \u2014 rejects empty strings, partial IDs, placeholder values\n const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n if (!uuidV4.test(String(auditLogEntryId))) {\n return [{\n json: {\n passed: false,\n failedCheck: 'audit_log',\n reason: 'auditLogEntryId is not a valid UUID v4: ' + auditLogEntryId + '. Confirm the audit log write succeeded and the ID was passed through correctly.'\n }\n }];\n }\n\n return [{ json: { passed: true, failedCheck: null, reason: null } }];\n\n} catch (err) {\n return [{\n json: {\n passed: false,\n failedCheck: 'audit_log',\n reason: 'Audit log confirm error: ' + err.message\n }\n }];\n}"
},
"id": "ae4f5f8a-b7ed-4041-afc5-62322398b726",
"name": "Check 5 \u2014 Audit Log Confirm",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2336,
176
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "cond-audit-01",
"leftValue": "={{ $json.passed }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "03be14d9-bd96-4c85-832e-9554a52b5365",
"name": "Audit Log Pass?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2560,
176
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-aud-01",
"name": "result",
"value": "FAIL",
"type": "string"
},
{
"id": "assign-aud-02",
"name": "failedCheck",
"value": "={{ $json.failedCheck }}",
"type": "string"
},
{
"id": "assign-aud-03",
"name": "reason",
"value": "={{ $json.reason }}",
"type": "string"
}
]
},
"options": {}
},
"id": "0833f31d-2ae3-450f-94ac-96a0d025b80b",
"name": "Return: Audit Log Failed",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2576,
480
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "assign-pass-01",
"name": "result",
"value": "PASS",
"type": "string"
},
{
"id": "assign-pass-02",
"name": "dryRun",
"value": "={{ $('When Called by Orchestrator').item.json.options?.dryRun ?? false }}",
"type": "boolean"
}
]
},
"options": {}
},
"id": "0b5f1797-440b-45d4-9416-f9afd2d145fb",
"name": "Return: PASS",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2816,
160
]
},
{
"parameters": {
"content": "## CHECK 1 \u2014 BALANCE\n\nSum of debits must equal sum of credits rounded to 2dp.\n\nFails fast on malformed line types or amounts.\n\n**Skip:** `options.skipChecks: ['balance']`",
"height": 664,
"width": 800
},
"id": "aed9c489-e180-4d15-88b9-70acbbb8858f",
"name": "Sticky Note \u2014 Check 1",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-304,
-16
]
},
{
"parameters": {
"content": "## CHECK 2 \u2014 COA VALIDITY\n\nEvery `accountCode` in journalLines must exist in the caller-provided `chartOfAccounts` array and have `active: true`.\n\nMatches by code, not name.\n\n**Skip:** `options.skipChecks: ['coa_validity']`",
"height": 664,
"width": 436,
"color": 2
},
"id": "3d91fe07-8e6b-4cdc-8e12-9bf2a4675367",
"name": "Sticky Note \u2014 Check 2",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
560,
0
]
},
{
"parameters": {
"content": "## CHECK 3 \u2014 PERIOD STATUS\n\nHTTP GET to QB Preferences endpoint returns `BookCloseDate`. The `periodDate` passed by the caller must be after that date.\n\nIf `BookCloseDate` is absent (books never formally closed), the check passes.\n\n**Skip:** `options.skipChecks: ['period_status']`",
"height": 652,
"width": 596,
"color": 3
},
"id": "71aef5b8-5d31-45d0-8643-244fd86baa61",
"name": "Sticky Note \u2014 Check 3",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1104,
-16
]
},
{
"parameters": {
"content": "## CHECK 4 \u2014 USER AUTHORISATION\n\nEvery `accountCode` in journalLines must appear in the caller-provided `credentialScope` array.\n\nPrevents AP credentials from posting to revenue or BS accounts outside their scope.\n\n**Skip:** `options.skipChecks: ['user_auth']` \u2014 single-user SMBs only.",
"height": 668,
"width": 532,
"color": 4
},
"id": "eef79bdf-2cb3-4edd-bf46-28416ecdc0bf",
"name": "Sticky Note \u2014 Check 4",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1728,
-16
]
},
{
"parameters": {
"content": "## CHECK 5 \u2014 AUDIT LOG CONFIRM\n\nVerifies `auditLogEntryId` is a valid UUID v4. The caller must write a PENDING audit log entry **before** calling this gate.\n\nThis check proves the log was created. If missing or malformed, the log was never written.\n\n**Skip:** `options.skipChecks: ['audit_log']`",
"height": 748,
"width": 724,
"color": 5
},
"id": "ff833dcb-dbf1-4bea-88e8-b16a5bcec1a3",
"name": "Sticky Note \u2014 Check 5",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
2288,
-64
]
},
{
"parameters": {
"content": "**WHY HTTP REQUEST, NOT QB NODE**\n\nThe QB Preferences API (`/v3/company/{realmId}/preferences`) holds `BookCloseDate` \u2014 the only field that reliably signals a closed period. The native n8n-nodes-base.quickbooks node does not expose the Preferences resource.\n\nThis node uses HTTP Request + QB OAuth2 credential for the same auth context with direct endpoint access. Replace `YOUR_QB_CREDENTIAL_ID` with your QuickBooks OAuth2 credential ID on import.",
"height": 220,
"width": 648,
"color": 6
},
"id": "faa2fa66-1b6d-46e4-80a2-c2aead999be7",
"name": "Sticky Note \u2014 QB Explanation",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1104,
688
]
},
{
"parameters": {
"jsCode": "const bookCloseDate = $input.first().json.Preferences?.AccountingInfoPrefs?.BookCloseDate;\nconst periodDate = $('When Called by Orchestrator').item.json.periodDate;\n\ntry {\n // No close date set \u2192 period is open by default\n if (!bookCloseDate || bookCloseDate === '') {\n return [{\n json: {\n passed: true,\n failedCheck: null,\n reason: 'No book close date set in QuickBooks; period is open by default.'\n }\n }];\n }\n\n // Both dates present \u2192 compare\n const periodOpen = new Date(periodDate) > new Date(bookCloseDate);\n\n return [{\n json: {\n passed: periodOpen,\n failedCheck: periodOpen ? null : 'period_status',\n reason: periodOpen\n ? `Period date ${periodDate} is after close date ${bookCloseDate}; period is open.`\n : `Period date ${periodDate} is on or before close date ${bookCloseDate}; period is closed.`\n }\n }];\n} catch (err) {\n throw new Error(`Period status check failed: ${err.message}. Inputs: periodDate=${periodDate}, bookCloseDate=${bookCloseDate}`);\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1328,
208
],
"id": "94c3191c-fe28-4c7c-9cb3-04f771dfbed7",
"name": "Check 3 \u2014 Period Status"
}
],
"connections": {
"When Called by Orchestrator": {
"main": [
[
{
"node": "Check 1 \u2014 Balance",
"type": "main",
"index": 0
}
]
]
},
"Check 1 \u2014 Balance": {
"main": [
[
{
"node": "Balance Pass?",
"type": "main",
"index": 0
}
]
]
},
"Balance Pass?": {
"main": [
[
{
"node": "Check 2 \u2014 COA Validity",
"type": "main",
"index": 0
}
],
[
{
"node": "Return: Balance Failed",
"type": "main",
"index": 0
}
]
]
},
"Check 2 \u2014 COA Validity": {
"main": [
[
{
"node": "COA Pass?",
"type": "main",
"index": 0
}
]
]
},
"COA Pass?": {
"main": [
[
{
"node": "Fetch QB Period Status",
"type": "main",
"index": 0
}
],
[
{
"node": "Return: COA Failed",
"type": "main",
"index": 0
}
]
]
},
"Fetch QB Period Status": {
"main": [
[
{
"node": "Check 3 \u2014 Period Status",
"type": "main",
"index": 0
}
]
]
},
"Period Open?": {
"main": [
[
{
"node": "Check 4 \u2014 Auth",
"type": "main",
"index": 0
}
],
[
{
"node": "Return: Period Failed",
"type": "main",
"index": 0
}
]
]
},
"Check 4 \u2014 Auth": {
"main": [
[
{
"node": "Auth Pass?",
"type": "main",
"index": 0
}
]
]
},
"Auth Pass?": {
"main": [
[
{
"node": "Check 5 \u2014 Audit Log Confirm",
"type": "main",
"index": 0
}
],
[
{
"node": "Return: Auth Failed",
"type": "main",
"index": 0
}
]
]
},
"Check 5 \u2014 Audit Log Confirm": {
"main": [
[
{
"node": "Audit Log Pass?",
"type": "main",
"index": 0
}
]
]
},
"Audit Log Pass?": {
"main": [
[
{
"node": "Return: PASS",
"type": "main",
"index": 0
}
],
[
{
"node": "Return: Audit Log Failed",
"type": "main",
"index": 0
}
]
]
},
"Check 3 \u2014 Period Status": {
"main": [
[
{
"node": "Period Open?",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
},
"versionId": "b46e8180-289a-4af3-a062-2b636f7e1db3",
"id": "6oZ7z5WgHEm2NGql",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
quickBooksOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AP Invoice — 02 Pre-Posting Gate. Uses executeWorkflowTrigger, httpRequest. Event-driven trigger; 24 nodes.
Source: https://github.com/tabii-dev/accounting-automation-portfolio/blob/main/quickbooks-ap-invoice-automation/workflows/02-pre-posting-gate.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.
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
This template is a powerful, reusable utility for managing stateful, long-running processes. It allows a main workflow to be paused indefinitely at "checkpoints" and then be resumed by external, async
Upload files from any source to your account Kommo or AmoCRM with a simple and reusable workflow. It can split a large file into small ones and upload chunks. Works for Kommo and amoCRM There are 3 re
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.