{
  "name": "Agent trace harness: log what the tool returned",
  "nodes": [
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000001",
      "name": "Called By Agent",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.2,
      "position": [
        -620,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Wrap the real tool in this sub-workflow, then point the agent's Call n8n\n// Workflow Tool at it instead of at the tool directly. Everything the model\n// sent and everything it got back is now on disk, which is the only way to\n// tell \"the tool was never called\" apart from \"the tool answered and the\n// model ignored it\". Those two look identical in the chat window.\n\nreturn [{\n  json: {\n    toolName: 'get_order_status',\n    startedAt: new Date().toISOString(),\n    startMs: Date.now(),\n    // Whatever the agent passed in. Keep it raw: a wrong argument value is\n    // one of the two most common causes, and paraphrasing it destroys the clue.\n    args: $json\n  }\n}];"
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000002",
      "name": "Stamp Call",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -400,
        0
      ]
    },
    {
      "parameters": {
        "url": "https://api.example.com/orders/{{ $json.args.order_id }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.ORDERS_API_KEY }}"
            },
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "timeout": 20000,
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        }
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000003",
      "name": "Call Real Tool",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -180,
        0
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "const call = $('Stamp Call').first().json;\nconst res = $json || {};\nconst code = Number(res.statusCode || 0);\nconst body = res.body !== undefined ? res.body : res;\n\nconst durationMs = Date.now() - call.startMs;\nconst serialised = JSON.stringify(body === undefined ? null : body);\n\n// The four returns that make a model behave as if the tool never ran.\nlet verdict = 'ok';\nif (code >= 400 || res.error) {\n  verdict = 'errored';\n} else if (body === null || body === undefined) {\n  verdict = 'null-return';\n} else if (Array.isArray(body) && body.length === 0) {\n  verdict = 'empty-array';\n} else if (typeof body === 'object' && Object.keys(body).length === 0) {\n  verdict = 'empty-object';\n} else if (serialised.length > 6000) {\n  // Long enough that the memory window may drop it before the next turn,\n  // which reads to the user exactly like the tool was ignored.\n  verdict = 'oversized';\n}\n\nreturn [{\n  json: {\n    toolName: call.toolName,\n    startedAt: call.startedAt,\n    durationMs,\n    statusCode: code,\n    verdict,\n    argsJson: JSON.stringify(call.args),\n    returnBytes: serialised.length,\n    returnJson: serialised.slice(0, 4000),\n    payload: body\n  }\n}];"
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000004",
      "name": "Build Trace Record",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        40,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://REPLACE_WITH_YOUR_LOG_SINK.example.com/tool-calls",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ tool: $json.toolName, at: $json.startedAt, ms: $json.durationMs, status: $json.statusCode, verdict: $json.verdict, args: $json.argsJson, bytes: $json.returnBytes, returned: $json.returnJson }) }}",
        "options": {
          "timeout": 5000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000005",
      "name": "Write To Log Sink",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        260,
        0
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Return to the agent, and never let a logging outage break the tool.\n// On a bad return, hand the model an explicit sentence instead of an empty\n// object. Models fill silence with invention; they follow instructions.\n\nconst trace = $('Build Trace Record').first().json;\n\nif (trace.verdict === 'ok') {\n  return [{ json: trace.payload }];\n}\n\nconst reasons = {\n  'errored': 'The lookup failed with status ' + trace.statusCode + '.',\n  'null-return': 'The lookup returned nothing.',\n  'empty-array': 'The lookup ran and found no matching records.',\n  'empty-object': 'The lookup ran and returned no fields.',\n  'oversized': 'The result was too large to return in full.'\n};\n\nreturn [{\n  json: {\n    ok: false,\n    tool_result: reasons[trace.verdict] || 'The lookup did not return usable data.',\n    instruction: 'Tell the user you could not retrieve this and offer to hand them to a person. Do not guess a value.'\n  }\n}];"
      },
      "id": "e4d0f1a2-0004-4a10-9c01-000000000006",
      "name": "Return To Agent",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        0
      ]
    }
  ],
  "connections": {
    "Called By Agent": {
      "main": [
        [
          {
            "node": "Stamp Call",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stamp Call": {
      "main": [
        [
          {
            "node": "Call Real Tool",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Real Tool": {
      "main": [
        [
          {
            "node": "Build Trace Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Trace Record": {
      "main": [
        [
          {
            "node": "Write To Log Sink",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write To Log Sink": {
      "main": [
        [
          {
            "node": "Return To Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "shared-template"
  }
}