AutomationFlowsAI & RAG › Gate Agent Releases with Evaluation Evidence

Gate Agent Releases with Evaluation Evidence

Gate agent releases with evaluation evidence. Webhook trigger; 5 nodes.

Webhook trigger★★☆☆☆ complexity5 nodes
AI & RAG Trigger: Webhook Nodes: 5 Complexity: ★★☆☆☆ Added:

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 →

Download .json
{
  "id": "5047dbe5-93f9-4bd1-aec0-cc73d31694ef",
  "name": "Gate agent releases with evaluation evidence",
  "description": "Reviews quality, safety, latency, cost, and evaluation coverage signals before an agent change is considered for release. It exists to make the decision policy explainable and testable before enterprise systems or irreversible actions are connected.",
  "nodes": [
    {
      "parameters": {
        "content": "### Gate agent releases with evaluation evidence\n\nReviews quality, safety, latency, cost, and evaluation coverage signals before an agent change is considered for release.\n\n**Production gate:** this inactive template uses an unauthenticated webhook for local testing only. Configure built-in webhook authentication, review policy version 1.0.7, and assign a human owner before activation.",
        "height": 320,
        "width": 460,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -520,
        -260
      ],
      "id": "3561ba49-365a-4088-af58-5976629450b5",
      "name": "Read before activation"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "enterprise/artificial-intelligence/agent-evaluation-release-gate",
        "authentication": "none",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -360,
        80
      ],
      "id": "26c02cb0-9a82-4089-a38d-a869955f081a",
      "name": "Receive request"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "={{ (() => {\n    const hasValue = function hasValue(value) {\n  return value !== undefined && value !== null && !(typeof value === \"string\" && value.trim() === \"\");\n};\n    const normalizeRequestId = function normalizeRequestId(value, fallback) {\n  if (!hasValue(value)) return String(fallback);\n  const normalized = String(value).replace(/[\\r\\n]/g, \"\").trim().slice(0, 200);\n  return normalized || String(fallback);\n};\n    const isRfc3339DateTime = function isRfc3339DateTime(value) {\n  if (typeof value !== \"string\") return false;\n  const match = /^(\\d{4})-(\\d{2})-(\\d{2})[Tt](\\d{2}):(\\d{2}):(\\d{2})(?:\\.\\d+)?(?:[Zz]|[+-](\\d{2}):(\\d{2}))$/.exec(value);\n  if (!match) return false;\n\n  const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;\n  const year = Number(yearText);\n  const month = Number(monthText);\n  const day = Number(dayText);\n  const hour = Number(hourText);\n  const minute = Number(minuteText);\n  const second = Number(secondText);\n  const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n  const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n\n  if (month < 1 || month > 12 || day < 1 || day > daysInMonth[month - 1]) return false;\n  if (hour > 23 || minute > 59 || second > 59) return false;\n  if (offsetHourText !== undefined && (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 59)) return false;\n  return true;\n};\n    const validateValue = function validateValue(field, value, contract) {\n  const violations = [];\n  const actualType = Array.isArray(value) ? \"array\" : typeof value;\n\n  if (contract.type === \"number\" && (typeof value !== \"number\" || !Number.isFinite(value))) {\n    return [{ field, code: \"invalid_type\", message: `${field} must be a finite number`, expected: \"number\" }];\n  }\n  if (contract.type === \"string\" && typeof value !== \"string\") {\n    return [{ field, code: \"invalid_type\", message: `${field} must be a string`, expected: \"string\" }];\n  }\n  if (contract.type === \"boolean\" && typeof value !== \"boolean\") {\n    return [{ field, code: \"invalid_type\", message: `${field} must be a boolean`, expected: \"boolean\" }];\n  }\n  if (contract.type === \"array\" && !Array.isArray(value)) {\n    return [{ field, code: \"invalid_type\", message: `${field} must be an array`, expected: \"array\" }];\n  }\n  if (![\"number\", \"string\", \"boolean\", \"array\"].includes(contract.type)) {\n    return [{ field, code: \"invalid_contract\", message: `Unsupported contract type for ${field}`, expected: contract.type, actual: actualType }];\n  }\n\n  if (typeof value === \"string\") {\n    if (contract.minLength !== undefined && value.length < contract.minLength) {\n      violations.push({ field, code: \"too_short\", message: `${field} must contain at least ${contract.minLength} character(s)`, expected: `minLength:${contract.minLength}` });\n    }\n    if (contract.maxLength !== undefined && value.length > contract.maxLength) {\n      violations.push({ field, code: \"too_long\", message: `${field} must contain at most ${contract.maxLength} characters`, expected: `maxLength:${contract.maxLength}` });\n    }\n    if (contract.format === \"email\" && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value)) {\n      violations.push({ field, code: \"invalid_format\", message: `${field} must be a valid email address`, expected: \"email\" });\n    }\n    if (contract.format === \"date-time\" && !isRfc3339DateTime(value)) {\n      violations.push({ field, code: \"invalid_format\", message: `${field} must be an RFC 3339 date-time`, expected: \"date-time\" });\n    }\n    if (contract.pattern && !(new RegExp(contract.pattern)).test(value)) {\n      violations.push({ field, code: \"invalid_format\", message: `${field} has an invalid format`, expected: contract.pattern });\n    }\n    if (contract.enum && !contract.enum.includes(value)) {\n      violations.push({ field, code: \"invalid_value\", message: `${field} must be one of the supported values`, expected: contract.enum.join(\"|\") });\n    }\n  }\n\n  if (typeof value === \"number\") {\n    if (contract.minimum !== undefined && value < contract.minimum) {\n      violations.push({ field, code: \"below_minimum\", message: `${field} must be at least ${contract.minimum}`, expected: `minimum:${contract.minimum}` });\n    }\n    if (contract.maximum !== undefined && value > contract.maximum) {\n      violations.push({ field, code: \"above_maximum\", message: `${field} must be at most ${contract.maximum}`, expected: `maximum:${contract.maximum}` });\n    }\n  }\n\n  if (Array.isArray(value)) {\n    if (contract.minItems !== undefined && value.length < contract.minItems) {\n      violations.push({ field, code: \"too_short\", message: `${field} must contain at least ${contract.minItems} item(s)`, expected: `minItems:${contract.minItems}` });\n    }\n    if (contract.maxItems !== undefined && value.length > contract.maxItems) {\n      violations.push({ field, code: \"too_long\", message: `${field} must contain at most ${contract.maxItems} item(s)`, expected: `maxItems:${contract.maxItems}` });\n    }\n    if (contract.items) {\n      value.forEach((item, index) => violations.push(...validateValue(`${field}[${index}]`, item, contract.items)));\n    }\n  }\n\n  return violations;\n};\n    const matchesRule = function matchesRule(rule, value) {\n  switch (rule.operator) {\n    case \"missing\": return !hasValue(value);\n    case \"truthy\": return value === true;\n    case \"falsy\": return value !== true;\n    case \"equals\": return value === rule.value;\n    case \"includes\": return Array.isArray(value) && value.includes(rule.value);\n    case \"gt\": return Number(value) > Number(rule.value);\n    case \"gte\": return Number(value) >= Number(rule.value);\n    case \"lt\": return Number(value) < Number(rule.value);\n    default: return false;\n  }\n};\n    const evaluatePolicy = function evaluatePolicy({ policy, envelope, executionId, evaluatedAt }) {\n  const input = envelope && typeof envelope === \"object\" && typeof envelope.body !== \"undefined\"\n    ? envelope.body\n    : envelope;\n  const headerRequestId = envelope?.headers?.[\"x-request-id\"];\n  const requestId = normalizeRequestId(headerRequestId, executionId);\n  const contentType = envelope?.headers?.[\"content-type\"];\n  if (hasValue(contentType) && !/^application\\/(?:[a-z0-9!#$&^_.+-]+\\+)?json(?:\\s*;|$)/i.test(String(contentType).trim())) {\n    return {\n      ok: false,\n      httpStatus: 415,\n      requestId,\n      error: \"unsupported_media_type\",\n      message: \"Request Content-Type must be application/json\",\n      expectedContentType: \"application/json\"\n    };\n  }\n  const schema = policy.inputSchema;\n  const violations = [];\n\n  if (!input || typeof input !== \"object\" || Array.isArray(input)) {\n    violations.push({ field: \"$\", code: \"invalid_type\", message: \"Request body must be a JSON object\", expected: \"object\" });\n  } else {\n    for (const field of schema.required) {\n      if (!hasValue(input[field])) {\n        violations.push({ field, code: \"required\", message: `${field} is required`, expected: schema.properties[field].type });\n      }\n    }\n    for (const [field, contract] of Object.entries(schema.properties)) {\n      if (hasValue(input[field])) violations.push(...validateValue(field, input[field], contract));\n    }\n  }\n\n  if (violations.length > 0) {\n    return {\n      ok: false,\n      httpStatus: 400,\n      requestId,\n      error: \"validation_error\",\n      message: \"Request does not match the workflow input contract\",\n      details: {\n        violations,\n        missingFields: violations.filter((item) => item.code === \"required\").map((item) => item.field)\n      },\n      requestSchema: schema\n    };\n  }\n\n  const matchedRules = policy.rules\n    .map((rule, index) => ({ ...rule, ruleId: rule.id ?? `${rule.field}_${rule.operator}_${index + 1}` }))\n    .filter((rule) => matchesRule(rule, input[rule.field]))\n    .map(({ ruleId, field, points, reason, minimumBand }) => ({ ruleId, field, points, reason, ...(minimumBand ? { minimumBand } : {}) }));\n  const rawScore = matchedRules.reduce((total, rule) => total + rule.points, 0);\n  const minimumScore = matchedRules.reduce((floor, rule) => Math.max(floor, rule.minimumBand ? policy.thresholds[rule.minimumBand] : 0), 0);\n  const score = Math.max(0, Math.min(100, Math.max(rawScore, minimumScore)));\n  const priorityBand = score >= policy.thresholds.high ? \"high\" : score >= policy.thresholds.medium ? \"medium\" : \"low\";\n\n  return {\n    ok: true,\n    httpStatus: 200,\n    requestId,\n    workflow: policy.slug,\n    policyVersion: policy.policyVersion,\n    decision: policy.decisions[priorityBand],\n    priorityBand,\n    score,\n    matchedRules,\n    recommendedActions: policy.actions,\n    evaluatedAt\n  };\n};\n    return evaluatePolicy({\n      policy: {\n  \"slug\": \"agent-evaluation-release-gate\",\n  \"policyVersion\": \"1.0.7\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"required\": [\n      \"evaluationRunId\",\n      \"changeType\",\n      \"evaluationCoveragePercent\",\n      \"qualityScore\"\n    ],\n    \"properties\": {\n      \"evaluationRunId\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 500,\n        \"pattern\": \"\\\\S\"\n      },\n      \"changeType\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\"\n      },\n      \"evaluationCoveragePercent\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 100\n      },\n      \"qualityScore\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 100\n      },\n      \"safetyRegression\": {\n        \"type\": \"boolean\"\n      },\n      \"latencyRegressionPercent\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 1000\n      },\n      \"costRegressionPercent\": {\n        \"type\": \"number\",\n        \"minimum\": 0,\n        \"maximum\": 1000\n      },\n      \"baselineAvailable\": {\n        \"type\": \"boolean\"\n      },\n      \"exceptionRequested\": {\n        \"type\": \"boolean\"\n      }\n    },\n    \"additionalProperties\": true\n  },\n  \"rules\": [\n    {\n      \"field\": \"evaluationCoveragePercent\",\n      \"operator\": \"lt\",\n      \"value\": 90,\n      \"points\": 30,\n      \"reason\": \"Evaluation coverage is below 90%\"\n    },\n    {\n      \"field\": \"qualityScore\",\n      \"operator\": \"lt\",\n      \"value\": 80,\n      \"points\": 35,\n      \"reason\": \"Quality score is below the release target\"\n    },\n    {\n      \"field\": \"safetyRegression\",\n      \"operator\": \"truthy\",\n      \"points\": 70,\n      \"minimumBand\": \"high\",\n      \"reason\": \"Evaluation detected a safety regression\"\n    },\n    {\n      \"field\": \"latencyRegressionPercent\",\n      \"operator\": \"gt\",\n      \"value\": 25,\n      \"points\": 25,\n      \"reason\": \"Latency regressed by more than 25%\"\n    },\n    {\n      \"field\": \"costRegressionPercent\",\n      \"operator\": \"gt\",\n      \"value\": 25,\n      \"points\": 20,\n      \"reason\": \"Unit cost regressed by more than 25%\"\n    },\n    {\n      \"field\": \"baselineAvailable\",\n      \"operator\": \"falsy\",\n      \"points\": 40,\n      \"reason\": \"No approved baseline is available for comparison\"\n    },\n    {\n      \"field\": \"exceptionRequested\",\n      \"operator\": \"truthy\",\n      \"points\": 70,\n      \"minimumBand\": \"high\",\n      \"reason\": \"Release requires an owner-approved policy exception\"\n    }\n  ],\n  \"thresholds\": {\n    \"medium\": 30,\n    \"high\": 70\n  },\n  \"decisions\": {\n    \"low\": \"recommend_release_candidate_for_owner_approval\",\n    \"medium\": \"route_release_evidence_to_owner_review\",\n    \"high\": \"hold_release_for_owner_exception_review\"\n  },\n  \"actions\": [\n    \"Present evaluation evidence to the release owner\",\n    \"Recommend remediation for failed quality, safety, latency, or cost gates\",\n    \"Keep deployment outside the starter until a release owner approves the evidence\"\n  ]\n},\n      envelope: $('Receive request').first().json,\n      executionId: $execution.id,\n      evaluatedAt: $now.toUTC().toISO()\n    });\n  })() }}",
        "includeOtherFields": false,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -60,
        80
      ],
      "id": "8731685a-7ed3-4dbc-a083-8c41e22a5d2f",
      "name": "Evaluate policy signals",
      "notes": "Evaluates one request with a native expression, enforces the documented input contract, and returns matched reasons without external writes.",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $('Evaluate policy signals').item.json }}",
        "options": {
          "responseCode": "={{ $('Evaluate policy signals').item.json.httpStatus }}",
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "Cache-Control",
                "value": "no-store"
              },
              {
                "name": "X-Content-Type-Options",
                "value": "nosniff"
              },
              {
                "name": "X-Request-Id",
                "value": "={{ $('Evaluate policy signals').item.json.requestId }}"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        240,
        80
      ],
      "id": "e44468a7-f04a-4c62-aa8c-8a6f19a1f6c3",
      "name": "Return structured decision"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ (() => {\n            const supplied = $('Receive request').first().json.headers?.['x-request-id'];\n            const requestId = String(supplied ?? $execution.id).replace(/[\\r\\n]/g, '').trim().slice(0, 200) || String($execution.id);\n            return {\n              ok: false,\n              httpStatus: 500,\n              requestId,\n              workflow: \"agent-evaluation-release-gate\",\n              policyVersion: \"1.0.7\",\n              error: 'internal_error',\n              message: 'The policy could not be evaluated',\n              retryable: true\n            };\n          })() }}",
        "options": {
          "responseCode": 500,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "Cache-Control",
                "value": "no-store"
              },
              {
                "name": "X-Content-Type-Options",
                "value": "nosniff"
              },
              {
                "name": "X-Request-Id",
                "value": "={{ (() => {\n                    const supplied = $('Receive request').first().json.headers?.['x-request-id'];\n                    return String(supplied ?? $execution.id).replace(/[\\r\\n]/g, '').trim().slice(0, 200) || String($execution.id);\n                  })() }}"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        240,
        260
      ],
      "id": "a6398ea0-8e8d-4d96-a7d1-7689adc9f24d",
      "name": "Return internal error",
      "notes": "Returns a sanitized, retryable 500 response without exposing stack traces, node details, or caller data."
    }
  ],
  "connections": {
    "Receive request": {
      "main": [
        [
          {
            "node": "Evaluate policy signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate policy signals": {
      "main": [
        [
          {
            "node": "Return structured decision",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return internal error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 120,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": true,
    "timezone": "UTC"
  },
  "tags": []
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Gate agent releases with evaluation evidence. Webhook trigger; 5 nodes.

Source: https://github.com/zarif3624/n8n-enterprise-workflows/blob/main/workflows/artificial-intelligence/agent-evaluation-release-gate/workflow.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

AGA Complaint Agent (LINE). Uses httpRequest, googleSheets. Webhook trigger; 13 nodes.

HTTP Request, Google Sheets
AI & RAG

Send Postcards to Contacts Automatically using CentralStationCRM and EchtPost. Uses httpRequest. Webhook trigger; 12 nodes.

HTTP Request
AI & RAG

Clara AI - Demo to Agent Pipeline. Uses start, executeCommand. Webhook trigger; 8 nodes.

Start, Execute Command
AI & RAG

Voice / Booking Agent (template). Webhook trigger; 6 nodes.

AI & RAG

v2 Like Minds — Instagram Publish. Uses httpRequest. Webhook trigger; 6 nodes.

HTTP Request