{
  "name": "Lumeor | NDA generation from a request form",
  "nodes": [
    {
      "parameters": {
        "content": "## NDA generation starter\n\nA requester submits a short form (company, contact, purpose, mutual or one-way, free-text notes). Claude extracts the counterparty's legal name, signer and any non-standard asks as strict JSON. Review rules in code decide whether the request is standard. Standard requests get a copy of your approved Google Docs template with only the variable fields filled in. Everything else goes to legal in Slack with the reasons attached.\n\nThe legal text never passes through the model. It only fills named fields.\n\n### Setup\n1. Point your form (or a CRM button) at the **NDA request form** webhook URL. Fields: `requester_email`, `requester_name`, `counterparty_company`, `contact_name`, `contact_email`, `purpose`, `nda_type` (mutual or one_way), `counterparty_paper` (yes/no), `notes`.\n2. In **Validate request and build prompt**, replace `REPLACE_WITH_MUTUAL_TEMPLATE_DOC_ID` and `REPLACE_WITH_ONE_WAY_TEMPLATE_DOC_ID`. In **Apply review rules**, edit the allowed governing law list and maximum term.\n3. Tag your Google Docs templates with `[[COUNTERPARTY_LEGAL_NAME]]`, `[[COUNTERPARTY_SIGNER_NAME]]`, `[[COUNTERPARTY_SIGNER_TITLE]]`, `[[EFFECTIVE_DATE]]`, `[[PURPOSE]]`, `[[TERM_YEARS]]`, `[[GOVERNING_LAW]]`.\n4. Set `REPLACE_WITH_NDA_DRAFTS_FOLDER_ID` on **Copy NDA template**, and the Slack channel IDs on both Slack nodes.\n5. Credentials: Anthropic API (on **Claude: extract NDA fields**), Google Drive OAuth2, Google Docs OAuth2, Slack.\n\nThe model is `claude-opus-5-5`. `claude-haiku-5-5` is a cheaper option for high-volume, simple extraction like this.\n\nNot included: CRM lookup, e-signature envelope and the completion callback. Add them once this path is trusted.\n\nTemplate by Lumeor Studio, https://www.lumeor.studio/guides/automate-nda-generation/",
        "height": 820,
        "width": 500,
        "color": 7
      },
      "id": "cc11752c-6dd0-48ba-b527-61d15c466aec",
      "name": "About this template",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -520,
        -200
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "nda-request",
        "options": {}
      },
      "id": "830ff7d5-b1f6-4bc2-82ac-322ada6fbc41",
      "name": "NDA request form",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        300
      ],
      "webhookId": "aba5cf10-db60-40d0-bcf2-b053d044ad6e"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Config: approved template doc IDs (Google Docs) per NDA type.\nconst TEMPLATES = {\n  mutual: 'REPLACE_WITH_MUTUAL_TEMPLATE_DOC_ID',\n  one_way: 'REPLACE_WITH_ONE_WAY_TEMPLATE_DOC_ID',\n};\nconst emailOk = (s) => typeof s === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s.trim());\nconst clean = (s) => (typeof s === 'string' ? s.trim() : '');\n\n// Nullable field helper (structured outputs support anyOf and null).\nconst nul = (type, description) => ({ anyOf: [{ type }, { type: 'null' }], ...(description ? { description } : {}) });\n\nconst schema = {\n  type: 'object',\n  additionalProperties: false,\n  required: ['counterparty_legal_name', 'signer_name', 'signer_title', 'signer_email', 'counterparty_country', 'requested_governing_law', 'requested_term_years', 'purpose_summary', 'counterparty_paper', 'requested_changes', 'confidence'],\n  properties: {\n    counterparty_legal_name: nul('string', 'Full legal entity name, e.g. \"Acme Robotics, Inc.\". Null if not stated.'),\n    signer_name: nul('string'),\n    signer_title: nul('string'),\n    signer_email: nul('string'),\n    counterparty_country: nul('string', 'Country of the counterparty entity if stated or clearly implied.'),\n    requested_governing_law: nul('string', 'US state or country the requester asked for, or null.'),\n    requested_term_years: nul('number'),\n    purpose_summary: { type: 'string', description: 'One neutral sentence describing why information will be shared.' },\n    counterparty_paper: { type: 'boolean', description: 'True if the other side wants to use their own NDA.' },\n    requested_changes: { type: 'array', items: { type: 'string' }, description: 'Any requested edits to standard terms.' },\n    confidence: { type: 'string', enum: ['high', 'medium', 'low'] },\n  },\n};\n\nreturn $input.all().map((item, i) => {\n  const b = item.json.body || {};\n  const issues = [];\n  const ndaType = clean(b.nda_type).toLowerCase().replace(/[\\s-]/g, '_') || 'mutual';\n  if (!TEMPLATES[ndaType]) issues.push('Unknown nda_type \"' + b.nda_type + '\"');\n  if (!emailOk(b.requester_email)) issues.push('Missing or invalid requester_email');\n  if (!clean(b.counterparty_company)) issues.push('Missing counterparty_company');\n\n  const requestText = [\n    'Company: ' + clean(b.counterparty_company),\n    'Contact: ' + clean(b.contact_name) + ' <' + clean(b.contact_email) + '>',\n    'Purpose: ' + clean(b.purpose),\n    'NDA type: ' + ndaType,\n    'Counterparty paper (form answer): ' + clean(b.counterparty_paper),\n    'Notes from requester: ' + clean(b.notes),\n  ].join('\\n');\n\n  return {\n    json: {\n      request: {\n        requesterEmail: clean(b.requester_email),\n        requesterName: clean(b.requester_name),\n        company: clean(b.counterparty_company),\n        contactName: clean(b.contact_name),\n        contactEmail: clean(b.contact_email),\n        purpose: clean(b.purpose),\n        ndaType,\n        counterpartyPaperForm: /^y(es)?$/i.test(clean(b.counterparty_paper)),\n        templateDocId: TEMPLATES[ndaType] || null,\n      },\n      validationIssues: issues,\n      claudeRequest: {\n        model: 'claude-opus-5-5',\n        max_tokens: 2000,\n        system: 'You extract fields from NDA requests for a legal operations team. Use only facts stated in the request. Never invent a legal entity name, person or email; return null when a value is not stated. Do not draft or assess legal terms.',\n        messages: [{ role: 'user', content: 'Extract the NDA request fields from this submission.\\n\\n<request>\\n' + requestText + '\\n</request>' }],\n        output_config: { format: { type: 'json_schema', schema } },\n      },\n    },\n    pairedItem: { item: i },\n  };\n});\n"
      },
      "id": "78ddd225-fe37-406c-bd0b-30d97ae9c704",
      "name": "Validate request and build prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.anthropic.com/v1/messages",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "anthropicApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.claudeRequest) }}",
        "options": {
          "timeout": 120000
        }
      },
      "id": "aa13ae31-dc6e-49d8-8842-97039419c7a6",
      "name": "Claude: extract NDA fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        300
      ],
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Review rules live here, not in the prompt. Edit to match what counsel approved.\nconst ALLOWED_GOVERNING_LAW = ['Pennsylvania', 'Delaware', 'New York'];\nconst DEFAULT_GOVERNING_LAW = 'Pennsylvania';\nconst DEFAULT_TERM_YEARS = 2;\nconst MAX_TERM_YEARS = 3;\nconst US_NAMES = ['united states', 'usa', 'us', 'u.s.', 'united states of america'];\n\n// Pull the structured JSON out of a Messages API response.\n// Opus 5.5 always thinks, so a (possibly empty) thinking block can come before the text block.\nfunction parseClaude(res) {\n  if (!res || typeof res !== 'object') return { error: 'Empty response from the Messages API' };\n  if (res.stop_reason === 'refusal') {\n    return { error: 'Model declined the request (refusal' + (res.stop_details?.category ? ': ' + res.stop_details.category : '') + ')' };\n  }\n  if (res.stop_reason === 'max_tokens') return { error: 'Response hit max_tokens before the JSON was complete' };\n  const block = (res.content || []).find((b) => b.type === 'text');\n  if (!block) return { error: 'No text block in the response' };\n  try {\n    return { data: JSON.parse(block.text) };\n  } catch (e) {\n    return { error: 'Could not parse model JSON: ' + e.message };\n  }\n}\n\nreturn $input.all().map((item, i) => {\n  const src = $('Validate request and build prompt').itemMatching(i).json;\n  const req = src.request;\n  const reasons = [...src.validationIssues];\n  const { data, error } = parseClaude(item.json);\n  if (error) reasons.push(error);\n  const x = data || {};\n\n  const legalName = x.counterparty_legal_name || null;\n  const signerEmail = x.signer_email || req.contactEmail || null;\n  if (!legalName) reasons.push('Counterparty legal name not stated; ask the requester instead of guessing');\n  if (!signerEmail) reasons.push('No signer email');\n  if (x.confidence === 'low') reasons.push('Model marked the extraction as low confidence');\n  if (x.counterparty_country && !US_NAMES.includes(String(x.counterparty_country).toLowerCase())) {\n    reasons.push('Non-US counterparty (' + x.counterparty_country + ')');\n  }\n  if (x.counterparty_paper || req.counterpartyPaperForm) reasons.push('Counterparty wants to use their own paper');\n  if (Array.isArray(x.requested_changes) && x.requested_changes.length) reasons.push('Requested changes: ' + x.requested_changes.join('; '));\n\n  let governingLaw = DEFAULT_GOVERNING_LAW;\n  if (x.requested_governing_law) {\n    const match = ALLOWED_GOVERNING_LAW.find((s) => s.toLowerCase() === String(x.requested_governing_law).toLowerCase());\n    if (match) governingLaw = match;\n    else reasons.push('Governing law \"' + x.requested_governing_law + '\" is not on the approved list');\n  }\n  let termYears = DEFAULT_TERM_YEARS;\n  if (x.requested_term_years != null) {\n    if (x.requested_term_years > MAX_TERM_YEARS) reasons.push('Requested term of ' + x.requested_term_years + ' years exceeds ' + MAX_TERM_YEARS);\n    else termYears = x.requested_term_years;\n  }\n\n  return {\n    json: {\n      route: reasons.length ? 'review' : 'standard',\n      reasons,\n      requesterEmail: req.requesterEmail,\n      requesterName: req.requesterName,\n      company: req.company,\n      ndaType: req.ndaType,\n      templateDocId: req.templateDocId,\n      counterpartyLegalName: legalName,\n      signerName: x.signer_name || req.contactName || '',\n      signerTitle: x.signer_title || '',\n      signerEmail,\n      purpose: x.purpose_summary || req.purpose,\n      governingLaw,\n      termYears,\n      effectiveDate: $now.toFormat('MMMM d, yyyy'),\n      effectiveDateIso: $now.toISODate(),\n    },\n    pairedItem: { item: i },\n  };\n});\n"
      },
      "id": "e53e4c94-9901-4f6f-b880-1a88efa364d8",
      "name": "Apply review rules",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "4c6fe0f9-2662-4b51-b433-4150959d00f9",
              "leftValue": "={{ $json.route }}",
              "rightValue": "standard",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a23062b9-3615-440d-a65b-211923b30e40",
      "name": "Standard request?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "operation": "copy",
        "fileId": {
          "__rl": true,
          "value": "={{ $json.templateDocId }}",
          "mode": "id"
        },
        "name": "=NDA - {{ $json.counterpartyLegalName }} - {{ $json.effectiveDateIso }}",
        "sameFolder": false,
        "driveId": {
          "__rl": true,
          "value": "My Drive",
          "mode": "list",
          "cachedResultName": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "value": "REPLACE_WITH_NDA_DRAFTS_FOLDER_ID",
          "mode": "id"
        },
        "options": {}
      },
      "id": "829b613d-dacf-4384-b78f-0896cd61a1b6",
      "name": "Copy NDA template",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        1100,
        160
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentURL": "={{ $json.id }}",
        "simple": true,
        "actionsUi": {
          "actionFields": [
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[COUNTERPARTY_LEGAL_NAME]]",
              "replaceText": "={{ $('Apply review rules').item.json.counterpartyLegalName }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[COUNTERPARTY_SIGNER_NAME]]",
              "replaceText": "={{ $('Apply review rules').item.json.signerName }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[COUNTERPARTY_SIGNER_TITLE]]",
              "replaceText": "={{ $('Apply review rules').item.json.signerTitle }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[EFFECTIVE_DATE]]",
              "replaceText": "={{ $('Apply review rules').item.json.effectiveDate }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[PURPOSE]]",
              "replaceText": "={{ $('Apply review rules').item.json.purpose }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[TERM_YEARS]]",
              "replaceText": "={{ $('Apply review rules').item.json.termYears }}",
              "matchCase": true
            },
            {
              "object": "text",
              "action": "replaceAll",
              "text": "[[GOVERNING_LAW]]",
              "replaceText": "={{ $('Apply review rules').item.json.governingLaw }}",
              "matchCase": true
            }
          ]
        }
      },
      "id": "872cc594-4b7f-4313-b566-e3424521c80f",
      "name": "Fill template fields",
      "type": "n8n-nodes-base.googleDocs",
      "typeVersion": 2,
      "position": [
        1320,
        160
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_SLACK_CHANNEL_ID_FOR_REQUESTERS",
          "mode": "id"
        },
        "text": "=:page_facing_up: NDA draft ready for *{{ $('Apply review rules').item.json.counterpartyLegalName }}* ({{ $('Apply review rules').item.json.ndaType }})\nRequested by {{ $('Apply review rules').item.json.requesterEmail }}\nSigner: {{ $('Apply review rules').item.json.signerName }} <{{ $('Apply review rules').item.json.signerEmail }}>\nTerm {{ $('Apply review rules').item.json.termYears }} years, governing law {{ $('Apply review rules').item.json.governingLaw }}\nDoc: https://docs.google.com/document/d/{{ $('Copy NDA template').item.json.id }}/edit\nNext step: send for e-signature.",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "3bba91ad-486f-458f-a47a-0362c4bb7f8f",
      "name": "Slack: draft ready",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1540,
        160
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "REPLACE_WITH_SLACK_CHANNEL_ID_FOR_LEGAL",
          "mode": "id"
        },
        "text": "=:warning: NDA request needs legal review\nCompany: {{ $json.company ?? $('Validate request and build prompt').item.json.request.company }}\nRequester: {{ $json.requesterEmail ?? $('Validate request and build prompt').item.json.request.requesterEmail }}\nWhy: {{ $json.reasons ? $json.reasons.join(' | ') : 'Claude API call failed: ' + ($json.error?.message ?? 'unknown error') }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "cee42569-2706-4c55-8cd1-5f0fdf7d45e9",
      "name": "Slack: legal review needed",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1100,
        460
      ]
    }
  ],
  "connections": {
    "NDA request form": {
      "main": [
        [
          {
            "node": "Validate request and build prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate request and build prompt": {
      "main": [
        [
          {
            "node": "Claude: extract NDA fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude: extract NDA fields": {
      "main": [
        [
          {
            "node": "Apply review rules",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: legal review needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply review rules": {
      "main": [
        [
          {
            "node": "Standard request?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Standard request?": {
      "main": [
        [
          {
            "node": "Copy NDA template",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack: legal review needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Copy NDA template": {
      "main": [
        [
          {
            "node": "Fill template fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fill template fields": {
      "main": [
        [
          {
            "node": "Slack: draft ready",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
