{
  "name": "Report Watch — check one saved CSV submission",
  "active": false,
  "nodes": [
    {
      "id": "manual",
      "name": "Start manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        240
      ],
      "parameters": {}
    },
    {
      "id": "envelope",
      "name": "Saved submission envelope",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        240
      ],
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"monitorId\": \"REPLACE_MONITOR_ID\",\n  \"submissionId\": \"REPLACE_SAVED_SUBMISSION_ID\",\n  \"deadlineMs\": \"REPLACE_DEADLINE_MS\",\n  \"csv\": \"client,orders\\nExample agency,12\\n\"\n}",
        "options": {}
      }
    },
    {
      "id": "validate",
      "name": "Validate saved envelope",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        240
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const validateEnvelope = function validateN8nEnvelope(value) {\n  const fail = () => {throw new Error('Configure one saved submission envelope: monitorId, submissionId, deadlineMs and csv. Keep it unchanged for a retry.');};\n  if (!value || typeof value !== 'object' || Array.isArray(value)\n      || Object.keys(value).sort().join(',') !== 'csv,deadlineMs,monitorId,submissionId') fail();\n  if (typeof value.monitorId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(value.monitorId)\n      || typeof value.submissionId !== 'string' || !/^[!-~]{1,128}$/.test(value.submissionId)\n      || typeof value.deadlineMs !== 'string' || !/^\\d{13}$/.test(value.deadlineMs)\n      || [value.monitorId,value.submissionId,value.deadlineMs].some(v => v.startsWith('REPLACE_'))\n      || typeof value.csv !== 'string' || !value.csv.trim()\n      || Buffer.byteLength(value.csv,'utf8') > 1048576) fail();\n  return {monitorId:value.monitorId,submissionId:value.submissionId,deadlineMs:value.deadlineMs,csv:value.csv};\n};\nconst items=$input.all();\nif(items.length!==1)throw new Error('Supply exactly one saved envelope.');\nreturn [{json:validateEnvelope(items[0].json)}];"
      }
    },
    {
      "id": "request",
      "name": "Submit CSV once",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        720,
        240
      ],
      "retryOnFail": false,
      "onError": "continueRegularOutput",
      "parameters": {
        "method": "POST",
        "url": "={{ 'https://reportwatch.mayd-it.com/api/ingest/' + encodeURIComponent($json.monitorId) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.submissionId }}"
            },
            {
              "name": "X-Report-Deadline",
              "value": "={{ $json.deadlineMs }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/csv; charset=utf-8",
        "body": "={{ $json.csv }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 30000
        }
      }
    },
    {
      "id": "classify",
      "name": "Read the check receipt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        240
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const classifyReceipt = function classifyN8nReceipt(response, envelope) {\n  const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);\n  const review = reason => ({outcome:'review',reason,httpStatus:Number.isInteger(response?.statusCode)?response.statusCode:null});\n  if (!object(response) || response.statusCode !== 200) return review('No accepted receipt. Review transport, access or quota; retain the original envelope.');\n  const type = object(response.headers) ? Object.entries(response.headers).find(([key])=>key.toLowerCase()==='content-type')?.[1] : null;\n  if (typeof type !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(type)\n      || !(typeof response.body === 'string' || object(response.body))) return review('Unexpected response format. Do not follow redirects or forward the credential.');\n  let receipt;\n  try {\n    const text=typeof response.body === 'string'?response.body:JSON.stringify(response.body);\n    if(text.length>262144) return review('Unexpected response format. Do not follow redirects or forward the credential.');\n    receipt=typeof response.body === 'string'?JSON.parse(text):response.body;\n  } catch {return review('Incomplete JSON receipt. Retain the original envelope for review.');}\n  const strings = value => Array.isArray(value) && value.every(item=>typeof item==='string');\n  if (!object(receipt) || typeof receipt.checkId!=='string' || !receipt.checkId\n      || !['passed','failed'].includes(receipt.status) || typeof receipt.duplicate!=='boolean'\n      || !Number.isSafeInteger(receipt.checkedAt) || receipt.checkedAt<0\n      || receipt.deadline!==Number(envelope.deadlineMs) || typeof receipt.late!=='boolean'\n      || !Number.isSafeInteger(receipt.rowCount) || receipt.rowCount<0 || !strings(receipt.columns)\n      || !Array.isArray(receipt.checks) || !receipt.checks.length\n      || !receipt.checks.every(check=>object(check)&&typeof check.passed==='boolean')\n      || (receipt.status==='passed')!==receipt.checks.every(check=>check.passed)\n      || !object(receipt.rules) || !strings(receipt.rules.requiredColumns)\n      || !Number.isInteger(receipt.rules.minRows) || !Number.isInteger(receipt.rules.maxRows)\n      || receipt.proof!=='Submitted CSV only; independent client delivery is not verified.') return review('Receipt fields do not match the expected report deadline or contract.');\n  return {outcome:receipt.status==='passed'?'passed':'quality_failed',httpStatus:200,\n    checkId:receipt.checkId,status:receipt.status,deadline:receipt.deadline,checkedAt:receipt.checkedAt,\n    late:receipt.late,duplicate:receipt.duplicate};\n};\nconst items=$input.all();\nif(items.length!==1)throw new Error('Expected one HTTP response.');\nreturn [{json:classifyReceipt(items[0].json,$('Validate saved envelope').first().json)}];"
      }
    },
    {
      "id": "receipt",
      "name": "Inspect outcome before continuing",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1200,
        240
      ],
      "parameters": {}
    },
    {
      "id": "setup-note",
      "name": "Setup and authorization",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        220,
        -180
      ],
      "parameters": {
        "width": 560,
        "height": 330,
        "content": "## Set up before running\nImport as an unpublished workflow. Manual trigger only.\n1. Use an eligible Report Watch monitor (public workspaces require paid monitoring).\n2. Replace the envelope with ONE saved artifact, original submission ID and intended scheduled deadline in Unix milliseconds. It must be durable outside this example. No token in these fields.\n3. Select a protected Header Auth credential on Submit CSV once: name Authorization, value Bearer <report token>. No credential is included.\n4. Use a synthetic report first. Review https://reportwatch.mayd-it.com/guides/n8n-csv-monitoring before connecting your real workflow."
      }
    },
    {
      "id": "result-note",
      "name": "Outcome and retry rules",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        900,
        -180
      ],
      "parameters": {
        "width": 560,
        "height": 330,
        "content": "## HTTP 200 can contain a failed CSV\nInspect outcome: passed, quality_failed or review. Only passed means the file met its configured rules. Check late and duplicate separately.\nOne request per run; no retry loop. Keep the SAME saved bytes, ID and deadline for an authorized retry. Corrections get a new ID and keep the original deadline.\nNetwork/status errors route to review; malformed input can stop the workflow before submission.\nThis template does not send email, create a monitor, buy a plan or schedule itself. n8n may retain execution data or logs; review its retention controls."
      }
    }
  ],
  "connections": {
    "Start manually": {
      "main": [
        [
          {
            "node": "Saved submission envelope",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Saved submission envelope": {
      "main": [
        [
          {
            "node": "Validate saved envelope",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate saved envelope": {
      "main": [
        [
          {
            "node": "Submit CSV once",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit CSV once": {
      "main": [
        [
          {
            "node": "Read the check receipt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the check receipt": {
      "main": [
        [
          {
            "node": "Inspect outcome before continuing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": false,
    "saveExecutionProgress": false
  },
  "pinData": {},
  "tags": []
}
