ADF-XML Migration Guide

A complete guide for migrating from ADF-XML (Auto-lead Data Format) to ACP. Includes bidirectional field mapping, conversion code in JavaScript, Python, and PHP, data loss analysis, and a phased rollout strategy for production environments.

Why Migrate

ADF-XML was designed in 2000 for form-fill lead submissions. It captures roughly 15 flat fields: name, email, phone, and a vehicle. AI-powered interactions generate 10–50× more structured data — intents, scores, transcripts, behavioral events, deal state — none of which ADF can carry. Every ADF submission is a lossy compression of what the AI actually captured.

ACP is not a replacement that requires ripping out ADF overnight. It's designed for parallel operation: produce ACP envelopes natively, and downgrade to ADF-XML for CRMs that haven't adopted ACP yet.

▶  Watch: ADF → ACP Conversion

Migration Strategy

The recommended approach is dual-output with phased cutover:

Phase 1
Dual Output
Produce ACP envelopes natively. Auto-generate ADF-XML from each envelope for legacy CRMs. Ship both. Zero disruption.
Phase 2
ACP Primary
CRMs that support ACP receive envelopes directly. ADF fallback continues for those that don't. Monitor adoption.
Phase 3
ADF Sunset
Once a CRM accepts ACP, disable ADF for that dealer. Eventually deprecate ADF output entirely.

Field Mapping: Customer → lead.contact

ADF-XML Path ACP Path Notes
<name part="first"> lead.contact.first_name Direct mapping.
<name part="last"> lead.contact.last_name Direct mapping.
<email> lead.contact.email Direct mapping.
<phone type="voice"> lead.contact.phone ACP recommends E.164 format.
<address>...<street> lead.contact.address.street Direct mapping.
<address>...<city> lead.contact.address.city Direct mapping.
<address>...<regioncode> lead.contact.address.state ADF uses regioncode, ACP uses state.
<address>...<postalcode> lead.contact.zip Also available at lead.contact.address.zip.

Field Mapping: Vehicle → lead.vehicle_interests[]

ADF-XML Path ACP Path Notes
<vehicle interest="buy"> lead.vehicle_interests[0] ADF allows multiple <vehicle> elements; each maps to an array item.
<year> .year Direct mapping. Integer in ACP.
<make> .make Direct mapping.
<model> .model Direct mapping.
<trim> .trim Direct mapping.
<stock> .stock_number Renamed for clarity.
<vin> .vin Direct mapping.
<colorcombination>...<color> .color ADF nests under colorcombination; ACP flattens.
status="new|used" .condition ACP adds "certified" option.
— .features No ADF equivalent. ACP-only field.

Field Mapping: Vendor → dealership

ADF-XML Path ACP Path Notes
<vendorname> dealership.name Direct mapping.
<id> (vendor child) dealership.id ADF vendor ID if present; otherwise producer-generated.
— dealership.crm_provider No ADF equivalent.
— dealership.dms_provider No ADF equivalent.
— dealership.timezone No ADF equivalent.

Field Mapping: Provider → source

ADF-XML Path ACP Path Notes
<provider>...<name> source.provider Direct mapping.
<provider>...<url> source.page_url Approximate mapping.
— source.agent_type No ADF equivalent. ACP-only.
— source.channel No ADF equivalent.
— source.session_id No ADF equivalent.
— source.utm No ADF equivalent.
— source.device No ADF equivalent.

Field Mapping: Prospect → acp + lead

ADF-XML Path ACP Path Notes
<requestdate> acp.generated_at ADF uses requestdate; ACP uses ISO 8601 generated_at.
prospect status="new" lead.status ADF only has new/resend. ACP has 5 statuses.
<comments> conversation.messages[] ADF dumps all context into a single comments field. ACP has structured messages.

What ADF-XML Cannot Carry

Converting ACP → ADF-XML is a deterministic but lossy downgrade. The following ACP objects and fields have no ADF equivalent and are discarded during conversion:

0% loss89% data loss100%

