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": [
{
"id": "n001",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
300
],
"parameters": {
"httpMethod": "POST",
"path": "wm-state-engine",
"responseMode": "lastNode",
"options": {}
}
},
{
"id": "n002",
"name": "Parse Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
300
],
"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": "n003",
"name": "Redis GET State",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
440,
300
],
"parameters": {
"operation": "get",
"propertyName": "state_raw",
"key": "=lead:{{ $json.tel }}:state",
"options": {}
},
"credentials": {
"redis": {
"name": "<your credential>"
}
}
},
{
"id": "n004",
"name": "Hydrate State",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
300
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const 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: $json.tel, created_at: $json.now_iso, updated_at: $json.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: $json.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: { ...$json, state, state_existed } };"
}
},
{
"id": "n005",
"name": "Switch Action",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
880,
300
],
"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": "n006",
"name": "Action Get",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
300
],
"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": "n007",
"name": "Action Transition",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
300
],
"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\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": "n008",
"name": "Action Pause Bot",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1540,
300
],
"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": "n009",
"name": "Action Resume Bot",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1760,
300
],
"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": "n010",
"name": "Action Clear",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1980,
300
],
"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": "n011",
"name": "Action Schedule",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2200,
300
],
"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": "n012",
"name": "Action Cancel Schedule",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2420,
300
],
"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": "n013",
"name": "IF Needs Write",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2640,
300
],
"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": "n014",
"name": "Redis SET State",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
2860,
300
],
"parameters": {
"operation": "set",
"key": "=lead:{{ $json.tel }}:state",
"value": "={{ JSON.stringify($json.state) }}",
"expire": true,
"ttl": 604800
},
"credentials": {
"redis": {
"name": "<your credential>"
}
}
},
{
"id": "n015",
"name": "IF Log Transition",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3080,
300
],
"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": "n016",
"name": "Postgres Log Transition",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3300,
300
],
"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": {}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "n017",
"name": "Respond OK",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3520,
300
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "r-ok",
"name": "ok",
"type": "boolean",
"value": "={{ $json.ok !== false }}"
},
{
"id": "r-tel",
"name": "tel",
"type": "string",
"value": "={{ $json.tel }}"
},
{
"id": "r-state",
"name": "state",
"type": "object",
"value": "={{ $json.state }}"
},
{
"id": "r-extra",
"name": "extra",
"type": "object",
"value": "={{ { from_step: $json.from_step, to_step: $json.to_step, actions: $json.actions, error: $json.error, paused_until_ms: $json.paused_until_ms, scope: $json.scope, state_existed: $json.state_existed } }}"
}
]
},
"options": {}
}
}
],
"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
}
]
]
},
"Redis SET State": {
"main": [
[
{
"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/ceb2070651ca3156795f7a242f37080a — 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.