Skip to workflow content
Vora
← Workflow library

Migration Operations

Normalize contact rows and quarantine bad mappings

Check a contact import mapping before writing anything to a CRM. Produce normalized draft rows and a row-numbered quarantine list without carrying unrelated source columns into the result.

Experimental native example · local preview · revision 1

WORKFLOW PREVIEW

See how it works

2 nodes · 1 connections

Use + and − to zoom, F to fit. Press Enter on a node to inspect it.

Private preview. These tested drafts are available for inspection. Adding them to a Vora workspace is still being tested.

What you need

  • An authorized source export converted into rows of column/value string cells.
  • Three distinct mapping columns for externalId, name and email, and an operator responsible for resolving quarantined rows.
Follow the workflow step by step
  1. Supplied agency data

    Receive the explicitly supplied manual input packet.

  2. Normalize contact rows and quarantine bad mappings

    Map operator-supplied column keys to normalized contact drafts and return row-indexed quarantine reasons. No import or CRM write occurs.

Setup and review

  • Supply the exact column labels in mapping. Column labels are trimmed; the declared mapping must stay unambiguous.
  • Test a valid row, a malformed address and two rows with the same external ID.
  • Inspect drafts and quarantine. Importing the drafts requires a separately tested destination adapter and idempotency policy.

Inspect every node

Expand a node to inspect its complete settings. Review the runtime and adoption evidence before installing it.

Supplied agency data · wfManualTrigger
{
  "label": "Supplied agency data"
}
Normalize contact rows and quarantine bad mappings · code
{
  "label": "Normalize contact rows and quarantine bad mappings",
  "code": "\nfunction obj(value, keys, required = keys) {\n  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Expected object');\n  if (Object.keys(value).some(key => !keys.includes(key))) throw new Error('Unknown input field');\n  if (required.some(key => !Object.hasOwn(value, key))) throw new Error('Missing required input field');\n  return value;\n}\nfunction text(value, name, max = 200, allowEmpty = false) {\n  if (typeof value !== 'string' || value.length > max || (!allowEmpty && !value.trim())) throw new Error('Invalid ' + name);\n  return value.trim();\n}\nfunction list(value, name, max = 500) {\n  if (!Array.isArray(value) || value.length > max) throw new Error('Invalid ' + name);\n  return value;\n}\nfunction int(value, name, min = 0, max = Number.MAX_SAFE_INTEGER) {\n  if (!Number.isSafeInteger(value) || value < min || value > max) throw new Error('Invalid ' + name);\n  return value;\n}\nfunction amount(value, name) {\n  if (!Number.isFinite(value) || value < 0 || value > 1000000000) throw new Error('Invalid ' + name);\n  return value;\n}\nfunction email(value) { return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value); }\nfunction day(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) throw new Error('Invalid calendar date');\n  const ms = Date.parse(value + 'T00:00:00Z');\n  if (!Number.isFinite(ms) || new Date(ms).toISOString().slice(0, 10) !== value) throw new Error('Invalid calendar date');\n  return ms;\n}\nfunction instant(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z$/.test(value)) throw new Error('Invalid UTC timestamp');\n  const ms = Date.parse(value);\n  const normalized = value.includes('.') ? value : value.replace('Z', '.000Z');\n  if (!Number.isFinite(ms) || new Date(ms).toISOString() !== normalized) throw new Error('Invalid UTC timestamp');\n  return ms;\n}\nfunction uniqueIds(items) {\n  const ids = items.map(item => text(item.id, 'item ID', 120));\n  if (new Set(ids).size !== ids.length) throw new Error('Duplicate item ID');\n  return ids;\n}\nfunction ratioBps(numerator, denominator) {\n  const n = BigInt(numerator) * 10000n;\n  const d = BigInt(denominator);\n  const quotient = n / d;\n  const floor = n < 0n && n % d !== 0n ? quotient - 1n : quotient;\n  const result = Number(floor);\n  if (!Number.isSafeInteger(result)) throw new Error('Unsafe calculated ratio');\n  return result;\n}\n\n\nconst x = obj(context.input, ['importId','mapping','rows']);\nconst importId = text(x.importId, 'import ID', 120), mapping = obj(x.mapping, ['externalId','name','email']);\nconst keys = ['externalId','name','email'];\nconst columns = keys.map(key => text(mapping[key], 'mapping column', 120));\nif (new Set(columns).size !== columns.length) throw new Error('Overlapping mapping keys');\nconst rows = list(x.rows, 'rows');\nconst parsed = rows.map((row,index) => {\n  obj(row, ['cells']);\n  const cells = list(row.cells, 'cells', 50), values = new Map(), reasons = [];\n  cells.forEach(cell => {\n    obj(cell, ['column','value']);\n    const column = text(cell.column, 'column', 120), value = text(cell.value, 'cell value', 4000, true);\n    if (!columns.includes(column)) return;\n    if (values.has(column)) { if (!reasons.includes('ambiguous_mapped_column')) reasons.push('ambiguous_mapped_column'); }\n    else values.set(column, value);\n  });\n  const externalId = values.get(columns[0]) || '', name = values.get(columns[1]) || '', address = (values.get(columns[2]) || '').toLowerCase();\n  if (!externalId || externalId.length > 120) reasons.push('invalid_external_id');\n  if (!name || name.length > 200) reasons.push('invalid_name');\n  if (!email(address) || address.length > 320) reasons.push('invalid_email');\n  return {row:index + 1, externalId, name, address, reasons};\n});\nconst counts = new Map();\nparsed.forEach(row => { if (row.externalId) counts.set(row.externalId, (counts.get(row.externalId) || 0) + 1); });\nconst drafts = [], quarantine = [];\nparsed.forEach(row => {\n  if (counts.get(row.externalId) > 1) row.reasons.push('duplicate_external_id');\n  if (row.reasons.length) quarantine.push({row:row.row, reasons:row.reasons});\n  else drafts.push({externalId:row.externalId, name:row.name, email:row.address});\n});\nreturn {importId, drafts, quarantine};\n",
  "outputVar": "result",
  "timeout": 1000,
  "memoryLimit": 16
}
Input and output contracts

