LoopFour

Transform Steps

Map, normalize, and validate data in workflows

Transform Steps

Transform steps manipulate and reshape data as it flows through your workflow. They support field mapping, data normalization, validation, and enrichment operations.

Configuration

{
  "id": "transform-data",
  "type": "transform",
  "name": "Transform Customer Data",
  "config": {
    "action": "transform",
    "sourceData": "{{input}}",
    "targetSchema": {
      "type": "object",
      "properties": {
        "fullName": { "type": "string" },
        "email": { "type": "string" }
      }
    },
    "mappingRules": [
      {
        "source": "first_name",
        "target": "fullName",
        "transform": "capitalize"
      }
    ]
  }
}
FieldTypeRequiredDescription
idstringYesUnique step identifier
typestringYesMust be "transform"
namestringYesHuman-readable name
config.actionstringNoTransform action (default: transform)
config.sourceDatastringYesTemplate expression for source data
config.targetSchemaobjectNoJSON Schema for target structure
config.mappingRulesarrayNoField mapping rules
config.normalizationsarrayNoData normalization rules
config.validationRulesarrayNoValidation rules
config.strictbooleanNoFail on any transformation error

Transform Actions

ActionDescription
transformFull transformation with mapping and normalization
mapSimple field-to-field mapping
normalizeStandardize data formats
validateValidate data against schema
enrichAdd computed or external data

Transform Types

Transforms can be categorized by type:

TypeDescription
mappingField-to-field transformations
normalizationData standardization
enrichmentAdding external or computed data
aggregationCombining multiple values
customCustom transformation logic

Mapping Rules

Mapping rules define how fields are transformed from source to target:

{
  "config": {
    "action": "map",
    "sourceData": "{{input}}",
    "mappingRules": [
      {
        "source": "customer.firstName",
        "target": "name.first",
        "transform": "capitalize"
      },
      {
        "source": "customer.lastName",
        "target": "name.last",
        "transform": "uppercase"
      },
      {
        "source": "customer.emailAddress",
        "target": "email",
        "transform": "lowercase",
        "required": true
      }
    ]
  }
}

Mapping Rule Fields

FieldTypeRequiredDescription
idstringNoUnique rule identifier
sourcestringYesJSONPath or dot notation to source field
targetstringYesJSONPath or dot notation to target field
transformstringNoTransformation operation (default: direct)
customTransformstringNoNatural language transformation description
defaultanyNoDefault value if source is missing
requiredbooleanNoWhether source field is required
conditionstringNoCondition for applying this rule

Field Transforms

TransformDescriptionExample
directCopy value as-is"John""John"
uppercaseConvert to uppercase"John""JOHN"
lowercaseConvert to lowercase"John""john"
trimRemove whitespace" John ""John"
capitalizeCapitalize first letter"john""John"
titlecaseTitle Case Each Word"john doe""John Doe"
snake_caseConvert to snake_case"firstName""first_name"
camelCaseConvert to camelCase"first_name""firstName"
numberParse as number"123"123
stringConvert to string123"123"
booleanParse as boolean"true"true
arrayWrap in array"value"["value"]
flattenFlatten nested array[[1], [2]][1, 2]
customCustom transformationUse with customTransform

Conditional Mapping

Apply mappings conditionally:

{
  "mappingRules": [
    {
      "source": "amount",
      "target": "total",
      "transform": "number",
      "condition": "{{input.currency}} === 'USD'"
    },
    {
      "source": "amount_eur",
      "target": "total",
      "transform": "number",
      "condition": "{{input.currency}} === 'EUR'"
    }
  ]
}

Default Values

Provide fallback values for missing fields:

{
  "mappingRules": [
    {
      "source": "customer.phone",
      "target": "contactPhone",
      "default": "Not provided"
    },
    {
      "source": "preferences.language",
      "target": "locale",
      "default": "en-US"
    }
  ]
}

Normalization Rules

Normalization rules standardize data formats:

{
  "config": {
    "action": "normalize",
    "sourceData": "{{input}}",
    "normalizations": [
      {
        "field": "created_date",
        "type": "date",
        "format": "YYYY-MM-DD"
      },
      {
        "field": "amount",
        "type": "currency",
        "locale": "en-US"
      },
      {
        "field": "phone_number",
        "type": "phone",
        "format": "E.164"
      }
    ]
  }
}

Normalization Types

