{
  "name": "Lab 4: Agentic Workflow",
  "nodes": [
    {
      "parameters": {},
      "id": "trigger-004",
      "name": "Start Lab",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [100, 300],
      "notes": "This lab implements a simplified agent loop: Plan -> Select Tool -> Execute -> Observe -> Decide.\n\nThe agent receives a task, plans what to do, selects a tool, observes the result, and decides whether to continue or return a final answer.\n\nNotice the TRUST BOUNDARY notes on tool nodes -- every tool execution is a potential attack surface."
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "task",
              "value": "Research the current price of Bitcoin and calculate what 2.5 Bitcoin would be worth in US dollars."
            },
            {
              "name": "iteration",
              "value": "1"
            },
            {
              "name": "previous_observations",
              "value": "None yet -- this is the first iteration."
            }
          ]
        },
        "options": {}
      },
      "id": "set-task-004",
      "name": "Agent Task Input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [320, 300],
      "notes": "The task for the agent to complete. This requires multiple steps:\n1. Use web search to find the current Bitcoin price\n2. Use the calculator to multiply by 2.5\n3. Return the final answer\n\nYou can modify the task to test different multi-step scenarios."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'system', content: 'STUDENT TASK: Write the planning prompt. Replace this text with instructions that tell the LLM to act as a planning agent. The prompt should instruct the LLM to: 1) Analyze the task and previous observations, 2) Decide which tool to use next (web_search, calculator, or done), 3) Output a structured response with exactly these fields: tool (web_search, calculator, or done), input (the input for the tool), reasoning (why this tool was chosen). IMPORTANT: The output MUST be valid JSON with those three fields.' }, { role: 'user', content: 'Task: ' + $json.task + '\\n\\nIteration: ' + $json.iteration + '\\n\\nPrevious observations: ' + $json.previous_observations + '\\n\\nDecide the next action. Respond with JSON: {\"tool\": \"web_search|calculator|done\", \"input\": \"...\", \"reasoning\": \"...\"}' }], temperature: 0.2 }) }}",
        "options": {}
      },
      "id": "http-plan-004",
      "name": "Agent: Plan Next Action",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [540, 300],
      "notes": "STUDENT TASK: Write the planning prompt in the system message.\n\nThis is the 'brain' of the agent -- it decides what to do next.\n\nYour planning prompt should instruct the LLM to:\n1. Analyze the task and any previous observations\n2. Choose the appropriate tool (web_search, calculator, or done)\n3. Provide structured JSON output\n\nThe quality of this prompt determines how effectively the agent works.\nA poorly written planning prompt = an agent that makes bad decisions = Chapter 2 attack surface."
    },
    {
      "parameters": {
        "functionCode": "// Parse the LLM's planning response to extract the tool decision\nconst response = $input.item.json.choices[0].message.content;\nlet decision;\n\ntry {\n  // Try to parse as JSON directly\n  decision = JSON.parse(response);\n} catch (e) {\n  // If not valid JSON, try to extract JSON from the response\n  const jsonMatch = response.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    decision = JSON.parse(jsonMatch[0]);\n  } else {\n    decision = { tool: 'done', input: 'Error parsing agent response', reasoning: 'Could not parse' };\n  }\n}\n\nreturn [{\n  json: {\n    tool: decision.tool || 'done',\n    tool_input: decision.input || '',\n    reasoning: decision.reasoning || '',\n    task: $('Agent Task Input').item.json.task,\n    iteration: $('Agent Task Input').item.json.iteration,\n    previous_observations: $('Agent Task Input').item.json.previous_observations\n  }\n}];"
      },
      "id": "func-parse-004",
      "name": "Parse Agent Decision",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [760, 300],
      "notes": "Extracts the structured decision from the LLM's response.\n\nThis step converts the LLM's text output into actionable routing data for the Switch node."
    },
    {
      "parameters": {
        "rules": {
          "rules": [
            {
              "value2": "STUDENT TASK: Set the condition value for web_search routing"
            },
            {
              "value2": "STUDENT TASK: Set the condition value for calculator routing"
            },
            {
              "value2": "STUDENT TASK: Set the condition value for done/final answer routing"
            }
          ]
        },
        "fallbackOutput": "extra",
        "options": {},
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.tool }}",
              "value2": "web_search",
              "operation": "equals"
            },
            {
              "value1": "={{ $json.tool }}",
              "value2": "calculator",
              "operation": "equals"
            },
            {
              "value1": "={{ $json.tool }}",
              "value2": "done",
              "operation": "equals"
            }
          ]
        }
      },
      "id": "switch-004",
      "name": "Route to Tool",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 1,
      "position": [980, 300],
      "notes": "STUDENT TASK: Configure the routing conditions.\n\nThis Switch node routes the agent's decision to the appropriate tool:\n- Output 0: web_search (when the agent needs to look something up)\n- Output 1: calculator (when the agent needs to compute something)\n- Output 2: done (when the agent has the final answer)\n\nSet the conditions to match the exact tool names from the planning prompt.\nThis is the decision logic that determines the agent's behavior."
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "tool_name",
              "value": "web_search"
            },
            {
              "name": "tool_result",
              "value": "={{ 'Web search result for: ' + $json.tool_input + '\\n\\nSimulated result: Based on current market data, Bitcoin (BTC) is trading at approximately $67,450 USD per coin. The price has been relatively stable over the past 24 hours with minor fluctuations. (Note: This is simulated data for the lab exercise.)' }}"
            },
            {
              "name": "observation",
              "value": "={{ 'Used web_search tool. Query: ' + $json.tool_input + '. Result: Bitcoin is currently trading at approximately $67,450 USD.' }}"
            }
          ]
        },
        "options": {}
      },
      "id": "set-websearch-004",
      "name": "Tool: Web Search (Mock)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [1200, 120],
      "notes": "TRUST BOUNDARY CROSSING: Web Search Tool\n\nIn a real agent, this would make an actual HTTP request to a search API.\nThis is a mock that returns simulated data.\n\nSECURITY NOTE: In production, web search results could contain:\n- Indirect prompt injection (malicious instructions in web pages)\n- Misleading data designed to manipulate the agent\n- Links to malicious resources\n\nEvery web search result crosses a trust boundary -- the agent trusts external data."
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "tool_name",
              "value": "calculator"
            },
            {
              "name": "tool_result",
              "value": "={{ (function() { try { var expr = $json.tool_input.replace(/[^0-9+\\-*/.() ]/g, ''); return 'Calculation: ' + $json.tool_input + ' = ' + eval(expr); } catch(e) { return 'Error: Could not evaluate expression: ' + $json.tool_input; } })() }}"
            },
            {
              "name": "observation",
              "value": "={{ 'Used calculator tool. Expression: ' + $json.tool_input + '. Result computed.' }}"
            }
          ]
        },
        "options": {}
      },
      "id": "set-calc-004",
      "name": "Tool: Calculator (Mock)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [1200, 300],
      "notes": "TRUST BOUNDARY CROSSING: Calculator Tool\n\nThis performs a basic arithmetic calculation based on the agent's input.\n\nSECURITY NOTE: In a real agent with code execution capabilities,\nthis could be exploited to run arbitrary code if the agent's input\nis manipulated through prompt injection. Even a 'simple calculator'\nthat uses eval() is a code execution vector.\n\nThis is why Chapter 2 covers how tool inputs can be weaponized."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'Based on the following task and observations, provide a clear final answer.\\n\\nTask: ' + $json.task + '\\n\\nTool input (agent\\'s final reasoning): ' + $json.tool_input + '\\n\\nAll previous observations: ' + $json.previous_observations + '\\n\\nProvide a clear, concise final answer:' }], temperature: 0.3 }) }}",
        "options": {}
      },
      "id": "http-final-004",
      "name": "Generate Final Answer",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [1200, 480],
      "notes": "When the agent decides it's 'done', this node generates the final answer based on all accumulated observations.\n\nThis represents the end of the agent loop -- the agent has gathered enough information and computed the result."
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "final_answer",
              "value": "={{ $json.choices[0].message.content }}"
            },
            {
              "name": "status",
              "value": "Agent task complete"
            }
          ]
        },
        "options": {}
      },
      "id": "set-final-004",
      "name": "Final Output",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [1420, 480],
      "notes": "The agent's final answer.\n\nReflect on the workflow you just built:\n- How many trust boundary crossings occurred?\n- What if the web search returned manipulated data?\n- What if the planning prompt was tricked into choosing the wrong tool?\n- These are exactly the attack vectors Chapter 2 will explore."
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "observation_log",
              "value": "={{ $json.previous_observations + '\\n\\nIteration ' + $json.iteration + ': ' + $json.observation }}"
            },
            {
              "name": "next_iteration",
              "value": "={{ String(parseInt($json.iteration) + 1) }}"
            },
            {
              "name": "tool_result_display",
              "value": "={{ $json.tool_result }}"
            }
          ]
        },
        "options": {}
      },
      "id": "set-observe-004",
      "name": "Observe & Log",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [1420, 200],
      "notes": "The observation step of the agent loop.\n\nThis accumulates the results from each tool execution so the planning step can use them in the next iteration.\n\nNote: In this simplified lab, the loop doesn't automatically cycle back to the planning step. In a full implementation, this would feed back into the planning node for the next iteration. You can manually re-run the workflow with updated observations to simulate multiple iterations."
    }
  ],
  "connections": {
    "Start Lab": {
      "main": [
        [
          {
            "node": "Agent Task Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agent Task Input": {
      "main": [
        [
          {
            "node": "Agent: Plan Next Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Agent: Plan Next Action": {
      "main": [
        [
          {
            "node": "Parse Agent Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Agent Decision": {
      "main": [
        [
          {
            "node": "Route to Tool",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Tool": {
      "main": [
        [
          {
            "node": "Tool: Web Search (Mock)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tool: Calculator (Mock)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Final Answer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool: Web Search (Mock)": {
      "main": [
        [
          {
            "node": "Observe & Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool: Calculator (Mock)": {
      "main": [
        [
          {
            "node": "Observe & Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Final Answer": {
      "main": [
        [
          {
            "node": "Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "instanceId": "lab-template"
  }
}
