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 →
{
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "wm-state-engine",
"responseMode": "lastNode",
"options": {}
},
"id": "03cf3e82-c894-477f-a379-c388cf24a74d",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const body = $json.body || {};\nconst tel = String(body.tel || '').replace(/\\D/g, '');\nif (!tel || tel.length < 10) {\n return { json: { ok: false, error: 'invalid_phone', tel } };\n}\nconst action = String(body.action || '').toLowerCase();\nconst VALID_ACTIONS = ['get','transition','pause_bot','resume_bot','clear','schedule_action','cancel_scheduled_action'];\nif (!VALID_ACTIONS.includes(action)) {\n return { json: { ok: false, error: 'invalid_action', action } };\n}\nreturn { json: { ok: true, tel, action, params: body.params || {}, now_ms: Date.now(), now_iso: new Date().toISOString() } };"
},
"id": "8bff8d4e-ee52-4138-82e7-70007ef2df15",
"name": "Parse Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
224,
0
]
},
{
"parameters": {
"operation": "get",
"propertyName": "state_raw",
"key": "=lead:{{ $json.tel }}:state",
"options": {}
},
"id": "901597db-26f0-4580-87cf-17263c466295",
"name": "Redis GET State",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
448,
0
],
"credentials": {
"redis": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const up = $('Parse Input').item.json;\nconst raw = $json.state_raw;\nlet state = null;\ntry { state = raw ? (typeof raw === 'string' ? JSON.parse(raw) : raw) : null; } catch(e) { state = null; }\nconst state_existed = !!state && typeof state === 'object' && !Array.isArray(state);\nif (!state_existed) {\n state = {\n schema_version: 2, phone: up.tel, created_at: up.now_iso, updated_at: up.now_iso, version: 0,\n bot: { enabled: true, paused_until_ms: 0, paused_reason: null },\n lead_profile: { name_confirmed: false, name_for_use: null, push_name: null, supabase_lead_id: null, ihelp_contact_id: null },\n stage: { current_id: null, updated_at: null },\n journey: { step: 'new', entered_at_ms: up.now_ms, history: [] },\n prova: { form_sent: false, form_sent_at_ms: 0, form_completed: false, attempt: 0, reprova_count: 0, tally_submission_id: null, answers: null, evaluation: null },\n scheduled_action: { kind: null, due_at_ms: 0, attempt: 0, cancelled_at_ms: 0 },\n dedupe: { outgoing_ia_msg_ids: [] }\n };\n}\nreturn { json: { tel: up.tel, action: up.action, params: up.params, now_ms: up.now_ms, now_iso: up.now_iso, state, state_existed } };"
},
"id": "2cff1561-32f0-47e2-87ee-652ae03ef19d",
"name": "Hydrate State",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
672,
0
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "get",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c1"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "get"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "transition",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c2"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "transition"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "pause_bot",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c3"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "pause_bot"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "resume_bot",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c4"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "resume_bot"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "clear",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c5"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "clear"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "schedule_action",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c6"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "schedule_action"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "cancel_scheduled_action",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c7"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancel_scheduled_action"
}
]
},
"options": {}
},
"id": "c462cfc9-e61e-4e96-95e5-47af744ef06d",
"name": "Switch Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
880,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "return { json: { ok: true, tel: $json.tel, state: $json.state, state_existed: $json.state_existed, needs_write: false, log_transition: false } };"
},
"id": "332b0ad8-90f5-4d77-917d-e0fd82374958",
"name": "Action Get",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1104,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, params, now_ms, now_iso } = $json;\nconst intent = String(params.intent || '');\nconst currentStep = state.journey.step;\n\nconst TRANSITIONS = {\n 'new': {\n 'has_push_name': { to: 'abertura_enviada', actions: ['send_audio_1','send_audio_2','send_aulas_link','move_stage_706'] },\n 'needs_name': { to: 'apresentando', actions: [] }\n },\n 'apresentando': {\n 'name_extracted': { to: 'abertura_enviada', actions: ['send_audio_1','send_audio_2','send_aulas_link','move_stage_706'] },\n 'followup_final': { to: 'encerrado_inativo', actions: ['apply_tag_inativo_apresentacao'] }\n },\n 'abertura_enviada': {\n 'any_msg': { to: 'aguardando_aulas', actions: [] }\n },\n 'aguardando_aulas': {\n 'schedule_tomorrow': { to: 'agendado_amanha_8h', actions: [] },\n 'aulas_done': { to: 'aulas_concluidas', actions: [] },\n 'followup_final': { to: 'encaminhado_humano', actions: [] }\n },\n 'agendado_amanha_8h': {\n 'wake': { to: 'aguardando_aulas', actions: [] }\n },\n 'aulas_concluidas': {\n 'send_tally': { to: 'aguardando_form', actions: ['send_tally_link','move_stage_707'] }\n },\n 'aguardando_form': {\n 'tally_approved': { to: 'aprovado', actions: ['move_stage_708','apply_tags_profile','apply_tag_prova_aprovada','apply_tag_prova_finalizada'] },\n 'tally_reprovado_once': { to: 'aguardando_revisao', actions: ['move_stage_711','apply_tags_profile','apply_tag_prova_reprovada'] },\n 'tally_reprovado_block': { to: 'bloqueado_humano', actions: ['move_stage_711','apply_tag_prova_reprovada'] },\n 'followup_final': { to: 'bloqueado_humano', actions: [] }\n },\n 'aguardando_revisao': {\n 'retry': { to: 'aguardando_form', actions: ['send_tally_link'] },\n 'followup_final': { to: 'bloqueado_humano', actions: [] }\n }\n};\n\nconst allowed = TRANSITIONS[currentStep] || {};\nconst transition = allowed[intent];\n\nif (!transition) {\n return { json: { ok: false, error: 'invalid_transition', from_step: currentStep, to_step: null, intent, tel: $json.tel, state, needs_write: false, log_transition: true, log_trigger: 'rejected' } };\n}\n\nconst nextStep = transition.to;\nconst newState = JSON.parse(JSON.stringify(state));\nnewState.journey.step = nextStep;\nnewState.journey.entered_at_ms = now_ms;\nnewState.journey.history = (newState.journey.history || []).concat([{ step: currentStep, at_ms: state.journey.entered_at_ms }]);\nif (newState.journey.history.length > 20) newState.journey.history = newState.journey.history.slice(-20);\nnewState.updated_at = now_iso;\nnewState.version = (state.version || 0) + 1;\n\nif (nextStep === 'aguardando_form') {\n newState.prova.form_sent = true;\n newState.prova.form_sent_at_ms = now_ms;\n newState.prova.attempt = (newState.prova.attempt || 0) + 1;\n}\nif (intent === 'tally_reprovado_once' || intent === 'tally_reprovado_block') {\n newState.prova.reprova_count = (newState.prova.reprova_count || 0) + 1;\n}\nif (nextStep === 'aprovado' || nextStep === 'bloqueado_humano' || nextStep === 'encerrado_inativo' || nextStep === 'encaminhado_humano') {\n newState.bot.enabled = false;\n newState.bot.paused_reason = nextStep;\n}\n\n\n// Persist lead profile from params\nif (params && params.push_name) {\n newState.lead_profile.push_name = String(params.push_name).slice(0, 80);\n}\nif (params && params.name_for_use) {\n newState.lead_profile.name_for_use = String(params.name_for_use).slice(0, 60);\n newState.lead_profile.name_confirmed = true;\n}\n\nreturn { json: { ok: true, tel: $json.tel, from_step: currentStep, to_step: nextStep, actions: transition.actions, state: newState, needs_write: true, log_transition: true, log_trigger: 'accepted' } };"
},
"id": "5f484cd4-fa06-42e2-885f-e7fa428bce89",
"name": "Action Transition",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1328,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, params, now_ms, now_iso } = $json;\nconst duration_ms = Number(params.duration_ms || 900000);\nconst reason = String(params.reason || 'human_typed');\nconst newState = JSON.parse(JSON.stringify(state));\nnewState.bot.paused_until_ms = now_ms + duration_ms;\nnewState.bot.paused_reason = reason;\nnewState.updated_at = now_iso;\nnewState.version = (state.version || 0) + 1;\nreturn { json: { ok: true, tel: $json.tel, state: newState, needs_write: true, log_transition: false, paused_until_ms: newState.bot.paused_until_ms } };"
},
"id": "e9d31617-245d-442e-a724-7dd9f36c4365",
"name": "Action Pause Bot",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1552,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, now_iso } = $json;\nconst newState = JSON.parse(JSON.stringify(state));\nnewState.bot.paused_until_ms = 0;\nnewState.bot.paused_reason = null;\nnewState.bot.enabled = true;\nnewState.updated_at = now_iso;\nnewState.version = (state.version || 0) + 1;\nreturn { json: { ok: true, tel: $json.tel, state: newState, needs_write: true, log_transition: false } };"
},
"id": "49b3be6e-24ae-4b29-bf36-d45dc32dc7aa",
"name": "Action Resume Bot",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1760,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, params, now_ms, now_iso, tel } = $json;\nconst scope = String(params.scope || 'state_only');\nlet newState;\nif (scope === 'state_only' || scope === 'total') {\n newState = {\n schema_version: 2, phone: tel, created_at: now_iso, updated_at: now_iso, version: 0,\n bot: { enabled: true, paused_until_ms: 0, paused_reason: null },\n lead_profile: { name_confirmed: false, name_for_use: null, push_name: null, supabase_lead_id: null, ihelp_contact_id: null },\n stage: { current_id: null, updated_at: null },\n journey: { step: 'new', entered_at_ms: now_ms, history: [] },\n prova: { form_sent: false, form_sent_at_ms: 0, form_completed: false, attempt: 0, reprova_count: 0, tally_submission_id: null, answers: null, evaluation: null },\n scheduled_action: { kind: null, due_at_ms: 0, attempt: 0, cancelled_at_ms: 0 },\n dedupe: { outgoing_ia_msg_ids: [] }\n };\n} else {\n newState = JSON.parse(JSON.stringify(state));\n newState.scheduled_action = { kind: null, due_at_ms: 0, attempt: 0, cancelled_at_ms: 0 };\n newState.dedupe = { outgoing_ia_msg_ids: [] };\n newState.updated_at = now_iso;\n newState.version = (state.version || 0) + 1;\n}\nreturn { json: { ok: true, tel, state: newState, needs_write: true, log_transition: false, scope } };"
},
"id": "a5ae8f58-0fd2-409f-bb5b-e75ef05978aa",
"name": "Action Clear",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1984,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, params, now_iso } = $json;\nconst newState = JSON.parse(JSON.stringify(state));\nnewState.scheduled_action = {\n kind: String(params.kind || ''),\n due_at_ms: Number(params.due_at_ms || 0),\n attempt: Number(params.attempt || 0),\n cancelled_at_ms: 0\n};\nnewState.updated_at = now_iso;\nnewState.version = (state.version || 0) + 1;\nreturn { json: { ok: true, tel: $json.tel, state: newState, needs_write: true, log_transition: false } };"
},
"id": "3812ad43-e874-4308-91d8-5458f1edb4dd",
"name": "Action Schedule",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2208,
0
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const { state, now_iso, now_ms } = $json;\nconst newState = JSON.parse(JSON.stringify(state));\nnewState.scheduled_action = { ...(newState.scheduled_action || {}), cancelled_at_ms: now_ms };\nnewState.updated_at = now_iso;\nnewState.version = (state.version || 0) + 1;\nreturn { json: { ok: true, tel: $json.tel, state: newState, needs_write: true, log_transition: false } };"
},
"id": "e3770346-fc56-4b04-bfeb-f741f0669dde",
"name": "Action Cancel Schedule",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2432,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "nw",
"leftValue": "={{ $json.needs_write }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "801037a5-2029-4a4c-b152-f5ea4e60619d",
"name": "IF Needs Write",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2640,
0
]
},
{
"parameters": {
"operation": "set",
"key": "=lead:{{ $json.tel }}:state",
"value": "={{ JSON.stringify($json.state) }}",
"expire": true,
"ttl": 604800
},
"id": "1b828e0c-c371-440e-ac46-ec3b8f4068f4",
"name": "Redis SET State",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
2864,
0
],
"credentials": {
"redis": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "lt",
"leftValue": "={{ $json.log_transition }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f5a79d59-4231-46a4-af3d-07e4a0f1a491",
"name": "IF Log Transition",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3088,
0
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "=INSERT INTO wm_state_log (phone, from_step, to_step, trigger, trigger_payload) VALUES ('{{ $json.tel }}', {{ $json.from_step ? \"'\" + $json.from_step + \"'\" : 'NULL' }}, '{{ $json.to_step || $json.from_step }}', '{{ $json.log_trigger }}', '{{ JSON.stringify({intent: $json.params?.intent || null, error: $json.error || null}).replace(/'/g, \"''\") }}'::jsonb);",
"options": {}
},
"id": "795893a5-5c44-4ba9-88c5-ad4e55edb34e",
"name": "Postgres Log Transition",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3312,
0
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Find the action output that ran\nconst actionNames = ['Action Get','Action Transition','Action Pause Bot','Action Resume Bot','Action Clear','Action Schedule','Action Cancel Schedule'];\nlet src = $json;\nfor (const n of actionNames) {\n try {\n const node = $(n);\n if (node && node.isExecuted) { src = node.item.json; break; }\n } catch(e) {}\n}\nreturn { json: {\n ok: src.ok !== false,\n tel: src.tel,\n state: src.state,\n extra: {\n from_step: src.from_step,\n to_step: src.to_step,\n actions: src.actions,\n error: src.error,\n paused_until_ms: src.paused_until_ms,\n scope: src.scope,\n state_existed: src.state_existed\n }\n} };"
},
"id": "82323b4f-910b-4465-ae3c-766f276c7b57",
"name": "Respond OK",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3520,
0
]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Parse Input",
"type": "main",
"index": 0
}
]
]
},
"Parse Input": {
"main": [
[
{
"node": "Redis GET State",
"type": "main",
"index": 0
}
]
]
},
"Redis GET State": {
"main": [
[
{
"node": "Hydrate State",
"type": "main",
"index": 0
}
]
]
},
"Hydrate State": {
"main": [
[
{
"node": "Switch Action",
"type": "main",
"index": 0
}
]
]
},
"Switch Action": {
"main": [
[
{
"node": "Action Get",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Transition",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Pause Bot",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Resume Bot",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Clear",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Schedule",
"type": "main",
"index": 0
}
],
[
{
"node": "Action Cancel Schedule",
"type": "main",
"index": 0
}
]
]
},
"Action Get": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Transition": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Pause Bot": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Resume Bot": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Clear": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Schedule": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"Action Cancel Schedule": {
"main": [
[
{
"node": "IF Needs Write",
"type": "main",
"index": 0
}
]
]
},
"IF Needs Write": {
"main": [
[
{
"node": "Redis SET State",
"type": "main",
"index": 0
},
{
"node": "IF Log Transition",
"type": "main",
"index": 0
}
],
[
{
"node": "IF Log Transition",
"type": "main",
"index": 0
}
]
]
},
"IF Log Transition": {
"main": [
[
{
"node": "Postgres Log Transition",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond OK",
"type": "main",
"index": 0
}
]
]
},
"Postgres Log Transition": {
"main": [
[
{
"node": "Respond OK",
"type": "main",
"index": 0
}
]
]
}
}
}
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.
postgresredis
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Wm-State-Engine.N8N-Import. Uses redis, postgres. Webhook trigger; 17 nodes.
Source: https://gist.github.com/bruunofco/55a116efb0f447da5948234bcb04ec2e — 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.
Wm-State-Engine.N8N-Import. Uses redis, postgres. Webhook trigger; 17 nodes.
Wm-Admin.N8N-Import. Uses postgres, httpRequest, redis. Webhook trigger; 12 nodes.
Reagendamiento_v2. Uses executeWorkflowTrigger, redis, httpRequest, n8n-nodes-evolution-api. Event-driven trigger; 89 nodes.
This solution enables you to manage all your Notion and Todoist tasks from different workspaces as well as your calendar events in a single place. This is 2 way sync with partial support for recurring
Scraping. Uses httpRequest, postgres, @apify/n8n-nodes-apify, respondToWebhook. Webhook trigger; 61 nodes.