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": "pulse.gateway",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "pulse-gateway",
"responseMode": "responseNode",
"options": {
"noResponseBody": false
}
},
"id": "webhook-trigger",
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "function validationError(code, message, extras = {}) {\n return {\n json: {\n stage: \"validation_error\",\n error: { code, message, retryable: false },\n brick: extras.brick || 'unknown',\n requestId: extras.requestId || null\n }\n };\n}\n\nconst wh = $node[\"Webhook Trigger\"]?.json || {};\nconst headers = wh.headers || {};\nconst headersLower = Object.fromEntries(Object.entries(headers).map(([k,v]) => [k.toLowerCase(), v]));\n\nconst timestamp = headersLower['x-pulse-timestamp'];\nconst providedSignature = headersLower['x-pulse-signature'];\nif (!timestamp) return validationError('MISSING_TIMESTAMP','Missing X-Pulse-Timestamp header');\nif (!providedSignature) return validationError('MISSING_SIGNATURE','Missing X-Pulse-Signature header');\n\n// Body: prefer webhook body; fallback to $json.body only if first is empty\nlet body = (wh.body && typeof wh.body === 'object') ? wh.body\n : ($json && typeof $json.body === 'object') ? $json.body\n : null;\nif (!body || typeof body !== 'object') return validationError('INVALID_JSON','Request body must be a valid JSON object');\n\nif (!body.brick) return validationError('MISSING_BRICK','Missing required field: brick',{ requestId: body.requestId });\nif (!body.connectionId) return validationError('MISSING_CONNECTION_ID','Missing required field: connectionId',{ brick: body.brick, requestId: body.requestId });\nif (!body.params || typeof body.params !== 'object') return validationError('INVALID_PARAMS','Missing or invalid params field',{ brick: body.brick, requestId: body.requestId });\n\n// Check for HMAC secret availability\nconst hmacSecret = $env.PULSE_HMAC_SECRET || '58222cced25229c292d807ae59d64961197daf2692d06f566e58694502a258c8';\nif (!hmacSecret) return validationError('MISSING_HMAC_SECRET','HMAC secret not configured',{ brick: body.brick, requestId: body.requestId });\n\nconst hmacPayload = String(timestamp) + JSON.stringify(body);\n\nconst subInput = { connectionId: body.connectionId, ...body.params };\nif (body.requestId) subInput.requestId = body.requestId;\nif (body.idempotencyKey) subInput.idempotencyKey = body.idempotencyKey;\n\n// Success - validation complete\nreturn {\n json: {\n stage: \"validated\",\n timestamp: String(timestamp),\n providedSignature,\n hmacPayload,\n hmacSecret,\n brick: body.brick,\n connectionId: body.connectionId,\n requestId: body.requestId || null,\n idempotencyKey: body.idempotencyKey || null,\n subInput\n }\n};"
},
"id": "prepare-request",
"name": "Prepare Request & HMAC Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"dataType": "string",
"value1": "={{ $json.stage }}",
"rules": {
"rules": [
{
"value2": "validation_error",
"output": 0
},
{
"value2": "validated",
"output": 1
}
]
}
},
"id": "stage-router",
"name": "Stage Router",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [
580,
300
]
},
{
"parameters": {
"operation": "hmac",
"dataPropertyName": "data",
"value": "={{ $json.hmacPayload }}",
"secret": "={{ $json.hmacSecret }}",
"algorithm": "sha256",
"encoding": "hex"
},
"id": "crypto-hmac",
"name": "Crypto",
"type": "n8n-nodes-base.crypto",
"typeVersion": 1,
"position": [
780,
380
]
},
{
"parameters": {
"jsCode": "// Compare Signature with proper SHA256 handling\nlet providedSignature = $json.providedSignature || '';\n\n// Strip sha256= prefix if present\nif (providedSignature.startsWith('sha256=')) {\n providedSignature = providedSignature.slice(7);\n}\n\nconst computedSignature = $node[\"Crypto\"].json.data;\nconst timestamp = parseInt($json.timestamp);\nconst currentTime = Math.floor(Date.now() / 1000);\n\n// Check timestamp skew (\u00b1300 seconds)\nconst timeDiff = Math.abs(currentTime - timestamp);\nif (timeDiff > 300) {\n return {\n json: {\n stage: \"auth_error\",\n error: {\n code: 'TIMESTAMP_SKEW',\n message: `Timestamp skew too large: ${timeDiff}s (max 300s)`,\n retryable: false\n },\n brick: $json.brick,\n requestId: $json.requestId\n }\n };\n}\n\n// Constant-time comparison with lowercase\nfunction constantTimeCompare(a, b) {\n if (a.length !== b.length) return false;\n let result = 0;\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return result === 0;\n}\n\nif (!constantTimeCompare(providedSignature.toLowerCase(), computedSignature.toLowerCase())) {\n return {\n json: {\n stage: \"auth_error\",\n error: {\n code: 'INVALID_SIGNATURE',\n message: 'HMAC signature verification failed',\n retryable: false\n },\n brick: $json.brick,\n requestId: $json.requestId\n }\n };\n}\n\n// Signature valid - proceed to routing\nreturn {\n json: {\n stage: \"auth_ok\",\n brick: $json.brick,\n connectionId: $json.connectionId,\n requestId: $json.requestId,\n idempotencyKey: $json.idempotencyKey,\n subInput: $json.subInput\n }\n};"
},
"id": "compare-signature",
"name": "Compare Signature",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1000,
380
]
},
{
"parameters": {
"dataType": "string",
"value1": "={{ $json.stage }}",
"rules": {
"rules": [
{
"value2": "auth_error",
"output": 0
},
{
"value2": "auth_ok",
"output": 1
}
]
}
},
"id": "auth-router",
"name": "Auth Router",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [
1120,
380
]
},
{
"parameters": {
"dataType": "string",
"value1": "={{ $json.brick }}",
"rules": {
"rules": [
{
"value2": "gmail.search_messages",
"output": 0
},
{
"value2": "gmail.create_email_draft",
"output": 1
},
{
"value2": "gmail.send_email",
"output": 2
}
]
}
},
"id": "router-switch",
"name": "Router",
"type": "n8n-nodes-base.switch",
"typeVersion": 1,
"position": [
1340,
380
]
},
{
"parameters": {
"jsCode": "// Return validation error in normalized format\nconst e = $json.error || {};\nconst brick = $json.brick || 'unknown';\nconst requestId = $json.requestId || null;\n\nreturn {\n json: {\n ok: false,\n brick,\n brickVersion: 'v1',\n timestamp: new Date().toISOString(),\n requestId,\n error: {\n code: e.code || 'VALIDATION_ERROR',\n message: e.message || 'Validation failed',\n retryable: e.retryable || false,\n details: e.details || undefined,\n },\n },\n};"
},
"id": "return-validation-error",
"name": "Return Validation Error",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
780,
220
]
},
{
"parameters": {
"workflowId": "A7KEFe9iCNKHoiEB",
"waitForSubWorkflow": true,
"additionalFields": {
"input": "={{ $json.subInput }}"
}
},
"id": "execute-gmail-search",
"name": "Execute Gmail Search",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1,
"position": [
1440,
280
]
},
{
"parameters": {
"workflowId": "7wzqiWBOLY4z5nTV",
"waitForSubWorkflow": true,
"additionalFields": {
"input": "={{ $json.subInput }}"
}
},
"id": "execute-gmail-draft",
"name": "Execute Gmail Draft",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1,
"position": [
1440,
380
]
},
{
"parameters": {
"workflowId": "6yLecDWftZ3gpfkU",
"waitForSubWorkflow": true,
"additionalFields": {
"input": "={{ $json.subInput }}"
}
},
"id": "execute-gmail-send",
"name": "Execute Gmail Send",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1,
"position": [
1440,
480
]
},
{
"parameters": {
"jsCode": "// Unknown Brick Error\nconst brick = $json.brick || 'unknown';\nconst requestId = $json.requestId || null;\n\nreturn {\n json: {\n ok: false,\n brick,\n brickVersion: 'v1',\n timestamp: new Date().toISOString(),\n requestId,\n error: {\n code: 'UNKNOWN_BRICK',\n message: `Unknown brick: ${brick}`,\n retryable: false,\n details: {\n availableBricks: ['gmail.search_messages', 'gmail.create_email_draft', 'gmail.send_email']\n }\n }\n }\n};"
},
"id": "unknown-brick-error",
"name": "Unknown Brick Error",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
580
]
},
{
"parameters": {
"jsCode": "// Normalize Response with stage handling\nconst response = $json;\n\n// Handle stage-based responses\nif (response.stage === 'validation_error' || response.stage === 'auth_error') {\n const brick = response.brick || 'unknown';\n const requestId = response.requestId || null;\n const error = response.error || { code: 'UNKNOWN_ERROR', message: 'Unknown error occurred', retryable: false };\n \n return {\n json: {\n ok: false,\n brick,\n brickVersion: 'v1',\n timestamp: new Date().toISOString(),\n requestId,\n error\n }\n };\n}\n\n// Determine brick and requestId from previous nodes\nlet brick = 'unknown';\nlet requestId = null;\n\n// Try to get from Compare Signature or Auth Error nodes\ntry {\n const compareData = $node[\"Compare Signature\"]?.json;\n if (compareData) {\n brick = compareData.brick || 'unknown';\n requestId = compareData.requestId || null;\n }\n} catch (e) {\n // Fallback to current data\n brick = response.brick || 'unknown';\n requestId = response.requestId || null;\n}\n\nconst timestamp = new Date().toISOString();\n\n// If response is already normalized (has ok field), pass it through\nif (response && response.hasOwnProperty('ok')) {\n return { json: response };\n}\n\n// For successful responses from Execute Workflow nodes\nif (response && typeof response === 'object' && !response.error) {\n return {\n json: {\n ok: true,\n brick,\n brickVersion: 'v1',\n timestamp,\n requestId,\n data: response,\n cached: false\n }\n };\n}\n\n// For error responses\nreturn {\n json: {\n ok: false,\n brick,\n brickVersion: 'v1',\n timestamp,\n requestId,\n error: {\n code: 'EXECUTION_ERROR',\n message: response && response.error && response.error.message ? response.error.message : 'Unknown error occurred',\n retryable: false\n }\n }\n};"
},
"id": "normalize-response",
"name": "Normalize Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1660,
430
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"id": "respond-to-webhook",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1880,
350
]
}
],
"connections": {
"Webhook Trigger": {
"main": [
[
{
"node": "Prepare Request & HMAC Payload",
"type": "main",
"index": 0
}
]
]
},
"Prepare Request & HMAC Payload": {
"main": [
[
{
"node": "Stage Router",
"type": "main",
"index": 0
}
]
]
},
"Stage Router": {
"main": [
[
{
"node": "Return Validation Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Crypto",
"type": "main",
"index": 0
}
]
]
},
"Return Validation Error": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"Crypto": {
"main": [
[
{
"node": "Compare Signature",
"type": "main",
"index": 0
}
]
]
},
"Compare Signature": {
"main": [
[
{
"node": "Auth Router",
"type": "main",
"index": 0
}
]
]
},
"Auth Router": {
"main": [
[
{
"node": "Normalize Response",
"type": "main",
"index": 0
}
],
[
{
"node": "Router",
"type": "main",
"index": 0
}
]
]
},
"Router": {
"main": [
[
{
"node": "Execute Gmail Search",
"type": "main",
"index": 0
}
],
[
{
"node": "Execute Gmail Draft",
"type": "main",
"index": 0
}
],
[
{
"node": "Execute Gmail Send",
"type": "main",
"index": 0
}
],
[
{
"node": "Unknown Brick Error",
"type": "main",
"index": 0
}
]
]
},
"Execute Gmail Search": {
"main": [
[
{
"node": "Normalize Response",
"type": "main",
"index": 0
}
]
]
},
"Execute Gmail Draft": {
"main": [
[
{
"node": "Normalize Response",
"type": "main",
"index": 0
}
]
]
},
"Execute Gmail Send": {
"main": [
[
{
"node": "Normalize Response",
"type": "main",
"index": 0
}
]
]
},
"Unknown Brick Error": {
"main": [
[
{
"node": "Normalize Response",
"type": "main",
"index": 0
}
]
]
},
"Normalize Response": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"versionId": "1",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "pulse-gateway-v1"
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
pulse.gateway. Uses crypto. Webhook trigger; 14 nodes.
Source: https://github.com/SammyTourani/Pulse/blob/09d51f209c603477a489582b13f5c08d9a0af370/flows/bricks/pulse.gateway.working.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.
A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without
Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.
This n8n template demonstrates how a simple Multi-Layer Perceptron (MLP) neural network can predict housing prices. The prediction is based on four key features, processed through a three-layer model.
github code Try yourself
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.