Complete Loss Inventory

ACP Object / Field Status Impact
lead.intents[] LOST All detected purchase intents and confidence scores. CRM cannot see what the customer wants.
lead.score LOST Lead qualification score. CRM cannot prioritize leads by quality.
lead.score_factors[] LOST Transparent scoring breakdown. CRM has no audit trail.
lead.budget LOST Target price, monthly payment, down payment. Sales rep starts blind.
lead.trade_in LOST Trade-in vehicle, mileage, condition, payoff. Must be re-gathered.
transaction (entire) LOST Deal stage, offer history, finance terms, appointments, delivery.
conversation.messages[] LOST Full transcript. CRM gets nothing about what was actually discussed.
conversation.handoff LOST AI-to-human escalation context. Human agent starts from scratch.
conversation.sentiment LOST Customer mood. Was the customer happy, angry, or neutral?
events[] (entire) LOST All 18 behavioral event types. No audit trail of the interaction.
ai_analysis (entire) LOST Purchase probability, recommended actions, objections, summary.
source.utm LOST Marketing attribution. Cannot attribute conversions to campaigns.
source.device LOST Device type, OS, browser. No mobile vs desktop analytics.
source.session_id LOST Cross-channel linking. Cannot connect chat to return visit.
lead.contact MAPPED Name, email, phone, address survive.
lead.vehicle_interests[] MAPPED Year, make, model, trim, stock survive. Features lost.
dealership.name MAPPED Vendor name survives.
In practice: A typical qualified lead ACP envelope contains ~127 structured fields. The ADF-XML conversion preserves ~14. That's an 89% data loss rate. This is not a rounding error — it's the difference between a CRM that knows everything and a CRM that knows nothing.

Conversion: ACP → ADF-XML

The downgrade converter extracts the ADF-compatible subset from an ACP envelope and emits valid ADF-XML. This is deterministic — the same ACP envelope always produces the same ADF-XML.

