{
  "name": "Turn New Client Intake into a Project Handoff | Uptune",
  "nodes": [
    {
      "parameters": {
        "content": "## Turn New Client Intake into a Project Handoff\nTurn an approved deal into a delivery record, kickoff checklist, and welcome draft.\n\nRun **Run with sample data** first. **Setup** defaults to dry run and uses the sample fixture without contacting connected apps. Configure the accounts and destinations in README.md before switching modes.",
        "width": 450,
        "height": 235
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -390
      ],
      "id": "0fdadda9-19bb-5c12-a348-c58508250145",
      "name": "Start here"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -720,
        80
      ],
      "id": "3c970de6-f350-558c-b88f-2023e5227b2a",
      "name": "Run with sample data"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {sampleRun: true, body: {\"handoffId\": \"DEAL-1042\", \"clientName\": \"Avery Morgan\", \"clientEmail\": \"avery@example.com\", \"company\": \"Northstar Studio\", \"service\": \"CRM setup\", \"scope\": \"Map the existing sales pipeline and import the agreed contact fields.\", \"kickoffDate\": \"2027-01-12\", \"ownerEmail\": \"delivery@example.com\"}}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -480,
        80
      ],
      "id": "39fa6e66-db59-58cd-8e87-9330580da2ab",
      "name": "Sample input"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "uptune-client-onboarding-handoff",
        "authentication": "headerAuth",
        "responseMode": "onReceived",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -480,
        300
      ],
      "id": "1eb5b167-06ca-5301-a0f0-f7bc97a69626",
      "name": "Receive event",
      "webhookId": "1eb5b167-06ca-5301-a0f0-f7bc97a69626"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst config = {\n  \"dryRun\": true,\n  \"spreadsheetId\": \"\",\n  \"fallbackOwnerEmail\": \"\",\n  \"createWelcomeDraft\": false\n};\nreturn [{json: {...input, config}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        150
      ],
      "id": "c6e3cb62-4d4b-58de-ab7f-b41abf9f5bf6",
      "name": "Setup"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst body = input.body || {};\nconst get = (key, max = 180) => {\n  const value = typeof body[key] === 'string' ? body[key].trim() : '';\n  if (value.length > max) throw new Error(key + ' is too long.');\n  return value;\n};\nconst handoffId = get('handoffId', 121);\nconst clientName = get('clientName');\nconst clientEmail = get('clientEmail').toLowerCase();\nconst company = get('company');\nconst service = get('service');\nconst scope = get('scope', 2000);\nconst ownerEmail = (get('ownerEmail') || input.config.fallbackOwnerEmail || '').toLowerCase();\nconst kickoffDate = get('kickoffDate', 11);\nconst parsedDate = /^\\d{4}-\\d{2}-\\d{2}$/.test(kickoffDate) ? new Date(kickoffDate + 'T00:00:00Z') : new Date(NaN);\nconst validDate = !isNaN(parsedDate) && parsedDate.toISOString().slice(0,10) === kickoffDate;\nconst validEmail = /^[^\\s@<>,\";]+@[^\\s@<>,\";]+\\.[^\\s@<>,\";]+$/;\nif (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$/.test(handoffId)) throw new Error('Supply a stable handoffId from the signed deal.');\nconst issues = [];\nif (!clientName || !validEmail.test(clientEmail)) issues.push('Verify the client name and email');\nif (!company || !service || !scope || /[\\r\\n]/.test(service)) issues.push('Confirm the company, service, and agreed scope');\nif (!validEmail.test(ownerEmail)) issues.push('Assign a delivery owner');\nif (!validDate) issues.push('Confirm a valid kickoff date');\nconst tasks = ['Review signed scope', 'Confirm access and assets', 'Confirm kickoff with the client', 'Share the first delivery milestone'];\nconst subject = 'Getting started with ' + (service || 'your project');\nconst message = ['Hi ' + (clientName.split(/\\s+/)[0] || 'there') + ',', '', 'Thanks for getting started with us. Your delivery contact is ' + ownerEmail + '.', 'We have ' + kickoffDate + ' as the planned kickoff date. Please let us know if it needs to change.', '', 'Best,', 'The delivery team'].join('\\n');\nconst preview = input.config.dryRun !== false || input.sampleRun === true;\nif (!preview && !input.config.spreadsheetId) throw new Error('Enter the Google spreadsheet ID in Setup.');\nreturn [{json: {preview, handoffId, clientName, clientEmail, company, service, scope, ownerEmail,\n  kickoffDate: validDate ? kickoffDate : '', status: issues.length ? 'needs_review' : 'ready', issues: issues.join(' | '),\n  checklist: tasks.join('\\n'), subject, message, email: clientEmail}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -75,
        150
      ],
      "id": "73e618ab-bed5-5b29-99c0-f1bff3c11177",
      "name": "Build handoff"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "85982ad9-e703-52a0-8f31-e7bab5b1a537",
              "leftValue": "={{ !$json.preview }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        160,
        150
      ],
      "id": "207a7301-de19-5443-9761-aeb6f407309e",
      "name": "Write live records?"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{json: {...input, outcome: \"preview\"}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -65,
        -105
      ],
      "id": "d9085358-2335-5806-9f2f-3efeec042d30",
      "name": "Handoff preview"
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Handoffs",
          "mode": "name"
        },
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "Handoff ID",
              "lookupValue": "={{ $json.handoffId }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        630,
        150
      ],
      "id": "b6a7b298-51ee-5fc6-b6d0-bd120dca6ec9",
      "name": "Find existing handoff",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst handoff = $(\"Build handoff\").first().json;\nconst rows = $input.all().map(item => item.json).filter(row => row['Handoff ID']);\nif (rows.length > 1) throw new Error('More than one row uses this handoff ID. Resolve the duplicates before rerunning.');\nconst existing = rows[0];\nconst draftId = existing?.['Draft ID'] || '';\nif (existing?.Status === 'draft_pending') throw new Error('Check Gmail for an existing draft before clearing draft_pending.');\nif (existing?.['Client Email'] && handoff.clientEmail && existing['Client Email'] !== handoff.clientEmail) throw new Error('This handoff ID already belongs to a different email address.');\nreturn [{json: {...handoff, draftId, shouldDraft: handoff.status === 'ready' && !draftId && $(\"Setup\").first().json.config.createWelcomeDraft === true}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        865,
        150
      ],
      "id": "6f32fe02-5329-51be-b7f3-f4a98ef1acd1",
      "name": "Check handoff state"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Handoffs",
          "mode": "name"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Handoff ID": "={{ $(\"Check handoff state\").first().json.handoffId }}",
            "Client Name": "={{ $(\"Check handoff state\").first().json.clientName }}",
            "Client Email": "={{ $(\"Check handoff state\").first().json.clientEmail }}",
            "Company": "={{ $(\"Check handoff state\").first().json.company }}",
            "Service": "={{ $(\"Check handoff state\").first().json.service }}",
            "Agreed Scope": "={{ $(\"Check handoff state\").first().json.scope }}",
            "Delivery Owner": "={{ $(\"Check handoff state\").first().json.ownerEmail }}",
            "Kickoff Date": "={{ $(\"Check handoff state\").first().json.kickoffDate }}",
            "Status": "={{ $(\"Check handoff state\").first().json.status }}",
            "Review Reasons": "={{ $(\"Check handoff state\").first().json.issues }}",
            "Welcome Draft": "={{ $(\"Check handoff state\").first().json.message }}",
            "Draft ID": "={{ $(\"Check handoff state\").first().json.draftId }}"
          },
          "matchingColumns": [
            "Handoff ID"
          ],
          "schema": [
            {
              "id": "Handoff ID",
              "displayName": "Handoff ID",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Client Name",
              "displayName": "Client Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Client Email",
              "displayName": "Client Email",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Company",
              "displayName": "Company",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Service",
              "displayName": "Service",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Agreed Scope",
              "displayName": "Agreed Scope",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Delivery Owner",
              "displayName": "Delivery Owner",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Kickoff Date",
              "displayName": "Kickoff Date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Review Reasons",
              "displayName": "Review Reasons",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Welcome Draft",
              "displayName": "Welcome Draft",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft ID",
              "displayName": "Draft ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1100,
        150
      ],
      "id": "156b7027-0ecd-597a-a508-3486ad3fa58d",
      "name": "Save handoff"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Checklist",
          "mode": "name"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Handoff ID": "={{ $(\"Check handoff state\").first().json.handoffId }}",
            "Delivery Owner": "={{ $(\"Check handoff state\").first().json.ownerEmail }}",
            "Kickoff Date": "={{ $(\"Check handoff state\").first().json.kickoffDate }}",
            "Checklist": "={{ $(\"Check handoff state\").first().json.checklist }}"
          },
          "matchingColumns": [
            "Handoff ID"
          ],
          "schema": [
            {
              "id": "Handoff ID",
              "displayName": "Handoff ID",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Delivery Owner",
              "displayName": "Delivery Owner",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Kickoff Date",
              "displayName": "Kickoff Date",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Checklist",
              "displayName": "Checklist",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1335,
        150
      ],
      "id": "e283aa80-5658-56ae-beda-866fe63db514",
      "name": "Save kickoff checklist"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "f13ec870-b42a-5e0f-ac63-dd156040d87d",
              "leftValue": "={{ $(\"Check handoff state\").first().json.shouldDraft === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1570,
        150
      ],
      "id": "0b2b51ce-1967-5c3e-9adf-83eed2a0a55d",
      "name": "Create welcome draft?"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Handoffs",
          "mode": "name"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Handoff ID": "={{ $(\"Check handoff state\").first().json.handoffId }}",
            "Status": "={{ \"draft_pending\" }}"
          },
          "matchingColumns": [
            "Handoff ID"
          ],
          "schema": [
            {
              "id": "Handoff ID",
              "displayName": "Handoff ID",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1805,
        150
      ],
      "id": "63cc5a69-8b4c-5d3a-84f7-e7b2473a7105",
      "name": "Reserve draft creation"
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $(\"Check handoff state\").first().json.subject }}",
        "message": "={{ $(\"Check handoff state\").first().json.message }}",
        "options": {
          "sendTo": "={{ $(\"Check handoff state\").first().json.email }}"
        }
      },
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2040,
        150
      ],
      "id": "cc4e05d2-bf5a-5a12-944f-6fed37fd56be",
      "name": "Create welcome draft"
    },
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nif (!input.id) throw new Error(\"Gmail did not return a draft ID. Check Gmail before retrying.\"); return [{json: input}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2275,
        150
      ],
      "id": "5df328e8-71d8-5c63-937f-5322b79fdfd8",
      "name": "Verify Gmail draft"
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $(\"Setup\").first().json.config.spreadsheetId }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Handoffs",
          "mode": "name"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Handoff ID": "={{ $(\"Check handoff state\").first().json.handoffId }}",
            "Draft ID": "={{ $json.id }}",
            "Status": "={{ \"ready\" }}"
          },
          "matchingColumns": [
            "Handoff ID"
          ],
          "schema": [
            {
              "id": "Handoff ID",
              "displayName": "Handoff ID",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Draft ID",
              "displayName": "Draft ID",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "displayName": "Status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2510,
        150
      ],
      "id": "c419ed2e-4019-5d05-ae8e-7fa688f4e3c1",
      "name": "Record draft ID"
    }
  ],
  "connections": {
    "Run with sample data": {
      "main": [
        [
          {
            "node": "Sample input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample input": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive event": {
      "main": [
        [
          {
            "node": "Setup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup": {
      "main": [
        [
          {
            "node": "Build handoff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build handoff": {
      "main": [
        [
          {
            "node": "Write live records?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write live records?": {
      "main": [
        [
          {
            "node": "Find existing handoff",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handoff preview",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find existing handoff": {
      "main": [
        [
          {
            "node": "Check handoff state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check handoff state": {
      "main": [
        [
          {
            "node": "Save handoff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save handoff": {
      "main": [
        [
          {
            "node": "Save kickoff checklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save kickoff checklist": {
      "main": [
        [
          {
            "node": "Create welcome draft?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create welcome draft?": {
      "main": [
        [
          {
            "node": "Reserve draft creation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reserve draft creation": {
      "main": [
        [
          {
            "node": "Create welcome draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create welcome draft": {
      "main": [
        [
          {
            "node": "Verify Gmail draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Gmail draft": {
      "main": [
        [
          {
            "node": "Record draft ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "all"
  },
  "pinData": {},
  "tags": []
}
