{
  "name": "Pause risky AI agent actions for human approval with Wait and IF",
  "nodes": [
    {
      "parameters": {
        "content": "## Pause risky AI agent actions for human approval\n\nLets an agent act on its own for low-risk work, and parks anything consequential on a link a person has to click first.\n\n### Who's it for\nAnyone giving an AI agent real permissions: sending email, issuing refunds, updating records, posting publicly. The point is to keep the agent useful without letting it act unattended on the things that matter.\n\n### How it works\n- **Agent proposes an action** produces the action and a risk assessment rather than executing anything.\n- **Needs a human?** splits on that risk. Low-risk actions go straight to **Execute automatically**.\n- Everything else builds an approval request carrying the agent's reasoning, and **Wait for a human** suspends the execution on a resume URL.\n- Clicking approve resumes the run at **Execute after approval**. Rejecting it lands on **Do not execute**.\n- The wait has a 24-hour limit and fails closed. An unanswered request is never treated as a yes.\n\n### Setup\n1. Open **Agent proposes an action** and replace the sample with your real agent call. It must return an action and a risk level.\n2. Edit **Needs a human?** to match your own risk rules. Money, external messages and deletions are the usual line.\n3. Send the resume URL somewhere a person will see it. Slack, email and a ticket comment all work.\n\n### Requirements\nAn n8n instance reachable from wherever the approver clicks, since the Wait node resumes over a webhook.\n\n### How to customize\nShorten the timeout for time-sensitive work. Keep it failing closed.\n\nFull walkthrough: https://www.theagentecosystem.com/blog/n8n-ai-human-approval",
        "height": 992,
        "width": 760,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -562
      ],
      "name": "Note: Overview"
    },
    {
      "parameters": {
        "content": "## 1. Agent proposes, does not act\nReturns an action plus a risk level. Nothing has happened yet.",
        "height": 260,
        "width": 480,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        650
      ],
      "name": "Note: Agent proposes, does not act"
    },
    {
      "parameters": {
        "content": "## 2. Split on risk\nLow risk runs unattended. Everything else is packaged for a person to look at.",
        "height": 460,
        "width": 480,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        440,
        550
      ],
      "name": "Note: Split on risk"
    },
    {
      "parameters": {
        "content": "## 3. Wait for a real decision\nSuspends on a resume URL. 24-hour limit, and it fails closed.",
        "height": 260,
        "width": 480,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        920,
        750
      ],
      "name": "Note: Wait for a real decision"
    },
    {
      "parameters": {
        "content": "## 4. Act, or record the refusal\nApproval resumes the run. A rejection is logged, not silently dropped.",
        "height": 440,
        "width": 480,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1400,
        650
      ],
      "name": "Note: Act, or record the refusal"
    },
    {
      "parameters": {},
      "id": "e1f2a3b4-0000-4000-8000-000000000003",
      "name": "Run the agent",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// Stand-in for your real agent step. Swap this for the AI Agent node, or for\n// whatever produces the proposed action.\n//\n// The only contract the rest of the workflow needs is a `risk` score and\n// enough detail for a person to judge the action without opening anything else.\nreturn [{\n  json: {\n    action: 'issue_refund',\n    summary: 'Refund order #4417 in full after a duplicate charge',\n    amount: 249.0,\n    currency: 'USD',\n    customer: 'acme-corp',\n    // Score the CONSEQUENCE, not the model's confidence. A confident agent\n    // doing something irreversible is exactly the case worth stopping.\n    risk: 8,\n  },\n}];"
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000004",
      "name": "Agent proposes an action",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        800
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "e1f2a3b4-0000-4000-8000-000000000011",
              "leftValue": "={{ $json.risk }}",
              "rightValue": 7,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000005",
      "name": "Needs a human?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        480,
        800
      ]
    },
    {
      "parameters": {
        "jsCode": "// Compose what a person will actually read, plus the two links.\n//\n// Approve and reject are the SAME resume URL with a different query string.\n// Treat that URL as a capability: whoever holds it can approve, so put it\n// somewhere the right people can already see and nowhere public.\nconst a = $json;\nconst resumeUrl = $execution.resumeUrl;\n\nreturn [{\n  json: {\n    ...a,\n    approveUrl: `${resumeUrl}?decision=approve`,\n    rejectUrl: `${resumeUrl}?decision=reject`,\n    message: `Approval needed: ${a.summary}\\n\\nAction: ${a.action}\\nAmount: ${a.amount} ${a.currency}\\nCustomer: ${a.customer}\\nRisk: ${a.risk}/10`,\n  },\n}];"
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000006",
      "name": "Build the approval request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        900
      ]
    },
    {
      "parameters": {
        "resume": "webhook",
        "limitWaitTime": true,
        "limitType": "afterTimeInterval",
        "resumeAmount": 24,
        "resumeUnit": "hours",
        "options": {}
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000007",
      "name": "Wait for a human",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        960,
        900
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "e1f2a3b4-0000-4000-8000-000000000012",
              "leftValue": "={{ $json.query?.decision }}",
              "rightValue": "approve",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000008",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1200,
        900
      ]
    },
    {
      "parameters": {
        "jsCode": "// Below the threshold: the agent acts without asking. This is the branch that\n// makes the gate usable — if everything needed a human, the queue would be\n// ignored inside a week and the gate would become decoration.\nconst a = $('Agent proposes an action').first().json;\nreturn [{ json: { executed: true, path: 'auto', action: a.action, risk: a.risk } }];"
      },
      "id": "e1f2a3b4-0000-4000-8000-000000000009",
      "name": "Execute automatically",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        700
      ]
    },
    {
      "parameters": {
        "jsCode": "// Approved by a person. Do the real work here.\nconst a = $('Agent proposes an action').first().json;\nreturn [{ json: { executed: true, path: 'approved', action: a.action, risk: a.risk } }];"
      },
      "id": "e1f2a3b4-0000-4000-8000-00000000000a",
      "name": "Execute after approval",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        820
      ]
    },
    {
      "parameters": {
        "jsCode": "// Rejected, or the wait timed out with no decision attached. Both land here,\n// deliberately: a gate that proceeds when nobody answered is decorative.\n//\n// Record which it was — a rising timeout rate means the approval is going\n// somewhere nobody reads, which is worth knowing before it hides a real refusal.\nconst decision = $json.query?.decision;\nconst a = $('Agent proposes an action').first().json;\n\nreturn [{\n  json: {\n    executed: false,\n    path: decision === 'reject' ? 'rejected' : 'timed out',\n    action: a.action,\n    risk: a.risk,\n  },\n}];"
      },
      "id": "e1f2a3b4-0000-4000-8000-00000000000b",
      "name": "Do not execute",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        980
      ]
    },
    {
      "parameters": {},
      "id": "e1f2a3b4-0000-4000-8000-00000000000c",
      "name": "Done",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1680,
        800
      ]
    }
  ],
  "connections": {
    "Run the agent": {
      "main": [
        [
          {
            "node": "Agent proposes an action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agent proposes an action": {
      "main": [
        [
          {
            "node": "Needs a human?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs a human?": {
      "main": [
        [
          {
            "node": "Build the approval request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Execute automatically",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build the approval request": {
      "main": [
        [
          {
            "node": "Wait for a human",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for a human": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Approved?": {
      "main": [
        [
          {
            "node": "Execute after approval",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Do not execute",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute automatically": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute after approval": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Do not execute": {
      "main": [
        [
          {
            "node": "Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": []
}