Input

{
  "type": "object",
  "properties": {
    "importId": {
      "type": "string",
      "maxLength": 120
    },
    "mapping": {
      "type": "object",
      "properties": {
        "externalId": {
          "type": "string",
          "maxLength": 120
        },
        "name": {
          "type": "string",
          "maxLength": 120
        },
        "email": {
          "type": "string",
          "maxLength": 120
        }
      },
      "required": [
        "externalId",
        "name",
        "email"
      ],
      "additionalProperties": false
    },
    "rows": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "cells": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "column": {
                  "type": "string",
                  "maxLength": 120
                },
                "value": {
                  "type": "string",
                  "maxLength": 4000
                }
              },
              "required": [
                "column",
                "value"
              ],
              "additionalProperties": false
            },
            "maxItems": 50
          }
        },
        "required": [
          "cells"
        ],
        "additionalProperties": false
      },
      "maxItems": 500
    }
  },
  "required": [
    "importId",
    "mapping",
    "rows"
  ],
  "additionalProperties": false
}

Output

{
  "type": "object",
  "properties": {
    "importId": {
      "type": "string",
      "maxLength": 120
    },
    "drafts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "externalId": {
            "type": "string",
            "maxLength": 120
          },
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "maxLength": 320
          }
        },
        "required": [
          "externalId",
          "name",
          "email"
        ],
        "additionalProperties": false
      },
      "maxItems": 500
    },
    "quarantine": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "row": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 200
            },
            "maxItems": 6
          }
        },
        "required": [
          "row",
          "reasons"
        ],
        "additionalProperties": false
      },
      "maxItems": 500
    }
  },
  "required": [
    "importId",
    "drafts",
    "quarantine"
  ],
  "additionalProperties": false
}

Choose an output for this job

This business workflow also has 15 delivery formats. Compare their inputs, output examples and limits.

Compare implementations for this job

Example input and result

These examples use synthetic data.

Input

{
  "importId": "import-01",
  "mapping": {
    "externalId": "Contact ID",
    "name": "Full Name",
    "email": "Work Email"
  },
  "rows": [
    {
      "cells": [
        {
          "column": "Contact ID",
          "value": " 001 "
        },
        {
          "column": "Full Name",
          "value": " Ada Agency "
        },
        {
          "column": "Work Email",
          "value": " ADA@Example.test "
        },
        {
          "column": "Unneeded",
          "value": "drop"
        }
      ]
    }
  ]
}

Expected result

{
  "importId": "import-01",
  "drafts": [
    {
      "externalId": "001",
      "name": "Ada Agency",
      "email": "ada@example.test"
    }
  ],
  "quarantine": []
}

Success means: Map operator-supplied column keys to normalized contact drafts and return row-indexed quarantine reasons. No import or CRM write occurs.

When something goes wrong

  • Colliding mapping columns stop the batch. Missing/ambiguous mapped cells, blank required values, bad addresses or repeated external IDs quarantine rows. Correct the source cells and repeat; duplicate IDs quarantine every matching row.

Limits and costs

  • Input is structured JSON, not a raw CSV file. At most 500 rows and 50 cells per row are accepted. Unknown columns are omitted from output; this packet does not perform a live import, fuzzy identity resolution or rollback.
  • No paid model calls or metered third-party operations are declared in this pure graph. Vora plan availability, compute and quotas still apply; pricing has not been verified by this batch.

USE IN VORA

Normalize contact rows and quarantine bad mappings

Review this workflow’s steps and required inputs. Get access to Vora before setting it up in your workspace.

Workspace installation is being connected. This page lets you inspect the workflow; it does not create or run one.

Required input fields
{
  "additionalProperties": false,
  "properties": {
    "importId": {
      "maxLength": 120,
      "type": "string"
    },
    "mapping": {
      "additionalProperties": false,
      "properties": {
        "email": {
          "maxLength": 120,
          "type": "string"
        },
        "externalId": {
          "maxLength": 120,
          "type": "string"
        },
        "name": {
          "maxLength": 120,
          "type": "string"
        }
      },
      "required": [
        "externalId",
        "name",
        "email"
      ],
      "type": "object"
    },
    "rows": {
      "items": {
        "additionalProperties": false,
        "properties": {
          "cells": {
            "items": {
              "additionalProperties": false,
              "properties": {
                "column": {
                  "maxLength": 120,
                  "type": "string"
                },
                "value": {
                  "maxLength": 4000,
                  "type": "string"
                }
              },
              "required": [
                "column",
                "value"
              ],
              "type": "object"
            },
            "maxItems": 50,
            "type": "array"
          }
        },
        "required": [
          "cells"
        ],
        "type": "object"
      },
      "maxItems": 500,
      "type": "array"
    }
  },
  "required": [
    "importId",
    "mapping",
    "rows"
  ],
  "type": "object"
}