AutomationFlowsAI & RAG › Thesis Arena · Analyze Claim

Thesis Arena · Analyze Claim

Thesis Arena · Analyze Claim. Uses postgres, agent, lmChatOpenRouter, outputParserStructured. Webhook trigger; 36 nodes.

Webhook trigger★★★★★ complexityAI-powered36 nodesPostgresAgentOpenRouter ChatOutput Parser Structured
AI & RAG Trigger: Webhook Nodes: 36 Complexity: ★★★★★ AI nodes: yes Added:

This workflow follows the Agent → OpenRouter Chat recipe pattern — see all workflows that pair these two integrations.

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
{
  "name": "Thesis Arena \u00b7 Analyze Claim",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "thesis-arena-analyze",
        "authentication": "none",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1100,
        0
      ],
      "id": "b8c8bd98-ee4e-4c9b-9386-ae9833dee80b",
      "name": "Analyze Claim Webhook"
    },
    {
      "parameters": {
        "jsCode": "const payload = $json.body ?? $json;\nconst headers = $json.headers ?? {};\nconst thesis = String(payload.thesis ?? \"\").replace(/\\s+/g, \" \").trim();\nconst language = payload.language === \"ru\" ? \"ru\" : \"en\";\nconst accountUsername = String(headers[\"x-thesis-arena-user\"] ?? headers[\"X-Thesis-Arena-User\"] ?? \"\").trim().toLowerCase();\nif (thesis.length < 12) throw new Error(\"Thesis must contain at least 12 characters.\");\nif (thesis.length > 480) throw new Error(\"Thesis must contain at most 480 characters.\");\nif (!/^[a-z0-9_-]{1,64}$/.test(accountUsername)) throw new Error(\"A trusted account identity is required.\");\nreturn [{\n  json: {\n    thesis,\n    language,\n    account_username: accountUsername,\n    contract_version: String(payload.contract_version ?? \"1.0\"),\n    started_at_ms: Date.now(),\n    models: {\n      analysis: \"openai/gpt-5.6-luna\",\n      fact_check: \"openai/gpt-5.6-luna:online\"\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -850,
        0
      ],
      "id": "bcf43aed-cb03-4540-ba01-03388cc92d1f",
      "name": "Prepare Request"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH account_before AS MATERIALIZED (\n  SELECT username, role, quota, used_count, enabled\n  FROM thesis_arena.access_accounts\n  WHERE username = $1\n), consumed AS (\n  UPDATE thesis_arena.access_accounts\n  SET used_count = used_count + 1,\n      last_used_at = now(),\n      updated_at = now()\n  WHERE username = $1\n    AND enabled = true\n    AND (quota IS NULL OR used_count < quota)\n  RETURNING username, role, quota, used_count\n), initialized_run AS (\n  INSERT INTO thesis_arena.runs (\n    execution_id, account_username, thesis, language, status, stage,\n    analysis_model, fact_check_model, success, error, payload, duration_ms,\n    started_at, finished_at, updated_at\n  )\n  SELECT\n    $2, consumed.username, $3, $4, 'running', 'accepted', $5, $6, false, '',\n    jsonb_build_object(\n      'thesis', $3,\n      'language', $4,\n      'access', jsonb_build_object(\n        'username', consumed.username,\n        'role', consumed.role,\n        'quota', consumed.quota,\n        'used_count', consumed.used_count,\n        'remaining_attempts', CASE\n          WHEN consumed.quota IS NULL THEN NULL\n          ELSE GREATEST(consumed.quota - consumed.used_count, 0)\n        END\n      )\n    ),\n    0, $7::timestamptz, NULL, now()\n  FROM consumed\n  ON CONFLICT (execution_id) DO UPDATE SET\n    account_username = EXCLUDED.account_username,\n    thesis = EXCLUDED.thesis,\n    language = EXCLUDED.language,\n    status = EXCLUDED.status,\n    stage = EXCLUDED.stage,\n    analysis_model = EXCLUDED.analysis_model,\n    fact_check_model = EXCLUDED.fact_check_model,\n    payload = EXCLUDED.payload,\n    updated_at = now()\n  RETURNING execution_id\n)\nSELECT\n  consumed.username IS NOT NULL AS allowed,\n  COALESCE(consumed.username, account_before.username, $1) AS username,\n  COALESCE(consumed.role, account_before.role, 'unknown') AS role,\n  COALESCE(consumed.quota, account_before.quota) AS quota,\n  COALESCE(consumed.used_count, account_before.used_count, 0) AS used_count,\n  CASE\n    WHEN COALESCE(consumed.quota, account_before.quota) IS NULL THEN NULL\n    ELSE GREATEST(\n      COALESCE(consumed.quota, account_before.quota) -\n      COALESCE(consumed.used_count, account_before.used_count, 0),\n      0\n    )\n  END AS remaining_attempts,\n  COALESCE(consumed.quota, account_before.quota) IS NULL AS unlimited,\n  CASE\n    WHEN consumed.username IS NOT NULL THEN 'allowed'\n    WHEN account_before.username IS NULL THEN 'unknown_account'\n    WHEN account_before.enabled = false THEN 'disabled'\n    ELSE 'limit_exhausted'\n  END AS reason,\n  (SELECT COUNT(*) FROM initialized_run) AS initialized_runs\nFROM (SELECT 1) AS seed\nLEFT JOIN consumed ON true\nLEFT JOIN account_before ON true;",
        "options": {
          "queryReplacement": "={{ [\n        String($json.account_username),\n        String($execution.id),\n        String($json.thesis),\n        String($json.language),\n        String($json.models.analysis),\n        String($json.models.fact_check),\n        new Date(Number($json.started_at_ms)).toISOString()\n      ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -590,
        0
      ],
      "id": "f808805c-b096-4310-b7bd-c8a43c4a2d84",
      "name": "Consume Access"
    },
    {
      "parameters": {
        "jsCode": "const base = $(\"Prepare Request\").first().json;\nconst quota = $json.quota == null ? null : Number($json.quota);\nconst remaining = $json.remaining_attempts == null ? null : Number($json.remaining_attempts);\nreturn [{\n  json: {\n    ...base,\n    access: {\n      allowed: $json.allowed === true,\n      username: String($json.username ?? base.account_username),\n      role: String($json.role ?? \"unknown\"),\n      quota,\n      used_count: Number($json.used_count ?? 0),\n      remaining_attempts: remaining,\n      unlimited: quota == null,\n      reason: String($json.reason ?? \"unknown_account\")\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -330,
        0
      ],
      "id": "add0b3d1-8097-42cc-acd0-638425f3848b",
      "name": "Attach Access"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json.access.allowed\n      ? {\n          state: \"accepted\",\n          execution_id: String($execution.id),\n          poll_after_ms: 2500,\n          access: {\n            username: $json.access.username,\n            role: $json.access.role,\n            unlimited: $json.access.unlimited,\n            remaining_attempts: $json.access.remaining_attempts\n          }\n        }\n      : {\n          state: \"denied\",\n          code: $json.access.reason === \"limit_exhausted\" ? \"quota_exhausted\" : \"access_denied\",\n          error: $json.language === \"ru\"\n            ? ($json.access.reason === \"limit_exhausted\"\n              ? \"\u0412\u0441\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u0430\u043d\u0430\u043b\u0438\u0437\u044b \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b.\"\n              : \"\u042d\u0442\u043e\u043c\u0443 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0443 \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043d \u0437\u0430\u043f\u0443\u0441\u043a \u0430\u043d\u0430\u043b\u0438\u0437\u0430.\")\n            : ($json.access.reason === \"limit_exhausted\"\n              ? \"This account has used all available analyses.\"\n              : \"This account is not allowed to start an analysis.\"),\n          access: {\n            username: $json.access.username,\n            role: $json.access.role,\n            unlimited: $json.access.unlimited,\n            remaining_attempts: $json.access.remaining_attempts\n          }\n        } }}",
        "options": {
          "responseCode": "={{ $json.access.allowed ? 202 : ($json.access.reason === \"limit_exhausted\" ? 429 : 403) }}",
          "responseHeaders": {
            "entries": [
              {
                "name": "Cache-Control",
                "value": "no-store"
              },
              {
                "name": "X-Content-Type-Options",
                "value": "nosniff"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        -70,
        0
      ],
      "id": "d1626935-b15e-48e8-9235-58ca9ed23116",
      "name": "Acknowledge Run"
    },
    {
      "parameters": {
        "jsCode": "if (!$json.access?.allowed) return [];\nreturn $input.all();"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        190,
        0
      ],
      "id": "ad689e23-41fa-49a7-ba7e-a001d5c63cd3",
      "name": "Continue If Authorized"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nThesis to map:\n{{ $json.thesis }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the Claim Mapper for Thesis Arena. Decompose the user's thesis into 2\u20135 concise claims using Toulmin-style reasoning. Identify conclusions, warrants, causal links, empirical premises, value judgments, interpretations, and predictions. Do not critique the thesis yet. Each claim must be independently understandable and preserve the author's intended meaning. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        460,
        0
      ],
      "id": "9db44a75-0b92-4bd3-9ef9-24edf031819e",
      "name": "Claim Mapper",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        350,
        250
      ],
      "id": "e85e37b0-c585-40db-824a-208696df999b",
      "name": "Claim Mapper \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"claims\"\n  ],\n  \"properties\": {\n    \"claims\": {\n      \"type\": \"array\",\n      \"minItems\": 2,\n      \"maxItems\": 5,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"text\",\n          \"type\"\n        ],\n        \"properties\": {\n          \"text\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"empirical_claim\",\n              \"causal_claim\",\n              \"value_judgment\",\n              \"interpretation\",\n              \"prediction\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        580,
        250
      ],
      "id": "553c5da0-51ac-472b-96a6-d4d75a5ed6ae",
      "name": "Claim Mapper \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let parsed = $json.output ?? $json;\nif (typeof parsed === \"string\") parsed = JSON.parse(parsed);\nconst base = $(\"Attach Access\").first().json;\nconst typeMeta = {\n  empirical_claim: [\"Empirical claim\", \"\u042d\u043c\u043f\u0438\u0440\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435\", \"amber\"],\n  causal_claim: [\"Causal claim\", \"\u041f\u0440\u0438\u0447\u0438\u043d\u043d\u043e\u0435 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435\", \"teal\"],\n  value_judgment: [\"Value judgment\", \"\u0426\u0435\u043d\u043d\u043e\u0441\u0442\u043d\u043e\u0435 \u0441\u0443\u0436\u0434\u0435\u043d\u0438\u0435\", \"amber\"],\n  interpretation: [\"Interpretation\", \"\u0418\u043d\u0442\u0435\u0440\u043f\u0440\u0435\u0442\u0430\u0446\u0438\u044f\", \"blue\"],\n  prediction: [\"Prediction\", \"\u041f\u0440\u043e\u0433\u043d\u043e\u0437\", \"blue\"]\n};\nconst claims = (parsed.claims ?? []).slice(0, 5).map((claim, index) => {\n  const type = typeMeta[claim.type] ? claim.type : \"interpretation\";\n  return {\n    id: \"C\" + (index + 1),\n    text: String(claim.text ?? \"\").trim(),\n    type,\n    label: base.language === \"ru\" ? typeMeta[type][1] : typeMeta[type][0],\n    tone: typeMeta[type][2]\n  };\n}).filter((claim) => claim.text);\nif (claims.length < 1) throw new Error(\"Claim Mapper returned no usable claims.\");\nreturn [{ json: { ...base, claims, agents: {} } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        730,
        0
      ],
      "id": "90ab7661-ba0e-44a2-a599-ef054b51be4d",
      "name": "Store Claim Map"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nOriginal thesis:\n{{ $json.thesis }}\n\nClaim map:\n{{ JSON.stringify($json.claims) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the Red Team lens in Thesis Arena. Stress-test the claim aggressively but fairly. Find hidden assumptions, counterexamples, omitted alternatives, scope problems, causal confounds, edge cases, and practical tradeoffs. Focus on the argument, never the author. Produce 5\u20137 distinct pressure points. The summary must be one crisp sentence; each detail should be 2\u20134 useful sentences. Reference only claim IDs from the supplied map. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1010,
        0
      ],
      "id": "ea4fbdb1-94df-485d-8b38-65e48e6ea951",
      "name": "Red Team",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        900,
        250
      ],
      "id": "7137e6a8-e667-43ac-b145-2499cbc9f399",
      "name": "Red Team \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"summary\",\n    \"findings\"\n  ],\n  \"properties\": {\n    \"summary\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"findings\": {\n      \"type\": \"array\",\n      \"minItems\": 5,\n      \"maxItems\": 7,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"title\",\n          \"detail\",\n          \"claim_ids\",\n          \"severity\"\n        ],\n        \"properties\": {\n          \"title\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"detail\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"claim_ids\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": {\n              \"type\": \"string\",\n              \"pattern\": \"^C[1-9][0-9]*$\"\n            }\n          },\n          \"severity\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"low\",\n              \"medium\",\n              \"high\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1130,
        250
      ],
      "id": "e8095826-071e-470e-a8bd-4bb2af5d0b94",
      "name": "Red Team \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let parsed = $json.output ?? $json;\nif (typeof parsed === \"string\") parsed = JSON.parse(parsed);\nconst previous = $(\"Store Claim Map\").first().json;\nconst validClaims = new Set(previous.claims.map((claim) => claim.id));\nconst findings = (parsed.findings ?? []).map((finding, index) => {\n  const item = {\n    id: \"R\" + (index + 1),\n    title: String(finding.title ?? \"\").trim(),\n    detail: String(finding.detail ?? \"\").trim(),\n    claim_ids: (finding.claim_ids ?? []).map(String).filter((id) => validClaims.has(id)),\n    severity: [\"low\", \"medium\", \"high\"].includes(finding.severity) ? finding.severity : \"medium\"\n  };\n  \n  if (!item.claim_ids.length && previous.claims[0]) item.claim_ids = [previous.claims[0].id];\n  return item;\n}).filter((finding) => finding.title && finding.detail);\nif (!findings.length) throw new Error(\"Red Team returned no usable findings.\");\nreturn [{\n  json: {\n    ...previous,\n    agents: {\n      ...previous.agents,\n      red_team: {\n        name: previous.language === \"ru\" ? \"\u041a\u0440\u0430\u0441\u043d\u0430\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0430\" : \"Red Team\",\n        tone: \"red\",\n        summary: String(parsed.summary ?? \"\").trim(),\n        count: findings.length,\n        count_label: previous.language === \"ru\" ? \"\u043a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043f\u0443\u043d\u043a\u0442\u043e\u0432\" : \"pressure points\",\n        findings\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        0
      ],
      "id": "955e7f33-cbd5-4b75-8144-826242244d1e",
      "name": "Store Red Team"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nOriginal thesis:\n{{ $json.thesis }}\n\nClaim map:\n{{ JSON.stringify($json.claims) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the Steelman lens in Thesis Arena. Reconstruct the strongest defensible version of the argument. Supply missing warrants, necessary conditions, useful qualifications, boundary conditions, and the best available support without pretending uncertain premises are proven. Produce 4\u20136 distinct supporting arguments. The summary must be one crisp sentence; each detail should be 2\u20134 useful sentences. Reference only claim IDs from the supplied map. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        1550,
        0
      ],
      "id": "b2a93b62-2dcd-4253-81d7-21fa3bcb602e",
      "name": "Steelman",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        1440,
        250
      ],
      "id": "7eb05925-e3fa-4a52-9480-eefd317eca91",
      "name": "Steelman \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"summary\",\n    \"findings\"\n  ],\n  \"properties\": {\n    \"summary\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"findings\": {\n      \"type\": \"array\",\n      \"minItems\": 4,\n      \"maxItems\": 6,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"title\",\n          \"detail\",\n          \"claim_ids\",\n          \"severity\"\n        ],\n        \"properties\": {\n          \"title\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"detail\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"claim_ids\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": {\n              \"type\": \"string\",\n              \"pattern\": \"^C[1-9][0-9]*$\"\n            }\n          },\n          \"severity\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"low\",\n              \"medium\",\n              \"high\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        1670,
        250
      ],
      "id": "92df2fd2-279c-4baa-a45b-39c909e60457",
      "name": "Steelman \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let parsed = $json.output ?? $json;\nif (typeof parsed === \"string\") parsed = JSON.parse(parsed);\nconst previous = $(\"Store Red Team\").first().json;\nconst validClaims = new Set(previous.claims.map((claim) => claim.id));\nconst findings = (parsed.findings ?? []).map((finding, index) => {\n  const item = {\n    id: \"S\" + (index + 1),\n    title: String(finding.title ?? \"\").trim(),\n    detail: String(finding.detail ?? \"\").trim(),\n    claim_ids: (finding.claim_ids ?? []).map(String).filter((id) => validClaims.has(id)),\n    severity: [\"low\", \"medium\", \"high\"].includes(finding.severity) ? finding.severity : \"medium\"\n  };\n  \n  if (!item.claim_ids.length && previous.claims[0]) item.claim_ids = [previous.claims[0].id];\n  return item;\n}).filter((finding) => finding.title && finding.detail);\nif (!findings.length) throw new Error(\"Steelman returned no usable findings.\");\nreturn [{\n  json: {\n    ...previous,\n    agents: {\n      ...previous.agents,\n      steelman: {\n        name: previous.language === \"ru\" ? \"\u0423\u0441\u0438\u043b\u0435\u043d\u0438\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430\" : \"Steelman\",\n        tone: \"teal\",\n        summary: String(parsed.summary ?? \"\").trim(),\n        count: findings.length,\n        count_label: previous.language === \"ru\" ? \"\u0443\u0441\u0438\u043b\u0438\u0432\u0430\u044e\u0449\u0438\u0445 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u043e\u0432\" : \"supporting arguments\",\n        findings\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1820,
        0
      ],
      "id": "2f4fd798-38a9-4c15-b0b4-e2a5fbd97db6",
      "name": "Store Steelman"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nOriginal thesis:\n{{ $json.thesis }}\n\nClaim map:\n{{ JSON.stringify($json.claims) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the Logic lens in Thesis Arena. Check internal consistency, warrants, quantifiers, causal inference, contradictions, non sequiturs, equivocation, and whether the conclusion follows from the premises. Separate a merely plausible link from an established one. Produce 3\u20135 distinct reasoning gaps. The summary must be one crisp sentence; each detail should be 2\u20134 useful sentences. Reference only claim IDs from the supplied map. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2090,
        0
      ],
      "id": "c98f5d93-6bb4-43ff-8d10-fcc100be9d62",
      "name": "Logic",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        1980,
        250
      ],
      "id": "106a6dad-3666-4486-a114-cc35649554a1",
      "name": "Logic \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"summary\",\n    \"findings\"\n  ],\n  \"properties\": {\n    \"summary\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"findings\": {\n      \"type\": \"array\",\n      \"minItems\": 3,\n      \"maxItems\": 5,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"title\",\n          \"detail\",\n          \"claim_ids\",\n          \"severity\"\n        ],\n        \"properties\": {\n          \"title\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"detail\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"claim_ids\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": {\n              \"type\": \"string\",\n              \"pattern\": \"^C[1-9][0-9]*$\"\n            }\n          },\n          \"severity\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"low\",\n              \"medium\",\n              \"high\"\n            ]\n          }\n        }\n      }\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        2210,
        250
      ],
      "id": "13fa2bda-b9d0-4290-bd93-285a22520405",
      "name": "Logic \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let parsed = $json.output ?? $json;\nif (typeof parsed === \"string\") parsed = JSON.parse(parsed);\nconst previous = $(\"Store Steelman\").first().json;\nconst validClaims = new Set(previous.claims.map((claim) => claim.id));\nconst findings = (parsed.findings ?? []).map((finding, index) => {\n  const item = {\n    id: \"L\" + (index + 1),\n    title: String(finding.title ?? \"\").trim(),\n    detail: String(finding.detail ?? \"\").trim(),\n    claim_ids: (finding.claim_ids ?? []).map(String).filter((id) => validClaims.has(id)),\n    severity: [\"low\", \"medium\", \"high\"].includes(finding.severity) ? finding.severity : \"medium\"\n  };\n  \n  if (!item.claim_ids.length && previous.claims[0]) item.claim_ids = [previous.claims[0].id];\n  return item;\n}).filter((finding) => finding.title && finding.detail);\nif (!findings.length) throw new Error(\"Logic returned no usable findings.\");\nreturn [{\n  json: {\n    ...previous,\n    agents: {\n      ...previous.agents,\n      logic: {\n        name: previous.language === \"ru\" ? \"\u041b\u043e\u0433\u0438\u043a\u0430\" : \"Logic\",\n        tone: \"blue\",\n        summary: String(parsed.summary ?? \"\").trim(),\n        count: findings.length,\n        count_label: previous.language === \"ru\" ? \"\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043f\u0440\u043e\u0431\u0435\u043b\u043e\u0432\" : \"reasoning gaps\",\n        findings\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2360,
        0
      ],
      "id": "41c40724-138c-46f4-b432-14f83c8f8070",
      "name": "Store Logic"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nOriginal thesis:\n{{ $json.thesis }}\n\nClaim map:\n{{ JSON.stringify($json.claims) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the Fact Check lens in Thesis Arena and have web search enabled. Check only externally verifiable empirical, numerical, historical, scientific, and causal claims. Do not pretend that opinions, preferences, or value judgments can be fact-checked. Search for credible primary or authoritative sources. Never invent a URL or citation. If reliable evidence cannot be found, use insufficient_evidence and an empty sources array. Produce 2\u20135 evidence findings. The summary must be one crisp sentence; each detail should be 2\u20134 useful sentences. Reference only claim IDs from the supplied map. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        2630,
        0
      ],
      "id": "ec7ab3df-3553-4cad-b50e-6cefb2546a7c",
      "name": "Fact Check",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna:online",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        2520,
        250
      ],
      "id": "1c5eb9f4-7c16-46ad-b8e2-1bbb809e5e5b",
      "name": "Fact Check \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"summary\",\n    \"findings\"\n  ],\n  \"properties\": {\n    \"summary\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"findings\": {\n      \"type\": \"array\",\n      \"minItems\": 2,\n      \"maxItems\": 5,\n      \"items\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"required\": [\n          \"title\",\n          \"detail\",\n          \"claim_ids\",\n          \"severity\",\n          \"status\",\n          \"sources\"\n        ],\n        \"properties\": {\n          \"title\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"detail\": {\n            \"type\": \"string\",\n            \"minLength\": 1\n          },\n          \"claim_ids\": {\n            \"type\": \"array\",\n            \"minItems\": 1,\n            \"items\": {\n              \"type\": \"string\",\n              \"pattern\": \"^C[1-9][0-9]*$\"\n            }\n          },\n          \"severity\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"low\",\n              \"medium\",\n              \"high\"\n            ]\n          },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"supported\",\n              \"partially_supported\",\n              \"mixed\",\n              \"unsupported\",\n              \"insufficient_evidence\"\n            ]\n          },\n          \"sources\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"additionalProperties\": false,\n              \"required\": [\n                \"title\",\n                \"publisher\",\n                \"url\",\n                \"relation\"\n              ],\n              \"properties\": {\n                \"title\": {\n                  \"type\": \"string\",\n                  \"minLength\": 1\n                },\n                \"publisher\": {\n                  \"type\": \"string\",\n                  \"minLength\": 1\n                },\n                \"url\": {\n                  \"type\": \"string\",\n                  \"minLength\": 8\n                },\n                \"relation\": {\n                  \"type\": \"string\",\n                  \"minLength\": 1\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        2750,
        250
      ],
      "id": "8d64b270-28ab-4921-a50f-6fc853faa579",
      "name": "Fact Check \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let parsed = $json.output ?? $json;\nif (typeof parsed === \"string\") parsed = JSON.parse(parsed);\nconst previous = $(\"Store Logic\").first().json;\nconst validClaims = new Set(previous.claims.map((claim) => claim.id));\nconst findings = (parsed.findings ?? []).map((finding, index) => {\n  const item = {\n    id: \"F\" + (index + 1),\n    title: String(finding.title ?? \"\").trim(),\n    detail: String(finding.detail ?? \"\").trim(),\n    claim_ids: (finding.claim_ids ?? []).map(String).filter((id) => validClaims.has(id)),\n    severity: [\"low\", \"medium\", \"high\"].includes(finding.severity) ? finding.severity : \"medium\"\n  };\n  item.status = [\"supported\", \"partially_supported\", \"mixed\", \"unsupported\", \"insufficient_evidence\"].includes(finding.status) ? finding.status : \"insufficient_evidence\";\n  item.sources = (finding.sources ?? []).filter((source) => /^https?:\\/\\//i.test(String(source.url ?? \"\"))).map((source) => ({\n    title: String(source.title ?? \"Untitled source\").trim(),\n    publisher: String(source.publisher ?? \"Source\").trim(),\n    url: String(source.url).trim(),\n    relation: String(source.relation ?? \"context\").trim()\n  }));\n  if (!item.claim_ids.length && previous.claims[0]) item.claim_ids = [previous.claims[0].id];\n  return item;\n}).filter((finding) => finding.title && finding.detail);\nif (!findings.length) throw new Error(\"Fact Check returned no usable findings.\");\nreturn [{\n  json: {\n    ...previous,\n    agents: {\n      ...previous.agents,\n      fact_check: {\n        name: previous.language === \"ru\" ? \"\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0444\u0430\u043a\u0442\u043e\u0432\" : \"Fact Check\",\n        tone: \"amber\",\n        summary: String(parsed.summary ?? \"\").trim(),\n        count: findings.length,\n        count_label: previous.language === \"ru\" ? \"\u0432\u044b\u0432\u043e\u0434\u043e\u0432 \u043f\u043e \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430\u043c\" : \"evidence findings\",\n        findings\n      }\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2900,
        0
      ],
      "id": "db7bd02c-00c5-4209-a075-9e5d112773e7",
      "name": "Store Fact Check"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Language: {{ $json.language }}\n\nOriginal thesis:\n{{ $json.thesis }}\n\nClaim map:\n{{ JSON.stringify($json.claims) }}\n\nAnalytical lenses:\n{{ JSON.stringify($json.agents) }}",
        "hasOutputParser": true,
        "options": {
          "systemMessage": "You are the final Synthesizer in Thesis Arena. Reconcile the independent lenses without averaging them into vagueness. State what genuinely survived, what failed, the hidden assumptions, and the single key crux: the decisive empirical or logical hinge that could change the conclusion. Then rewrite the thesis into a more precise, conditional, defensible formulation. Preserve the author's core intent where possible. Be concise but substantive. When Language is ru, write every user-facing text field in Russian; otherwise write in English. Return only the structured result requested by the output parser."
        }
      },
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        3170,
        0
      ],
      "id": "f7e51f32-dcd5-45fe-a054-51327de5c271",
      "name": "Synthesizer",
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "model": "openai/gpt-5.6-luna",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "typeVersion": 1,
      "position": [
        3060,
        250
      ],
      "id": "d3c772ae-e6ed-4293-a895-961525be6055",
      "name": "Synthesizer \u00b7 Model"
    },
    {
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"required\": [\n    \"survived\",\n    \"failed\",\n    \"hidden_assumptions\",\n    \"key_crux\",\n    \"improved_thesis\"\n  ],\n  \"properties\": {\n    \"survived\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"failed\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"hidden_assumptions\": {\n      \"type\": \"array\",\n      \"minItems\": 2,\n      \"maxItems\": 6,\n      \"items\": {\n        \"type\": \"string\",\n        \"minLength\": 1\n      }\n    },\n    \"key_crux\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    },\n    \"improved_thesis\": {\n      \"type\": \"string\",\n      \"minLength\": 1\n    }\n  }\n}"
      },
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "typeVersion": 1.3,
      "position": [
        3290,
        250
      ],
      "id": "97beffed-6902-4c9a-b85c-23c2d49c359e",
      "name": "Synthesizer \u00b7 Parser"
    },
    {
      "parameters": {
        "jsCode": "let synthesis = $json.output ?? $json;\nif (typeof synthesis === \"string\") synthesis = JSON.parse(synthesis);\nconst state = $(\"Store Fact Check\").first().json;\nconst durationMs = Math.max(0, Date.now() - Number(state.started_at_ms ?? Date.now()));\nreturn [{\n  json: {\n    version: \"1.0\",\n    input: { thesis: state.thesis },\n    claims: state.claims,\n    agents: state.agents,\n    synthesis: {\n      survived: String(synthesis.survived ?? \"\").trim(),\n      failed: String(synthesis.failed ?? \"\").trim(),\n      hidden_assumptions: (synthesis.hidden_assumptions ?? []).map(String).filter(Boolean),\n      key_crux: String(synthesis.key_crux ?? \"\").trim(),\n      improved_thesis: String(synthesis.improved_thesis ?? \"\").trim()\n    },\n    meta: {\n      execution_id: String($execution.id),\n      model: \"openai/gpt-5.6-luna \u00b7 Fact Check: openai/gpt-5.6-luna:online\",\n      duration_ms: durationMs,\n      mode: \"live\",\n      language: state.language ?? \"en\",\n      account_username: state.access?.username ?? \"\",\n      access: {\n        username: state.access?.username ?? \"\",\n        role: state.access?.role ?? \"unknown\",\n        unlimited: Boolean(state.access?.unlimited),\n        remaining_attempts: state.access?.remaining_attempts ?? null\n      },\n      persisted: true,\n      completed: true\n    }\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3440,
        0
      ],
      "id": "c22b0bab-dc2b-49a5-9376-577f7f841f38",
      "name": "Assemble Response"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"running\",\n          \"claim_map\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          false,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          null\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        730,
        520
      ],
      "id": "7c73654b-3857-4484-abc2-9b22f6039b9e",
      "name": "Archive \u00b7 Claim Map",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"running\",\n          \"red_team\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          false,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          null\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1280,
        520
      ],
      "id": "9edf3af8-dde8-4f58-8331-e3346d35130e",
      "name": "Archive \u00b7 Red Team",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"running\",\n          \"steelman\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          false,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          null\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1820,
        520
      ],
      "id": "f78c3a3b-7357-46b4-a52d-0d12038aa1dc",
      "name": "Archive \u00b7 Steelman",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"running\",\n          \"logic\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          false,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          null\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2360,
        520
      ],
      "id": "cac1317e-2c4b-4f13-8194-e9b2e49ce3ae",
      "name": "Archive \u00b7 Logic",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"running\",\n          \"fact_check\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          false,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          null\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2900,
        520
      ],
      "id": "7ddd4413-6c5d-4c30-b18c-f8c648a28882",
      "name": "Archive \u00b7 Fact Check",
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO thesis_arena.runs (\n  execution_id, account_username, thesis, language, status, stage, analysis_model,\n  fact_check_model, success, error, payload, duration_ms, started_at, finished_at, updated_at\n) VALUES (\n  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12, $13::timestamptz, $14::timestamptz, now()\n)\nON CONFLICT (execution_id) DO UPDATE SET\n  account_username = COALESCE(NULLIF(EXCLUDED.account_username, ''), thesis_arena.runs.account_username),\n  thesis = EXCLUDED.thesis,\n  language = EXCLUDED.language,\n  status = EXCLUDED.status,\n  stage = EXCLUDED.stage,\n  analysis_model = EXCLUDED.analysis_model,\n  fact_check_model = EXCLUDED.fact_check_model,\n  success = EXCLUDED.success,\n  error = EXCLUDED.error,\n  payload = EXCLUDED.payload,\n  duration_ms = EXCLUDED.duration_ms,\n  started_at = COALESCE(thesis_arena.runs.started_at, EXCLUDED.started_at),\n  finished_at = COALESCE(EXCLUDED.finished_at, thesis_arena.runs.finished_at),\n  updated_at = now()\nRETURNING execution_id;",
        "options": {
          "queryReplacement": "={{ [\n          String($execution.id),\n          String($json.access?.username ?? $json.meta?.account_username ?? ''),\n          String($json.thesis ?? $json.input?.thesis ?? ''),\n          String($json.language ?? $json.meta?.language ?? 'en'),\n          \"success\",\n          \"completed\",\n          String($json.models?.analysis ?? $json.meta?.analysis_model ?? $json.meta?.model ?? ''),\n          String($json.models?.fact_check ?? $json.meta?.fact_check_model ?? ''),\n          true,\n          '',\n          JSON.stringify($json),\n          Number($json.meta?.duration_ms ?? (Date.now() - Number($json.started_at_ms ?? Date.now()))),\n          new Date(Number($json.started_at_ms ?? Date.now())).toISOString(),\n          new Date().toISOString()\n        ] }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        3440,
        520
      ],
      "id": "b7d50f7f-bee2-4345-bf59-11bdc5aa45f9",
      "name": "Archive \u00b7 Completed",
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Analyze Claim Webhook": {
      "main": [
        [
          {
            "node": "Prepare Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Request": {
      "main": [
        [
          {
            "node": "Consume Access",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Consume Access": {
      "main": [
        [
          {
            "node": "Attach Access",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Access": {
      "main": [
        [
          {
            "node": "Acknowledge Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Acknowledge Run": {
      "main": [
        [
          {
            "node": "Continue If Authorized",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim Mapper \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Claim Mapper",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claim Mapper \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Claim Mapper",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Continue If Authorized": {
      "main": [
        [
          {
            "node": "Claim Mapper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim Mapper": {
      "main": [
        [
          {
            "node": "Store Claim Map",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Red Team \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Red Team",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Red Team \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Red Team",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Store Claim Map": {
      "main": [
        [
          {
            "node": "Red Team",
            "type": "main",
            "index": 0
          },
          {
            "node": "Archive \u00b7 Claim Map",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Red Team": {
      "main": [
        [
          {
            "node": "Store Red Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Steelman \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Steelman",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Steelman \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Steelman",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Store Red Team": {
      "main": [
        [
          {
            "node": "Steelman",
            "type": "main",
            "index": 0
          },
          {
            "node": "Archive \u00b7 Red Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Steelman": {
      "main": [
        [
          {
            "node": "Store Steelman",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Logic \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Logic",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Logic \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Logic",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Store Steelman": {
      "main": [
        [
          {
            "node": "Logic",
            "type": "main",
            "index": 0
          },
          {
            "node": "Archive \u00b7 Steelman",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Logic": {
      "main": [
        [
          {
            "node": "Store Logic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fact Check \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Fact Check",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fact Check \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Fact Check",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Store Logic": {
      "main": [
        [
          {
            "node": "Fact Check",
            "type": "main",
            "index": 0
          },
          {
            "node": "Archive \u00b7 Logic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fact Check": {
      "main": [
        [
          {
            "node": "Store Fact Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthesizer \u00b7 Model": {
      "ai_languageModel": [
        [
          {
            "node": "Synthesizer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Synthesizer \u00b7 Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Synthesizer",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Store Fact Check": {
      "main": [
        [
          {
            "node": "Synthesizer",
            "type": "main",
            "index": 0
          },
          {
            "node": "Archive \u00b7 Fact Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Synthesizer": {
      "main": [
        [
          {
            "node": "Assemble Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble Response": {
      "main": [
        [
          {
            "node": "Archive \u00b7 Completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  }
}
Pro

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

About this workflow

Thesis Arena · Analyze Claim. Uses postgres, agent, lmChatOpenRouter, outputParserStructured. Webhook trigger; 36 nodes.

Source: https://github.com/PMaxus/thesis-arena/blob/main/n8n/thesis-arena-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

Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing

WordPress, OpenAI, Perplexity +8
AI & RAG

LineOA. Uses httpRequest, agent, lmChatGoogleGemini, outputParserStructured. Webhook trigger; 69 nodes.

HTTP Request, Agent, Google Gemini Chat +3
AI & RAG

🧪 LABR - nuevo asistente (REPARADO). Uses httpRequest, postgres, postgresTool, toolCalculator. Webhook trigger; 63 nodes.

HTTP Request, Postgres, Postgres Tool +9
AI & RAG

🧪 LABR - nuevo asistente (REPARADO). Uses httpRequest, postgres, postgresTool, toolCalculator. Webhook trigger; 63 nodes.

HTTP Request, Postgres, Postgres Tool +9
AI & RAG

🧪 LABR - nuevo asistente (REPARADO). Uses httpRequest, postgres, postgresTool, toolCalculator. Webhook trigger; 63 nodes.

HTTP Request, Postgres, Postgres Tool +9