TypeDescriptionExample
dateStandardize date format"01/15/2024""2024-01-15"
currencyFormat currency values1234.5"$1,234.50"
phoneStandardize phone numbers"(555) 123-4567""+15551234567"
addressNormalize address formatStandardize street, city, zip
nameStandardize name format"JOHN DOE""John Doe"
emailNormalize email addresses"John@EXAMPLE.com""john@example.com"
urlStandardize URLsAdd protocol, normalize case
percentageFormat percentages0.156"15.6%"

Normalization Rule Fields

FieldTypeRequiredDescription
idstringNoUnique rule identifier
fieldstringYesField path to normalize
typestringYesType of normalization
formatstringNoTarget format (e.g., YYYY-MM-DD for dates)
localestringNoLocale for formatting (e.g., en-US)
optionsobjectNoAdditional normalization options

Date Normalization Examples

{
  "normalizations": [
    {
      "field": "order_date",
      "type": "date",
      "format": "YYYY-MM-DD"
    },
    {
      "field": "timestamp",
      "type": "date",
      "format": "ISO8601"
    },
    {
      "field": "birth_date",
      "type": "date",
      "format": "MM/DD/YYYY",
      "locale": "en-US"
    }
  ]
}

Currency Normalization Examples

{
  "normalizations": [
    {
      "field": "price",
      "type": "currency",
      "locale": "en-US",
      "options": {
        "currency": "USD",
        "minimumFractionDigits": 2
      }
    },
    {
      "field": "total_eur",
      "type": "currency",
      "locale": "de-DE",
      "options": {
        "currency": "EUR"
      }
    }
  ]
}

Validation Rules

Validate transformed data against constraints:

{
  "config": {
    "action": "validate",
    "sourceData": "{{steps.transform.output}}",
    "validationRules": [
      {
        "field": "email",
        "rule": "required",
        "message": "Email is required"
      },
      {
        "field": "email",
        "rule": "pattern",
        "value": "^[^@]+@[^@]+\\.[^@]+$",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "rule": "range",
        "value": { "min": 18, "max": 120 },
        "message": "Age must be between 18 and 120"
      }
    ]
  }
}

Validation Rule Types

RuleDescriptionValue Example
requiredField must exist and not be nullN/A
typeField must be specific type"string", "number", "boolean"
patternField must match regex"^[A-Z]{2}$"
rangeNumeric range{ "min": 0, "max": 100 }
enumMust be one of listed values["active", "inactive", "pending"]
customCustom validation expression"{{value}} > 0"

Validation Rule Fields

FieldTypeRequiredDescription
idstringNoUnique rule identifier
fieldstringYesField path to validate
rulestringYesValidation type
valueanyDependsValidation parameter
messagestringNoCustom error message

Target Schema

Define the expected output structure:

{
  "config": {
    "action": "transform",
    "sourceData": "{{input}}",
    "targetSchema": {
      "type": "object",
      "required": ["customerId", "email"],
      "properties": {
        "customerId": {
          "type": "string",
          "pattern": "^cus_[a-zA-Z0-9]+$"
        },
        "email": {
          "type": "string",
          "format": "email"
        },
        "name": {
          "type": "object",
          "properties": {
            "first": { "type": "string" },
            "last": { "type": "string" }
          }
        },
        "tags": {
          "type": "array",
          "items": { "type": "string" }
        }
      }
    },
    "mappingRules": [...]
  }
}

Using Templates

Use pre-built transform templates:

{
  "config": {
    "action": "transform",
    "templateId": "stripe-to-quickbooks-customer",
    "sourceData": "{{steps.get-customer.output}}"
  }
}

Transform Output

Transform steps output detailed reports:

{
  "transformed": {
    "customerId": "cus_abc123",
    "email": "customer@example.com",
    "name": {
      "first": "John",
      "last": "Doe"
    }
  },
  "mappingReport": {
    "successful": ["customerId", "email", "name.first", "name.last"],
    "failed": [],
    "defaultsUsed": ["phone"],
    "skipped": []
  },
  "normalizationReport": {
    "normalized": ["email"],
    "issues": []
  },
  "validationErrors": [],
  "isValid": true,
  "metadata": {
    "sourceFieldCount": 5,
    "targetFieldCount": 4,
    "transformDurationMs": 12
  }
}

Output Fields

FieldDescription
transformedThe transformed data matching target schema
mappingReportReport of field mapping operations
normalizationReportReport of normalization operations
validationErrorsList of validation errors
isValidWhether the transformation is valid
metadataTransformation statistics

Common Patterns