JavaScript
Python
PHP
function acpToAdf(envelope) {
  const lead = envelope.lead;
  const c = lead.contact || {};
  const v = (lead.vehicle_interests || [])[0] || {};
  const d = envelope.dealership || {};

  const esc = s => s ? s.replace(/[<>&"]/g, m =>
    ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'})[m]) : '';

  const status = v.condition === 'certified' ? 'used' : (v.condition || 'new');

  return `<?xml version="1.0" encoding="UTF-8"?>
<?adf version="1.0"?>
<adf>
  <prospect status="new">
    <requestdate>${envelope.acp.generated_at}</requestdate>
    <vehicle interest="buy" status="${status}">
      ${v.year ? `<year>${v.year}</year>` : ''}
      ${v.make ? `<make>${esc(v.make)}</make>` : ''}
      ${v.model ? `<model>${esc(v.model)}</model>` : ''}
      ${v.trim ? `<trim>${esc(v.trim)}</trim>` : ''}
      ${v.stock_number ? `<stock>${esc(v.stock_number)}</stock>` : ''}
      ${v.vin ? `<vin>${esc(v.vin)}</vin>` : ''}
    </vehicle>
    <customer>
      <contact>
        ${c.first_name ? `<name part="first">${esc(c.first_name)}</name>` : ''}
        ${c.last_name ? `<name part="last">${esc(c.last_name)}</name>` : ''}
        ${c.email ? `<email>${esc(c.email)}</email>` : ''}
        ${c.phone ? `<phone type="voice">${esc(c.phone)}</phone>` : ''}
      </contact>
    </customer>
    <vendor>
      <vendorname>${esc(d.name)}</vendorname>
    </vendor>
    <provider>
      <name>${esc(envelope.source.provider)}</name>
    </provider>
  </prospect>
</adf>`;
}
from xml.sax.saxutils import escape

def acp_to_adf(envelope: dict) -> str:
    lead = envelope.get("lead", {})
    c = lead.get("contact") or {}
    v = (lead.get("vehicle_interests") or [{}])[0]
    d = envelope.get("dealership", {})
    src = envelope.get("source", {})
    gen = envelope["acp"]["generated_at"]

    condition = v.get("condition", "new")
    status = "used" if condition == "certified" else condition

    def tag(name, val, **attrs):
        if not val: return ""
        a = " ".join(f'{k}="{escape(str(v))}"' for k, v in attrs.items())
        return f"<{name}{' ' + a if a else ''}>{escape(str(val))}</{name}>"

    return f"""<?xml version="1.0" encoding="UTF-8"?>
<?adf version="1.0"?>
<adf>
  <prospect status="new">
    <requestdate>{gen}</requestdate>
    <vehicle interest="buy" status="{status}">
      {tag("year", v.get("year"))}
      {tag("make", v.get("make"))}
      {tag("model", v.get("model"))}
      {tag("trim", v.get("trim"))}
      {tag("stock", v.get("stock_number"))}
      {tag("vin", v.get("vin"))}
    </vehicle>
    <customer>
      <contact>
        {tag("name", c.get("first_name"), part="first")}
        {tag("name", c.get("last_name"), part="last")}
        {tag("email", c.get("email"))}
        {tag("phone", c.get("phone"), type="voice")}
      </contact>
    </customer>
    <vendor>{tag("vendorname", d.get("name"))}</vendor>
    <provider>{tag("name", src.get("provider"))}</provider>
  </prospect>
</adf>"""
function acpToAdf(array $envelope): string
{
    $lead = $envelope['lead'] ?? [];
    $c = $lead['contact'] ?? [];
    $v = ($lead['vehicle_interests'] ?? [[]])[0] ?? [];
    $d = $envelope['dealership'] ?? [];
    $src = $envelope['source'] ?? [];
    $gen = $envelope['acp']['generated_at'];

    $status = ($v['condition'] ?? 'new') === 'certified'
        ? 'used'
        : ($v['condition'] ?? 'new');

    $e = fn($s) => htmlspecialchars($s ?? '', ENT_XML1);

    $tag = fn($name, $val, $attrs = '') =>
        $val ? "<{$name}{$attrs}>{$e($val)}</{$name}>" : '';

    return <<<XML
    <?xml version="1.0" encoding="UTF-8"?>
    <?adf version="1.0"?>
    <adf>
      <prospect status="new">
        <requestdate>{$gen}</requestdate>
        <vehicle interest="buy" status="{$status}">
          {$tag('year', $v['year'] ?? null)}
          {$tag('make', $v['make'] ?? null)}
          {$tag('model', $v['model'] ?? null)}
          {$tag('trim', $v['trim'] ?? null)}
          {$tag('stock', $v['stock_number'] ?? null)}
          {$tag('vin', $v['vin'] ?? null)}
        </vehicle>
        <customer><contact>
          {$tag('name', $c['first_name'] ?? null, ' part="first"')}
          {$tag('name', $c['last_name'] ?? null, ' part="last"')}
          {$tag('email', $c['email'] ?? null)}
          {$tag('phone', $c['phone'] ?? null, ' type="voice"')}
        </contact></customer>
        <vendor>{$tag('vendorname', $d['name'] ?? null)}</vendor>
        <provider>{$tag('name', $src['provider'] ?? null)}</provider>
      </prospect>
    </adf>
    XML;
}

Conversion: ADF-XML → ACP

Ingesting legacy ADF-XML into an ACP-native system. This is a lossless upgrade — no ADF data is discarded. The resulting ACP envelope will have many empty optional fields, but it validates and can be enriched later.

JavaScript
Python
PHP
import { parseStringPromise } from 'xml2js';
import crypto from 'crypto';

async function adfToAcp(adfXml) {
  const parsed = await parseStringPromise(adfXml);
  const prospect = parsed.adf.prospect[0];
  const customer = prospect.customer?.[0]?.contact?.[0] || {};
  const vehicle = prospect.vehicle?.[0] || {};
  const vendor = prospect.vendor?.[0] || {};
  const provider = prospect.provider?.[0] || {};

  const names = (customer.name || []).reduce((acc, n) => {
    acc[n.$.part] = n._;
    return acc;
  }, {});

  const now = new Date().toISOString();
  const rand = crypto.randomBytes(4).toString('hex');

  return {
    acp: {
      version: "1.0.0",
      protocol: "acp",
      generated_at: prospect.requestdate?.[0] || now,
      envelope_id: `acp_${Date.now()}_${rand}`,
      parent_envelope_id: null
    },
    source: {
      provider: provider.name?.[0] || "adf_import",
      agent_type: "sales_chat",
      channel: "web_chat"
    },
    dealership: {
      id: vendor.id?.[0] || `dealer_${rand}`,
      name: vendor.vendorname?.[0] || "Unknown"
    },
    lead: {
      id: `lead_adf_${Date.now()}`,
      status: "new",
      contact: {
        first_name: names.first || null,
        last_name: names.last || null,
        email: customer.email?.[0] || null,
        phone: customer.phone?.[0]?._ || customer.phone?.[0] || null
      },
      vehicle_interests: vehicle.year ? [{
        year: parseInt(vehicle.year[0]),
        make: vehicle.make?.[0] || null,
        model: vehicle.model?.[0] || null,
        trim: vehicle.trim?.[0] || null,
        condition: vehicle.$?.status || "new",
        stock_number: vehicle.stock?.[0] || null,
        vin: vehicle.vin?.[0] || null
      }] : [],
      intents: [],
      score: null,
      score_factors: []
    },
    transaction: {
      id: `txn_adf_${Date.now()}`,
      stage: "inquiry",
      last_updated: now
    },
    conversation: {
      id: `conv_adf_${Date.now()}`,
      started_at: prospect.requestdate?.[0] || now,
      message_count: 0,
      messages: []
    },
    events: [{
      type: "conversation_started",
      timestamp: prospect.requestdate?.[0] || now,
      data: { channel: "adf_import", source: "legacy" }
    }]
  };
}
import xml.etree.ElementTree as ET
import uuid
from datetime import datetime, timezone

def adf_to_acp(adf_xml: str) -> dict:
    root = ET.fromstring(adf_xml)
    prospect = root.find("prospect")
    customer = prospect.find(".//customer/contact")
    vehicle = prospect.find("vehicle")
    vendor = prospect.find("vendor")

    # Extract name parts
    names = {}
    if customer is not None:
        for n in customer.findall("name"):
            names[n.get("part", "")] = n.text

    now = datetime.now(timezone.utc).isoformat()
    rand = uuid.uuid4().hex[:8]
    req_date = prospect.findtext("requestdate", now)

    return {
        "acp": {
            "version": "1.0.0",
            "protocol": "acp",
            "generated_at": req_date,
            "envelope_id": f"acp_{int(datetime.now().timestamp())}_{rand}",
            "parent_envelope_id": None
        },
        # ... same structure as JS version
        # Populate source, dealership, lead, transaction,
        # conversation, events from parsed XML
    }
function adfToAcp(string $adfXml): array
{
    $xml = simplexml_load_string($adfXml);
    $prospect = $xml->prospect;
    $contact = $prospect->customer->contact;
    $vehicle = $prospect->vehicle;
    $vendor = $prospect->vendor;

    $names = [];
    foreach ($contact->name ?? [] as $n) {
        $names[(string) $n['part']] = (string) $n;
    }

    $now = gmdate('c');
    $rand = bin2hex(random_bytes(4));
    $reqDate = (string) ($prospect->requestdate ?? $now);

    return [
        'acp' => [
            'version' => '1.0.0',
            'protocol' => 'acp',
            'generated_at' => $reqDate,
            'envelope_id' => "acp_" . time() . "_{$rand}",
            'parent_envelope_id' => null,
        ],
        'source' => [
            'provider' => (string) ($prospect->provider->name ?? 'adf_import'),
            'agent_type' => 'sales_chat',
            'channel' => 'web_chat',
        ],
        'dealership' => [
            'id' => (string) ($vendor->id ?? "dealer_{$rand}"),
            'name' => (string) ($vendor->vendorname ?? 'Unknown'),
        ],
        'lead' => [
            'id' => "lead_adf_" . time(),
            'status' => 'new',
            'contact' => [
                'first_name' => $names['first'] ?? null,
                'last_name' => $names['last'] ?? null,
                'email' => (string) ($contact->email ?? null),
                'phone' => (string) ($contact->phone ?? null),
            ],
            'vehicle_interests' => $vehicle ? [[
                'year' => (int) $vehicle->year,
                'make' => (string) $vehicle->make,
                'model' => (string) $vehicle->model,
                'trim' => (string) ($vehicle->trim ?? null),
                'condition' => (string) ($vehicle['status'] ?? 'new'),
                'stock_number' => (string) ($vehicle->stock ?? null),
            ]] : [],
            'intents' => [],
            'score' => null,
        ],
        // transaction, conversation, events...
    ];
}
ADF → ACP is lossless. Every field in ADF-XML has a corresponding ACP field. The resulting envelope is sparse (many null/empty optional fields) but schema-valid and ready for enrichment by AI processing.

Dual-Output Pattern

During migration, produce both formats from the same interaction. The ACP envelope is the source of truth; ADF-XML is deterministically derived from it.

dual-output.jsJavaScript
async function deliverTocrm(envelope, dealer) {
  // ACP-native delivery (preferred)
  if (dealer.supportsAcp) {
    await fetch(dealer.acpEndpoint, {
      method: "POST",
      headers: {
        "Content-Type": "application/acp+json",
        "X-ACP-Version": "1.0.0",
        "X-ACP-Envelope-Id": envelope.acp.envelope_id,
      },
      body: JSON.stringify(envelope)
    });
  }

  // ADF-XML fallback (lossy but compatible)
  if (dealer.adfEndpoint) {
    const adfXml = acpToAdf(envelope);
    await fetch(dealer.adfEndpoint, {
      method: "POST",
      headers: { "Content-Type": "application/xml" },
      body: adfXml
    });
  }
}
Never generate ADF-XML independently. Always derive it from the ACP envelope. This ensures data consistency and makes ADF a deterministic projection, not a separate pipeline.

Rollout Phases

Phase 1: Dual Output (Week 1–4)

  • Implement acpToAdf() converter
  • Ship ACP envelopes to internal logging / data warehouse
  • Ship ADF-XML to existing CRM endpoints (no CRM changes)
  • Compare field-level output to verify converter accuracy

Phase 2: ACP Primary (Week 5–12)

  • Engage CRM vendors who support ACP or have ingestion APIs
  • Switch ACP-ready CRMs from ADF to native ACP delivery
  • Maintain ADF fallback for remaining CRMs
  • Monitor ACP delivery success rates and error codes

Phase 3: ADF Sunset (Month 4+)

  • Disable ADF output per dealer as their CRM adopts ACP
  • Track remaining ADF-only dealers and engage their CRM vendors
  • Deprecation notice: 90-day warning before removing ADF for a dealer
  • Target: 100% ACP delivery within 12 months

Testing Checklist

  1. Round-trip test: Create ACP envelope → convert to ADF → convert back to ACP → verify contact + vehicle fields survive.
  2. Empty field handling: Convert ACP envelope with null contact/vehicle fields → verify ADF output omits empty tags cleanly.
  3. Special characters: Verify XML escaping for names like O'Brien, addresses with &, etc.
  4. Multiple vehicles: ACP envelope with 3+ vehicle interests → verify ADF generates multiple <vehicle> elements.
  5. Certified vehicles: ACP condition: "certified" → ADF status="used" (ADF has no certified option).
  6. Schema validation: Run both the ACP envelope and the generated ADF-XML through their respective schema validators.
  7. Data loss audit: For each converted envelope, log how many ACP fields were discarded. Track the loss rate over time.