{
  "name": "UndoGate Private Beta — Protected HubSpot Contact Property Update v0.2.1 (Generic)",
  "nodes": [
    {
      "parameters": {
        "inputSource": "workflowInputs",
        "workflowInputs": {
          "values": [
            {
              "name": "contact_id",
              "type": "string"
            },
            {
              "name": "property",
              "type": "string"
            },
            {
              "name": "new_value",
              "type": "string"
            },
            {
              "name": "executor_id",
              "type": "string"
            }
          ]
        }
      },
      "id": "2eceebe6-59d8-4510-8956-12e2734cbeac",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        -980,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst contactId = String($json.contact_id ?? '').trim();\nconst property = String($json.property ?? '').trim();\nconst newValue = String($json.new_value ?? '').trim();\nconst executorId = String($json.executor_id ?? '').trim();\nconst immutable = new Set(['hs_object_id','createdate','lastmodifieddate','hs_createdate','hs_lastmodifieddate','archived','hs_merged_object_ids','hs_all_owner_ids','hs_email_domain']);\n\nconst errors = [];\nif (!/^\\d+$/.test(contactId)) errors.push('contact_id must be a numeric HubSpot contact ID');\nif (!/^[a-z][a-z0-9_]{0,99}$/.test(property)) errors.push('property must be a HubSpot internal property name (lowercase letters, numbers, underscores)');\nif (immutable.has(property)) errors.push('property is an immutable HubSpot system property and is not supported');\nif (!newValue) errors.push('new_value is required and cannot be blank');\nif (!executorId) errors.push('executor_id is required');\nif (errors.length) throw new Error(errors.join('; '));\n\nreturn {json:{contact_id:contactId, property, new_value:newValue, executor_id:executorId}};\n"
      },
      "id": "82d67732-fbbd-41e0-91c7-b67a0ed0ad5c",
      "name": "Validate Protected Update Input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -760,
        0
      ]
    },
    {
      "parameters": {
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ encodeURIComponent($('Validate Protected Update Input').first().json.contact_id) }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "properties",
              "value": "={{ $('Validate Protected Update Input').first().json.property }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        -500,
        0
      ],
      "id": "0c831443-3c28-4321-8671-d0cc29b46ef6",
      "name": "Local HubSpot Read Before",
      "credentials": {
        "hubspotOAuth2Api": {
          "id": "",
          "name": "Select your HubSpot credential"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst input = $('Validate Protected Update Input').first().json;\nconst previousValue = $json.properties?.[input.property] == null ? '' : String($json.properties[input.property]);\nconst before = {properties:{[input.property]:previousValue}};\nconst after = {properties:{[input.property]:input.new_value}};\nreturn {json:{\n  agent_id:'protected_hubspot_contact_property_update_v0_2',\n  app:'hubspot', action_type:'update', resource_type:'contact',\n  resource_id:input.contact_id,\n  resource_locator:{contact_id:input.contact_id},\n  before_state:before,\n  after_state:after,\n  executor_id:input.executor_id,\n  undo_ttl_minutes:30,\n  reversibility_type:'field_state_restoration',\n  changed_fields:[input.property],\n}};\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -240,
        0
      ],
      "id": "37bbbf22-c568-4b68-a606-5c329af75cce",
      "name": "Build Local Prepare Payload"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://undogate.app.n8n.cloud/webhook/undogate/v1/local/actions/prepare",
        "options": {},
        "authentication": "genericCredentialType",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {agent_id:$('Build Local Prepare Payload').first().json.agent_id,app:'hubspot',action_type:'update',resource_type:'contact',resource_id:$('Build Local Prepare Payload').first().json.resource_id,resource_locator:$('Build Local Prepare Payload').first().json.resource_locator,before_state:$('Build Local Prepare Payload').first().json.before_state,after_state:$('Build Local Prepare Payload').first().json.after_state,changed_fields:$('Build Local Prepare Payload').first().json.changed_fields,executor_id:$('Build Local Prepare Payload').first().json.executor_id,undo_delivery:'signed_push',undo_ttl_minutes:30,reversibility_type:'field_state_restoration'} }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        20,
        0
      ],
      "id": "a215c3e8-0170-4d1c-b3e9-a19546034fc7",
      "name": "UndoGate Prepare Local Action",
      "credentials": {
        "httpHeaderAuth": {
          "id": "",
          "name": "UndoGate Workspace API Key (Header Auth)"
        }
      }
    },
    {
      "id": "aebbffa7-7f32-42ab-ac26-e11e51257a79",
      "name": "Build Dynamic HubSpot Apply Body",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        280,
        0
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const input = $('Validate Protected Update Input').first().json;\nreturn {json:{properties:{[input.property]:input.new_value}}};"
      }
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ encodeURIComponent($('Validate Protected Update Input').first().json.contact_id) }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        540,
        0
      ],
      "id": "13c11222-fc6f-4131-bc5f-0c2bf9a364b0",
      "name": "Local HubSpot Apply After",
      "credentials": {
        "hubspotOAuth2Api": {
          "id": "",
          "name": "Select your HubSpot credential"
        }
      }
    },
    {
      "parameters": {
        "url": "=https://api.hubapi.com/crm/v3/objects/contacts/{{ encodeURIComponent($('Validate Protected Update Input').first().json.contact_id) }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "properties",
              "value": "={{ $('Validate Protected Update Input').first().json.property }}"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        800,
        0
      ],
      "id": "be0a6083-c7ce-4a2c-b1c4-ba1bbc7be40a",
      "name": "Local HubSpot Read Applied",
      "credentials": {
        "hubspotOAuth2Api": {
          "id": "",
          "name": "Select your HubSpot credential"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst input = $('Validate Protected Update Input').first().json;\nconst expected = input.new_value;\nconst actual = String($json.properties?.[input.property] ?? '');\nreturn {json:{\n  verified:actual===expected,\n  verification_status:actual===expected?'applied_verified':'failed',\n  current_state:{properties:{[input.property]:actual}},\n  expected_state:{properties:{[input.property]:expected}},\n}};\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1060,
        0
      ],
      "id": "5c25f992-f839-4847-8f08-cf8525361d70",
      "name": "Verify Local Applied State"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "2cfea41c-a516-4b22-aa56-624bf758b549",
              "leftValue": "={{ $json.verified }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1300,
        0
      ],
      "id": "a0ec4901-256e-46fc-964d-b1ad01ecb14a",
      "name": "Applied Verified?"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://undogate.app.n8n.cloud/webhook/undogate/v1/local/actions/finalize",
        "options": {},
        "authentication": "genericCredentialType",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ {action_id:$('UndoGate Prepare Local Action').first().json.action_id,verified:true,verification_status:'applied_verified'} }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1540,
        -100
      ],
      "id": "dba824bc-ec49-42f1-8ad5-36daea5dcf22",
      "name": "UndoGate Finalize Local Action",
      "credentials": {
        "httpHeaderAuth": {
          "id": "",
          "name": "UndoGate Workspace API Key (Header Auth)"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 1000
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst prepared = $('UndoGate Prepare Local Action').first().json;\nconst verified = $('Verify Local Applied State').first().json;\nconst finalized = $json;\nreturn {json:{\n  success:true,\n  action_id:prepared.action_id,\n  verification_status:verified.verification_status,\n  undo_status:finalized.undo_status ?? 'available',\n  undo_available_until:prepared.undo_expires_at ?? null,\n  executor_id:prepared.executor_id,\n  credential_boundary:'HubSpot credential remains in this customer-owned n8n workflow',\n}};\n"
      },
      "id": "4e430c40-4725-4336-92dd-a283ebb18804",
      "name": "Protected Update Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1800,
        -100
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "\nconst prepared = $('UndoGate Prepare Local Action').first().json;\nconst verified = $('Verify Local Applied State').first().json;\nreturn {json:{\n  success:false,\n  action_id:prepared.action_id,\n  verification_status:verified.verification_status,\n  undo_status:'pending',\n  undo_available_until:prepared.undo_expires_at ?? null,\n  error:'HubSpot write was not read-back verified. Undo is not advertised; this action needs attention.',\n  credential_boundary:'HubSpot credential remains in this customer-owned n8n workflow',\n}};\n"
      },
      "id": "3a9201a5-2ab2-4ad7-9a24-ba2b10f6bf59",
      "name": "Protected Update Unverified Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        140
      ]
    }
  ],
  "connections": {
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Validate Protected Update Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Protected Update Input": {
      "main": [
        [
          {
            "node": "Local HubSpot Read Before",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Local HubSpot Read Before": {
      "main": [
        [
          {
            "node": "Build Local Prepare Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Local Prepare Payload": {
      "main": [
        [
          {
            "node": "UndoGate Prepare Local Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UndoGate Prepare Local Action": {
      "main": [
        [
          {
            "node": "Build Dynamic HubSpot Apply Body",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Dynamic HubSpot Apply Body": {
      "main": [
        [
          {
            "node": "Local HubSpot Apply After",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Local HubSpot Apply After": {
      "main": [
        [
          {
            "node": "Local HubSpot Read Applied",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Local HubSpot Read Applied": {
      "main": [
        [
          {
            "node": "Verify Local Applied State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Local Applied State": {
      "main": [
        [
          {
            "node": "Applied Verified?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Applied Verified?": {
      "main": [
        [
          {
            "node": "UndoGate Finalize Local Action",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Protected Update Unverified Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "UndoGate Finalize Local Action": {
      "main": [
        [
          {
            "node": "Protected Update Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