Data Mapping Between Systems

Map Stripe customer to QuickBooks format:

{
  "id": "map-to-qb",
  "type": "transform",
  "name": "Map to QuickBooks",
  "config": {
    "action": "map",
    "sourceData": "{{steps.stripe-customer.output}}",
    "mappingRules": [
      {
        "source": "name",
        "target": "DisplayName",
        "transform": "titlecase"
      },
      {
        "source": "email",
        "target": "PrimaryEmailAddr.Address",
        "transform": "lowercase"
      },
      {
        "source": "phone",
        "target": "PrimaryPhone.FreeFormNumber"
      },
      {
        "source": "address.line1",
        "target": "BillAddr.Line1"
      },
      {
        "source": "address.city",
        "target": "BillAddr.City"
      },
      {
        "source": "address.state",
        "target": "BillAddr.CountrySubDivisionCode",
        "transform": "uppercase"
      },
      {
        "source": "address.postal_code",
        "target": "BillAddr.PostalCode"
      }
    ]
  }
}

Data Cleanup and Standardization

{
  "id": "cleanup-data",
  "type": "transform",
  "name": "Clean Customer Data",
  "config": {
    "action": "normalize",
    "sourceData": "{{input}}",
    "normalizations": [
      {
        "field": "email",
        "type": "email"
      },
      {
        "field": "phone",
        "type": "phone"
      },
      {
        "field": "name",
        "type": "name"
      },
      {
        "field": "created_at",
        "type": "date",
        "format": "ISO8601"
      }
    ]
  }
}

Validation Before API Call

{
  "id": "validate-invoice",
  "type": "transform",
  "name": "Validate Invoice Data",
  "config": {
    "action": "validate",
    "sourceData": "{{input}}",
    "validationRules": [
      {
        "field": "customer_id",
        "rule": "required",
        "message": "Customer ID is required"
      },
      {
        "field": "customer_id",
        "rule": "pattern",
        "value": "^cus_",
        "message": "Invalid customer ID format"
      },
      {
        "field": "line_items",
        "rule": "required",
        "message": "At least one line item is required"
      },
      {
        "field": "amount",
        "rule": "range",
        "value": { "min": 100 },
        "message": "Amount must be at least $1.00"
      }
    ],
    "strict": true
  }
}

Enriching Data

Add computed fields:

{
  "id": "enrich-order",
  "type": "transform",
  "name": "Enrich Order Data",
  "config": {
    "action": "enrich",
    "sourceData": "{{input}}",
    "targetSchema": {
      "type": "object",
      "properties": {
        "orderId": { "type": "string" },
        "subtotal": { "type": "number" },
        "tax": { "type": "number" },
        "total": { "type": "number" },
        "processedAt": { "type": "string" }
      }
    },
    "mappingRules": [
      {
        "source": "id",
        "target": "orderId"
      },
      {
        "source": "amount",
        "target": "subtotal",
        "transform": "number"
      }
    ],
    "customInstructions": "Calculate tax at 8% and total. Add processedAt with current timestamp."
  }
}

Flattening Nested Data

{
  "id": "flatten-contacts",
  "type": "transform",
  "name": "Flatten Contact List",
  "config": {
    "action": "transform",
    "sourceData": "{{steps.fetch-accounts.output.accounts}}",
    "mappingRules": [
      {
        "source": "contacts",
        "target": "allContacts",
        "transform": "flatten"
      }
    ]
  }
}

Strict Mode

Enable strict mode to fail on any transformation error:

{
  "config": {
    "action": "transform",
    "sourceData": "{{input}}",
    "mappingRules": [...],
    "strict": true
  }
}

In strict mode:

  • Missing required fields cause failure
  • Transformation errors stop execution
  • Validation errors cause failure

Without strict mode (default):

  • Missing fields use defaults or are skipped
  • Transformation errors are logged but continue
  • Validation errors are reported but don't stop execution

Troubleshooting

Mapping Not Working

  1. Check field paths - Use dot notation for nested fields
  2. Verify source data - Add a debug transform to inspect data
  3. Check transform type - Ensure correct transform for the data type

Data Type Issues

  1. Use explicit transforms - Add number, string, boolean transforms
  2. Check for null values - Use default for missing fields
  3. Validate input - Add validation step before transform

Performance

  1. Minimize rules - Combine mappings where possible
  2. Use templates - Pre-built templates are optimized
  3. Skip unnecessary fields - Only map fields you need

Next Steps