<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kavi Writes</title>
    <description>The latest articles on DEV Community by Kavi Writes (@kaviwrites).</description>
    <link>https://dev.to/kaviwrites</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4148900%2Fcefa1b3d-38d1-4499-a790-edb330697608.png</url>
      <title>DEV Community: Kavi Writes</title>
      <link>https://dev.to/kaviwrites</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaviwrites"/>
    <language>en</language>
    <item>
      <title>Grounding Palantir AIP Workflows in the Ontology: Tool Design, Guardrails, and Evaluation Patterns</title>
      <dc:creator>Kavi Writes</dc:creator>
      <pubDate>Wed, 14 Oct 2026 10:24:46 +0000</pubDate>
      <link>https://dev.to/kaviwrites/grounding-palantir-aip-workflows-in-the-ontology-tool-design-guardrails-and-evaluation-patterns-1pg8</link>
      <guid>https://dev.to/kaviwrites/grounding-palantir-aip-workflows-in-the-ontology-tool-design-guardrails-and-evaluation-patterns-1pg8</guid>
      <description>&lt;h1&gt;
  
  
  Migrating Legacy ETL Pipelines to Palantir Foundry: Incremental Processing, Schema Evolution, and Reconciliation
&lt;/h1&gt;

&lt;p&gt;Replacing a legacy ETL platform is rarely a simple exercise in rewriting SQL or moving transformation code.&lt;/p&gt;

&lt;p&gt;The difficult part is preserving data correctness while changing how data is ingested, transformed, validated, and consumed. Existing pipelines may support dashboards, operational reports, financial processes, APIs, machine learning workloads, and applications that nobody wants to interrupt.&lt;/p&gt;

&lt;p&gt;For a &lt;strong&gt;&lt;a href="https://kasadara.com/palantir-consulting/" rel="noopener noreferrer"&gt;Palantir Foundry migration&lt;/a&gt;&lt;/strong&gt;, three concerns deserve particular attention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Incremental data processing&lt;/li&gt;
&lt;li&gt;Schema evolution&lt;/li&gt;
&lt;li&gt;Data reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fourth concern connects all three: how to operate the legacy and Foundry pipelines safely during the transition.&lt;/p&gt;

&lt;p&gt;The objective should not be to move every pipeline as quickly as possible. A better objective is to create a controlled migration in which differences can be detected before they affect consumers.&lt;/p&gt;

&lt;p&gt;This article describes one practical migration pattern. It is an architectural approach, not a mandatory Palantir methodology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Legacy ETL Migrations Are Difficult
&lt;/h2&gt;

&lt;p&gt;Older ETL estates often contain much more business logic than their diagrams suggest.&lt;/p&gt;

&lt;p&gt;A nightly job might appear to perform four steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract -&amp;gt; Transform -&amp;gt; Load -&amp;gt; Report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, the process may depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored procedures maintained by another team&lt;/li&gt;
&lt;li&gt;File drops from external systems&lt;/li&gt;
&lt;li&gt;Hard-coded business mappings&lt;/li&gt;
&lt;li&gt;Historical correction rules&lt;/li&gt;
&lt;li&gt;Scheduled dependencies&lt;/li&gt;
&lt;li&gt;Manual restart procedures&lt;/li&gt;
&lt;li&gt;Temporary tables&lt;/li&gt;
&lt;li&gt;Reporting-specific transformations&lt;/li&gt;
&lt;li&gt;Downstream jobs that assume a particular schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some dependencies are documented. Others exist only because one job happens to finish before another begins.&lt;/p&gt;

&lt;p&gt;This is why a direct "lift and shift" can be dangerous.&lt;/p&gt;

&lt;p&gt;The technical transformation may be reproduced correctly while an operational dependency is missed.&lt;/p&gt;

&lt;p&gt;A big-bang migration increases that risk. If extraction, transformation, storage, schedules, downstream interfaces, and reporting consumers all move together, diagnosing a discrepancy becomes harder.&lt;/p&gt;

&lt;p&gt;A safer approach reduces the migration boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Safer Migration Pattern
&lt;/h2&gt;

&lt;p&gt;Instead of migrating an entire ETL estate at once, divide it into data products or logical pipeline groups.&lt;/p&gt;

&lt;p&gt;A possible migration lifecycle is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Inventory existing pipelines.&lt;/li&gt;
&lt;li&gt;Identify source and downstream dependencies.&lt;/li&gt;
&lt;li&gt;Select a manageable migration boundary.&lt;/li&gt;
&lt;li&gt;Rebuild the required pipeline in Foundry.&lt;/li&gt;
&lt;li&gt;Keep the existing ETL process operating.&lt;/li&gt;
&lt;li&gt;Execute the new pipeline in parallel.&lt;/li&gt;
&lt;li&gt;Reconcile legacy and Foundry outputs.&lt;/li&gt;
&lt;li&gt;Investigate unexplained differences.&lt;/li&gt;
&lt;li&gt;Move consumers gradually.&lt;/li&gt;
&lt;li&gt;Retire the corresponding legacy jobs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, suppose a reporting warehouse receives customer, product, order, invoice, and payment data.&lt;/p&gt;

&lt;p&gt;Migrating all five domains simultaneously makes reconciliation difficult. Instead, the team might begin with the order domain while maintaining its existing interfaces to customer and product data.&lt;/p&gt;

&lt;p&gt;The boundary should be large enough to deliver something useful but small enough to validate thoroughly.&lt;/p&gt;

&lt;p&gt;This staged approach is an implementation strategy. Foundry provides capabilities that can support pipeline construction, data validation, and incremental processing, but organizations still need to design the migration sequence around their own dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental Transforms in Foundry
&lt;/h2&gt;

&lt;p&gt;Full-refresh pipelines are common in legacy environments.&lt;/p&gt;

&lt;p&gt;Consider a source table containing 500 million transaction records. A nightly pipeline might reread and transform the entire table even though only a fraction changed during the day.&lt;/p&gt;

&lt;p&gt;A simplified process could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read all transactions
        |
Apply transformation rules
        |
Recalculate entire output
        |
Replace reporting table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That design may be acceptable for smaller datasets, but it becomes increasingly expensive and operationally awkward as data grows.&lt;/p&gt;

&lt;p&gt;Foundry supports incremental transforms that use build history to avoid recomputing the entire output when appropriate. Palantir documents its incremental transform model as using previous build information to determine the state of inputs and process eligible changes incrementally.&lt;/p&gt;

&lt;p&gt;Conceptually, the migration could instead become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Identify new or changed records
        |
Process affected data
        |
Merge or append results
        |
Publish updated dataset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Change detection
&lt;/h3&gt;

&lt;p&gt;A migration team first needs to understand how the source exposes changes.&lt;/p&gt;

&lt;p&gt;Possible mechanisms include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Append-only transactions&lt;/li&gt;
&lt;li&gt;Modification timestamps&lt;/li&gt;
&lt;li&gt;Change-data-capture feeds&lt;/li&gt;
&lt;li&gt;Source sequence numbers&lt;/li&gt;
&lt;li&gt;Partition dates&lt;/li&gt;
&lt;li&gt;Snapshot comparisons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct choice depends on the source architecture.&lt;/p&gt;

&lt;p&gt;A timestamp such as &lt;code&gt;last_modified_at&lt;/code&gt; can sometimes act as a watermark:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous_watermark = last_successful_timestamp

read records where:
last_modified_at &amp;gt; previous_watermark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an architectural concept rather than a universal Foundry requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partitioning
&lt;/h3&gt;

&lt;p&gt;Partitioning can help narrow processing boundaries.&lt;/p&gt;

&lt;p&gt;A transaction pipeline might partition data by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transaction_date
region
source_system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the source changes only recent partitions, the pipeline may avoid unnecessary work on older data.&lt;/p&gt;

&lt;p&gt;Partition design should follow access and update patterns rather than being added only for migration purposes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Late-arriving data
&lt;/h3&gt;

&lt;p&gt;Watermarks introduce another challenge.&lt;/p&gt;

&lt;p&gt;Suppose the pipeline processed data through 02:00, but an upstream application later sends a transaction created at 01:55.&lt;/p&gt;

&lt;p&gt;A strict timestamp boundary could miss it.&lt;/p&gt;

&lt;p&gt;Teams may address late arrivals with strategies such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reprocessing a recent time window&lt;/li&gt;
&lt;li&gt;Tracking source sequence identifiers&lt;/li&gt;
&lt;li&gt;Maintaining correction feeds&lt;/li&gt;
&lt;li&gt;Comparing recent partitions&lt;/li&gt;
&lt;li&gt;Designing source ingestion around append semantics where possible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The correct approach depends on the source system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Idempotency matters
&lt;/h3&gt;

&lt;p&gt;Incremental processing should also consider retries.&lt;/p&gt;

&lt;p&gt;If yesterday's batch runs twice, the output should not unintentionally duplicate transactions.&lt;/p&gt;

&lt;p&gt;A useful design question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this transform safely process the same source change more than once?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keys, merge rules, deduplication logic, and transaction semantics should be considered before an incremental design is promoted.&lt;/p&gt;

&lt;p&gt;Palantir's documentation also notes that certain input changes can cause an incremental transform to fall back to non-incremental execution, so teams should understand how their selected transform implementation behaves rather than assuming every run will always remain incremental.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Schema Evolution
&lt;/h2&gt;

&lt;p&gt;Schema changes are one of the easiest ways to break a migration unexpectedly.&lt;/p&gt;

&lt;p&gt;Typical changes include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a column&lt;/li&gt;
&lt;li&gt;Renaming a field&lt;/li&gt;
&lt;li&gt;Removing a field&lt;/li&gt;
&lt;li&gt;Changing a data type&lt;/li&gt;
&lt;li&gt;Modifying nullability&lt;/li&gt;
&lt;li&gt;Changing nested structures&lt;/li&gt;
&lt;li&gt;Introducing a new source-system version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Foundry datasets can have schemas describing fields and their types, and schemas can change as the structure of a dataset changes over time.&lt;/p&gt;

&lt;p&gt;That does not mean every schema change should automatically propagate through a production pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use explicit schema contracts
&lt;/h3&gt;

&lt;p&gt;Assume a customer source currently provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id: string
customer_name: string
credit_limit: decimal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A source upgrade changes it to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id: string
customer_name: string
credit_limit: string
customer_segment: string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An automatic mapping could hide a significant problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;credit_limit&lt;/code&gt; may now contain values such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"50000"
"25K"
"NOT_AVAILABLE"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The field name stayed the same, but its meaning and validation requirements changed.&lt;/p&gt;

&lt;p&gt;A safer transformation explicitly handles the new schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate source type
        |
normalize credit_limit
        |
reject or quarantine invalid values
        |
map customer_segment
        |
publish stable consumer schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Separate source schemas from consumer contracts
&lt;/h3&gt;

&lt;p&gt;A useful pattern is to avoid forcing every upstream schema change directly onto downstream consumers.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw source schema
      |
Normalization layer
      |
Canonical customer schema
      |
Reporting / analytics / applications
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a controlled boundary.&lt;/p&gt;

&lt;p&gt;Source systems can evolve while the canonical output remains stable until consumers are ready for a new version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat incremental schema changes carefully
&lt;/h3&gt;

&lt;p&gt;Incremental pipelines deserve additional attention because historical and newly written records may have been produced under different assumptions.&lt;/p&gt;

&lt;p&gt;Palantir's incremental transform documentation describes schema compatibility considerations when reading previous output. It also documents patterns such as changing a transform's semantic version when a logic or schema change requires existing data to be recomputed.&lt;/p&gt;

&lt;p&gt;The implementation decision should therefore answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can old and new records coexist safely?&lt;/li&gt;
&lt;li&gt;Does historical data require recomputation?&lt;/li&gt;
&lt;li&gt;Will downstream consumers understand the new field?&lt;/li&gt;
&lt;li&gt;Does a type change require migration logic?&lt;/li&gt;
&lt;li&gt;Should the new schema be introduced through a versioned dataset?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schema evolution should be treated as an interface change, not merely a column change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation Between Legacy and Foundry Pipelines
&lt;/h2&gt;

&lt;p&gt;Building the new pipeline is only half the migration.&lt;/p&gt;

&lt;p&gt;The next question is more important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it produce the correct result?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common mistake is checking only row counts.&lt;/p&gt;

&lt;p&gt;Suppose both systems produce exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000,000 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That does not prove equivalence.&lt;/p&gt;

&lt;p&gt;The pipelines could contain different records, incorrect values, duplicate keys, or different revenue totals while still having identical row counts.&lt;/p&gt;

&lt;p&gt;Reconciliation should happen at several levels.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Row counts
&lt;/h3&gt;

&lt;p&gt;Compare overall counts and meaningful subsets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;legacy_count = 10,000,000
foundry_count = 10,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare by business dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;count by date
count by region
count by product
count by status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Key coverage
&lt;/h3&gt;

&lt;p&gt;Compare business keys.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;legacy_only_keys
foundry_only_keys
matching_keys
duplicate_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unexpected missing keys usually deserve investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Record-level comparison
&lt;/h3&gt;

&lt;p&gt;For shared keys, compare significant fields.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;foundry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;foundry&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;foundry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;foundry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real implementations may require null handling, rounding rules, timestamps, or tolerance thresholds.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Aggregate reconciliation
&lt;/h3&gt;

&lt;p&gt;Business aggregates often expose problems faster than row-level inspection.&lt;/p&gt;

&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUM(invoice_amount)
COUNT(DISTINCT customer_id)
AVG(order_value)
SUM(quantity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then calculate them across meaningful dimensions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Legacy&lt;/th&gt;
&lt;th&gt;Foundry&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Order count&lt;/td&gt;
&lt;td&gt;1,004,218&lt;/td&gt;
&lt;td&gt;1,004,218&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;87,412,552.10&lt;/td&gt;
&lt;td&gt;87,412,552.10&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique customers&lt;/td&gt;
&lt;td&gt;124,911&lt;/td&gt;
&lt;td&gt;124,911&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numbers above are purely illustrative.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Data quality indicators
&lt;/h3&gt;

&lt;p&gt;Compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Null percentages&lt;/li&gt;
&lt;li&gt;Duplicate rates&lt;/li&gt;
&lt;li&gt;Invalid codes&lt;/li&gt;
&lt;li&gt;Referential integrity&lt;/li&gt;
&lt;li&gt;Date ranges&lt;/li&gt;
&lt;li&gt;Unexpected values&lt;/li&gt;
&lt;li&gt;Key uniqueness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Foundry provides mechanisms for defining data expectations and health checks. Depending on the implementation, these can validate requirements such as row counts, non-null values, permitted values, primary-key conditions, and custom row-level conditions.&lt;/p&gt;

&lt;p&gt;These platform capabilities can support validation, but migration-specific reconciliation logic still has to reflect the organization's business rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Both Pipelines Without Downtime
&lt;/h2&gt;

&lt;p&gt;Parallel execution gives teams time to establish confidence before changing consumers.&lt;/p&gt;

&lt;p&gt;The pattern may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 +-&amp;gt; Legacy ETL -&amp;gt; Legacy output -&amp;gt; Current consumers
Source systems --|
                 +-&amp;gt; Foundry pipeline -&amp;gt; New output -&amp;gt; Validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users continue reading from the existing production path while the Foundry output is evaluated.&lt;/p&gt;

&lt;p&gt;This is often called shadow processing or parallel validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Move consumers gradually
&lt;/h3&gt;

&lt;p&gt;Once reconciliation is stable, consumers can be migrated separately.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Week 1:
Finance dashboard -&amp;gt; Legacy
Operations dashboard -&amp;gt; Legacy
Risk model -&amp;gt; Legacy

Week 2:
Finance dashboard -&amp;gt; Foundry
Operations dashboard -&amp;gt; Legacy
Risk model -&amp;gt; Legacy

Week 3:
Finance dashboard -&amp;gt; Foundry
Operations dashboard -&amp;gt; Foundry
Risk model -&amp;gt; Legacy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates smaller cutover events.&lt;/p&gt;

&lt;p&gt;If a problem appears in one consumer, the entire migration does not necessarily need to be reversed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define rollback before cutover
&lt;/h3&gt;

&lt;p&gt;A rollback plan should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What conditions trigger rollback?&lt;/li&gt;
&lt;li&gt;Can the consumer reconnect to the legacy output?&lt;/li&gt;
&lt;li&gt;How long will the legacy pipeline remain operational?&lt;/li&gt;
&lt;li&gt;Which team makes the rollback decision?&lt;/li&gt;
&lt;li&gt;How will data generated during the transition be handled?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not define these rules during an incident.&lt;/p&gt;

&lt;p&gt;The goal is not to promise literal zero downtime. The goal is to minimize disruption and preserve a controlled path back to a known state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Migration Example
&lt;/h2&gt;

&lt;p&gt;Consider a hypothetical distributor with a nightly ETL pipeline.&lt;/p&gt;

&lt;p&gt;Three operational systems provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer records&lt;/li&gt;
&lt;li&gt;Orders&lt;/li&gt;
&lt;li&gt;Payment transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every night, the legacy platform reloads the data warehouse. Finance and sales dashboards depend on the output by 7:00 AM.&lt;/p&gt;

&lt;p&gt;The migration could proceed in six stages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1: Understand the legacy pipeline
&lt;/h3&gt;

&lt;p&gt;The team documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRM -----------+
               |
ERP -&amp;gt; Orders -+-&amp;gt; Legacy ETL -&amp;gt; Warehouse -&amp;gt; BI
               |
Payments ------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transformation rules, schedules, keys, error handling, and downstream dependencies are identified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Build the Foundry version
&lt;/h3&gt;

&lt;p&gt;The equivalent processing path is implemented in Foundry.&lt;/p&gt;

&lt;p&gt;The objective is not yet optimization. The first priority is producing a result that can be compared with the existing pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3: Introduce incremental processing
&lt;/h3&gt;

&lt;p&gt;Instead of refreshing ten years of orders nightly, the new design processes changed data where the source and pipeline architecture allow it.&lt;/p&gt;

&lt;p&gt;Historical data is loaded during initial construction.&lt;/p&gt;

&lt;p&gt;Subsequent processing focuses on eligible new or changed records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 4: Run both pipelines
&lt;/h3&gt;

&lt;p&gt;For several production cycles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Legacy output  ----+
                   +-&amp;gt; Reconciliation process
Foundry output ----+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration team compares counts, keys, aggregates, duplicates, nulls, and important business rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 5: Explain every material difference
&lt;/h3&gt;

&lt;p&gt;Imagine the Foundry result reports 214 more orders.&lt;/p&gt;

&lt;p&gt;The difference should not immediately be classified as an error.&lt;/p&gt;

&lt;p&gt;Investigation might find that the legacy job silently excluded late-arriving orders after its extraction window.&lt;/p&gt;

&lt;p&gt;Now the team must decide what the correct business behavior should be.&lt;/p&gt;

&lt;p&gt;Reconciliation is therefore not simply about making the new system match old defects. It is about identifying and explaining differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 6: Move consumers and retire legacy jobs
&lt;/h3&gt;

&lt;p&gt;After agreed validation thresholds are met, consumers are migrated individually.&lt;/p&gt;

&lt;p&gt;The legacy process remains available during the defined rollback period.&lt;/p&gt;

&lt;p&gt;Only after downstream dependencies, schedules, reconciliations, and support responsibilities have been confirmed should the corresponding legacy jobs be retired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Migration Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Migrating everything at once
&lt;/h3&gt;

&lt;p&gt;Large migration boundaries increase the number of possible causes when something fails.&lt;/p&gt;

&lt;p&gt;Start with well-understood domains.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ignoring downstream dependencies
&lt;/h3&gt;

&lt;p&gt;A dataset is not isolated simply because no other ETL job reads it.&lt;/p&gt;

&lt;p&gt;Reports, spreadsheets, APIs, applications, data science workflows, and manual processes may depend on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treating schema changes as trivial
&lt;/h3&gt;

&lt;p&gt;Changing &lt;code&gt;integer&lt;/code&gt; to &lt;code&gt;string&lt;/code&gt; is not merely metadata if consumers perform arithmetic on the value.&lt;/p&gt;

&lt;p&gt;Evaluate semantic impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating only row counts
&lt;/h3&gt;

&lt;p&gt;Matching counts can hide incorrect records and aggregates.&lt;/p&gt;

&lt;p&gt;Use multiple reconciliation dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching consumers too early
&lt;/h3&gt;

&lt;p&gt;A successful pipeline run does not prove production readiness.&lt;/p&gt;

&lt;p&gt;Reconciliation should reach an agreed confidence level first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failing to define rollback criteria
&lt;/h3&gt;

&lt;p&gt;Teams need measurable cutover and rollback conditions.&lt;/p&gt;

&lt;p&gt;"Rollback if something looks wrong" is not sufficient.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaving ownership unclear
&lt;/h3&gt;

&lt;p&gt;During parallel operation, determine who owns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legacy failures&lt;/li&gt;
&lt;li&gt;Foundry failures&lt;/li&gt;
&lt;li&gt;Reconciliation issues&lt;/li&gt;
&lt;li&gt;Source-system changes&lt;/li&gt;
&lt;li&gt;Consumer migration&lt;/li&gt;
&lt;li&gt;Final cutover approval&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Retiring legacy pipelines too early
&lt;/h3&gt;

&lt;p&gt;Removing the previous processing path immediately after cutover eliminates a useful recovery option.&lt;/p&gt;

&lt;p&gt;Retirement should follow validated operational stability and dependency confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Palantir Consulting Can Fit
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Palantir consulting&lt;/strong&gt; team or experienced internal implementation group can be useful when the main challenge is architectural rather than simply writing transforms.&lt;/p&gt;

&lt;p&gt;Typical areas of support may include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legacy dependency analysis&lt;/li&gt;
&lt;li&gt;Migration boundary design&lt;/li&gt;
&lt;li&gt;Foundry pipeline architecture&lt;/li&gt;
&lt;li&gt;Incremental processing strategy&lt;/li&gt;
&lt;li&gt;Schema compatibility planning&lt;/li&gt;
&lt;li&gt;Reconciliation design&lt;/li&gt;
&lt;li&gt;Data quality controls&lt;/li&gt;
&lt;li&gt;Cutover planning&lt;/li&gt;
&lt;li&gt;Operational handover&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important qualification is domain knowledge.&lt;/p&gt;

&lt;p&gt;A technically elegant Foundry implementation can still fail a migration if the team does not understand the meaning of the legacy data, hidden consumer dependencies, or operational recovery requirements.&lt;/p&gt;

&lt;p&gt;For that reason, Palantir Consulting Services should be evaluated partly on their ability to reason about the existing environment, not only their familiarity with the destination platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving a pipeline into production, confirm the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  Source and dependencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Source systems are documented.&lt;/li&gt;
&lt;li&gt;[ ] Source update behavior is understood.&lt;/li&gt;
&lt;li&gt;[ ] Downstream consumers are identified.&lt;/li&gt;
&lt;li&gt;[ ] Scheduling dependencies are known.&lt;/li&gt;
&lt;li&gt;[ ] Hidden manual processes have been investigated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transformation design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Existing transformation rules are understood.&lt;/li&gt;
&lt;li&gt;[ ] Incremental versus full processing has been decided.&lt;/li&gt;
&lt;li&gt;[ ] Late-arriving data is handled.&lt;/li&gt;
&lt;li&gt;[ ] Retry behavior is understood.&lt;/li&gt;
&lt;li&gt;[ ] Idempotency has been considered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Schema
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Source schemas are documented.&lt;/li&gt;
&lt;li&gt;[ ] Consumer schema contracts are defined.&lt;/li&gt;
&lt;li&gt;[ ] Type changes are handled explicitly.&lt;/li&gt;
&lt;li&gt;[ ] Compatibility risks are identified.&lt;/li&gt;
&lt;li&gt;[ ] Historical recomputation requirements are understood.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Row counts are compared.&lt;/li&gt;
&lt;li&gt;[ ] Key coverage is compared.&lt;/li&gt;
&lt;li&gt;[ ] Important fields are reconciled.&lt;/li&gt;
&lt;li&gt;[ ] Business aggregates are validated.&lt;/li&gt;
&lt;li&gt;[ ] Null and duplicate rates are checked.&lt;/li&gt;
&lt;li&gt;[ ] Business rules are tested.&lt;/li&gt;
&lt;li&gt;[ ] Material differences are explained.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cutover
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Parallel execution has completed successfully.&lt;/li&gt;
&lt;li&gt;[ ] Monitoring is in place.&lt;/li&gt;
&lt;li&gt;[ ] Cutover criteria are documented.&lt;/li&gt;
&lt;li&gt;[ ] Rollback criteria are documented.&lt;/li&gt;
&lt;li&gt;[ ] Ownership is assigned.&lt;/li&gt;
&lt;li&gt;[ ] Consumers are migrated deliberately.&lt;/li&gt;
&lt;li&gt;[ ] Legacy retirement occurs only after validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;A successful legacy ETL migration to Palantir Foundry is not primarily a code-conversion project.&lt;/p&gt;

&lt;p&gt;It is a controlled change to the way enterprise data is produced and trusted.&lt;/p&gt;

&lt;p&gt;Incremental processing can reduce unnecessary recomputation, but it requires careful handling of source changes, retries, historical state, and late-arriving data.&lt;/p&gt;

&lt;p&gt;Schema evolution can support changing business systems, but only when compatibility and consumer impact are managed deliberately.&lt;/p&gt;

&lt;p&gt;Reconciliation provides the evidence needed to determine whether the new pipeline is actually correct.&lt;/p&gt;

&lt;p&gt;Finally, parallel execution creates room to validate those decisions before production consumers are fully committed to the new path.&lt;/p&gt;

&lt;p&gt;The strongest migration plans therefore focus on three questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the data correct?

Can we explain every important difference?

Can we recover safely if the cutover fails?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those questions can be answered confidently, the organization is doing more than moving ETL workloads. It is building a safer foundation for long-term enterprise data transformation.&lt;/p&gt;

</description>
      <category>palantir</category>
      <category>dataengineering</category>
      <category>etl</category>
      <category>datamigration</category>
    </item>
    <item>
      <title>Migrating Legacy ETL to Palantir Foundry: Incremental Transforms, Schema Evolution, and Reconciliation Without Downtime</title>
      <dc:creator>Kavi Writes</dc:creator>
      <pubDate>Tue, 06 Oct 2026 07:13:40 +0000</pubDate>
      <link>https://dev.to/kaviwrites/migrating-legacy-etl-to-palantir-foundry-incremental-transforms-schema-evolution-and-18lm</link>
      <guid>https://dev.to/kaviwrites/migrating-legacy-etl-to-palantir-foundry-incremental-transforms-schema-evolution-and-18lm</guid>
      <description>&lt;p&gt;Legacy ETL replacements rarely fail because the new platform can't run the transformations. They fail because nobody can prove the new outputs are correct, or because a finance report three teams away breaks on the first Monday after cutover.&lt;/p&gt;

&lt;p&gt;A legacy pipeline has usually been around long enough to collect undocumented fixes, silent type coercions, and consumers nobody remembers onboarding. Rewriting its SQL in a new tool is the easy part. Keeping the behavior that the business actually depends on is the hard part.&lt;/p&gt;

&lt;p&gt;This article covers three concerns that decide whether a migration to Palantir Foundry goes smoothly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Incremental transformation:&lt;/strong&gt; processing only what changed, instead of rebuilding everything nightly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema evolution:&lt;/strong&gt; handling source and output schema changes without breaking consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation:&lt;/strong&gt; proving, with evidence, that the new pipeline produces equivalent results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running the legacy and Foundry pipelines side by side ties these together. Parallel operation turns cutover from a leap of faith into a decision backed by data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Legacy ETL Migrations Are Difficult
&lt;/h2&gt;

&lt;p&gt;Most legacy ETL estates share a familiar set of problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monolithic jobs.&lt;/strong&gt; A single workflow extracts, cleans, joins, aggregates, and loads, so you can't migrate one piece without understanding all of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden dependencies.&lt;/strong&gt; A downstream job reads an intermediate staging table that was never meant to be public.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard-coded logic.&lt;/strong&gt; Currency rates, region mappings, and exclusion lists live inside stored procedures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragile schedules.&lt;/strong&gt; Job B starts at 02:30 because job A "usually finishes by then."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconsistent schemas.&lt;/strong&gt; The same customer ID is a string in one system and an integer in another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited observability.&lt;/strong&gt; Failures surface as a stale dashboard, not as an alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locked-in consumers.&lt;/strong&gt; Reports, extracts, and applications depend on exact table names, column orders, and even rounding behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A big bang migration bets that every one of these has been found and handled before go-live. In practice, some will only be found after go-live, when the old system is gone and there's nothing to compare against.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Safer Migration Pattern
&lt;/h2&gt;

&lt;p&gt;An incremental approach reduces that risk by migrating in bounded slices. One common lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt; existing jobs, sources, targets, and schedules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify&lt;/strong&gt; critical data products and who consumes them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select a migration boundary&lt;/strong&gt; small enough to validate fully, such as one subject area or one fact table and its dimensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build&lt;/strong&gt; the corresponding Foundry pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run both pipelines in parallel&lt;/strong&gt; on the same inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare outputs&lt;/strong&gt; systematically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve differences&lt;/strong&gt;, classifying each as a new-pipeline bug, a legacy bug, or an accepted change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shift consumers gradually.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retire&lt;/strong&gt; the legacy pipeline once nothing reads from it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is an architectural pattern, not a mandatory Palantir methodology. Its value is that every step produces evidence, and the blast radius of a mistake stays limited to one slice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental Transforms in Foundry
&lt;/h2&gt;

&lt;p&gt;Many legacy jobs follow a truncate-and-reload pattern: delete the target table, rebuild it from full source extracts. It's simple, but cost and runtime grow with total data volume rather than with the volume of change.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Foundry provides
&lt;/h3&gt;

&lt;p&gt;Foundry datasets are versioned through transactions. A build either commits a transaction or it doesn't, so a failed run doesn't leave a half-written output visible to consumers. Python transforms in Code Repositories support an &lt;code&gt;@incremental&lt;/code&gt; decorator. When a transform runs incrementally, reading an input returns only the rows added since the last successful build, and the output can append rather than replace. Bumping the decorator's &lt;code&gt;semantic_version&lt;/code&gt; forces a full recompute, which is useful when logic changes. Data Connection also supports incremental syncs for some source types, typically by tracking a monotonically increasing column such as an update timestamp or sequence ID. Pipeline Builder has its own incremental options; what's available can depend on your enrollment and pipeline type.&lt;/p&gt;

&lt;p&gt;An important constraint: incremental transforms are built around appended data. If an upstream dataset is fully replaced by a snapshot, a downstream incremental transform generally can't run incrementally. Foundry datasets also aren't key-based tables with native upserts, so "latest state per key" has to be modeled explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design concerns
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change detection.&lt;/strong&gt; Prefer reliable change signals (CDC feeds, sequence numbers) over &lt;code&gt;updated_at&lt;/code&gt; columns that applications forget to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partitioning.&lt;/strong&gt; Partition outputs by a column consumers filter on, such as business date, so reprocessing stays targeted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watermarks.&lt;/strong&gt; Ingestion-side watermarks decide what gets pulled. Transform-side incrementality decides what gets processed. Keep the two concepts separate in your design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late-arriving data.&lt;/strong&gt; An order updated today for last month's date lands in today's increment. Any aggregate by business date must recompute the affected dates, not just append.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency.&lt;/strong&gt; Transactions make commits atomic, but your logic must still be deterministic. Avoid &lt;code&gt;current_timestamp()&lt;/code&gt; inside business logic and deduplicate on stable keys.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: from full refresh to changelog plus current state
&lt;/h3&gt;

&lt;p&gt;Legacy: a nightly job truncates &lt;code&gt;fact_orders&lt;/code&gt; and reloads from a full ERP extract.&lt;/p&gt;

&lt;p&gt;Redesign: ingest only changed rows into an append-only changelog, then derive the current state per order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Window&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;transforms.api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incremental&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;

&lt;span class="nd"&gt;@incremental&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;semantic_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Sales/clean/orders_current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/Sales/clean/orders_changelog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;new_rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# only rows appended since last build
&lt;/span&gt;    &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;previous&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partitionBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unionByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row_number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;over&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rn = 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-off is visible: the extraction and changelog are incremental, but this current-state dataset is still rewritten each run. For very large tables, teams may partition the current-state output, or have consumers read from the changelog through a view. The decorator style above is the classic API; check your repository template for the syntax your Foundry version uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Schema Evolution
&lt;/h2&gt;

&lt;p&gt;Schema changes cause more migration incidents than transformation logic does, because they often fail silently.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Typical risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Column added&lt;/td&gt;
&lt;td&gt;Low, unless consumers use &lt;code&gt;SELECT *&lt;/code&gt; or positional reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field renamed&lt;/td&gt;
&lt;td&gt;Breaks consumers; can look like a dropped column plus a new null column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type changed&lt;/td&gt;
&lt;td&gt;Silent nulls on failed casts, precision loss, timezone shifts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field removed&lt;/td&gt;
&lt;td&gt;Breaks consumers and business rules that depend on it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nested structure changed&lt;/td&gt;
&lt;td&gt;Breaks flattening logic, often deep inside the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source version upgrade&lt;/td&gt;
&lt;td&gt;Several of the above at once, often undocumented&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Practices that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema contracts.&lt;/strong&gt; Define the canonical output schema explicitly and treat changes to it as reviewed changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit mappings.&lt;/strong&gt; Map source fields to canonical fields by name, never by position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version-aware transforms.&lt;/strong&gt; Handle known source versions deliberately instead of hoping the shape stays stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation before promotion.&lt;/strong&gt; Foundry Data Expectations let a transform fail a build when checks don't pass, for example &lt;code&gt;Check(E.primary_key("order_id"), "orders pk", on_error="FAIL")&lt;/code&gt;. Foundry also supports branching, so schema changes can be built and checked on a branch before merging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer impact analysis.&lt;/strong&gt; Use Foundry's data lineage to see which downstream datasets and applications read a column before you change it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example: a source rename and type change
&lt;/h3&gt;

&lt;p&gt;The ERP upgrade renames &lt;code&gt;cust_id&lt;/code&gt; to &lt;code&gt;customer_id&lt;/code&gt; and changes &lt;code&gt;amount&lt;/code&gt; from string to decimal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;

&lt;span class="n"&gt;CANONICAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DecimalType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimestampType&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;RENAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cust_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# known source v1 -&amp;gt; v2 mapping
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_canonical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RENAMES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumnRenamed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CANONICAL&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Schema contract violated, missing: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CANONICAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One catch: with Spark's default (non-ANSI) settings, a failed cast returns null instead of raising an error. Pair the mapping with a null-rate check on &lt;code&gt;amount&lt;/code&gt;. Also, if the output schema of an incremental transform changes, appending to the existing output will typically conflict. Bumping &lt;code&gt;semantic_version&lt;/code&gt; to force a full rebuild is the cleaner path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconciliation Between Legacy and Foundry Pipelines
&lt;/h2&gt;

&lt;p&gt;Matching row counts proves very little. Two tables can have identical counts while one has duplicated keys, swapped values, or nulls where the other has data.&lt;/p&gt;

&lt;p&gt;Reconcile at several levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Row counts&lt;/td&gt;
&lt;td&gt;Gross load failures, filter differences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key coverage&lt;/td&gt;
&lt;td&gt;Records present in only one output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate detection&lt;/td&gt;
&lt;td&gt;Join fan-out, broken dedup logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate totals&lt;/td&gt;
&lt;td&gt;Sums by date, region, product; rounding drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Null rates per column&lt;/td&gt;
&lt;td&gt;Failed casts, broken mappings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Record-level comparison&lt;/td&gt;
&lt;td&gt;Value differences on matching keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business-rule validation&lt;/td&gt;
&lt;td&gt;Rules that must hold regardless of either pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sample-based review&lt;/td&gt;
&lt;td&gt;Human inspection where full comparison is impractical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A common approach is to ingest the legacy output into Foundry via Data Connection, then compare in a dedicated reconciliation transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;COMPARE_COLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;row_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;null&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COMPARE_COLS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat_ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;foundry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;row_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;legacy_df&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;l&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;row_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foundry_df&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;legacy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foundry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_outer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;only_legacy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;l.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;only_foundry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;l.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;f.h&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalization rules matter more than the comparison itself: decimal scale, timestamp timezones, trailing whitespace, empty strings versus nulls. Agree on documented tolerances up front, and record every accepted difference with a reason. Some mismatches turn out to be legacy bugs, and the business has to decide whether to fix or preserve them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Both Pipelines Without Downtime
&lt;/h2&gt;

&lt;p&gt;"Without downtime" here means consumers keep getting correct data on schedule throughout the migration. Whether cutover itself is invisible depends on your architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dual processing.&lt;/strong&gt; Both pipelines run on the same inputs and schedule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow validation.&lt;/strong&gt; Foundry outputs are reconciled every run, but nobody consumes them yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer-by-consumer migration.&lt;/strong&gt; Move low-risk consumers first, critical ones last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-path switching.&lt;/strong&gt; Give consumers a stable interface, such as a warehouse view, so switching means redirecting the view, not editing every report. Where Foundry needs to feed an external system, Data Connection exports may handle this, depending on the target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback planning.&lt;/strong&gt; Define measurable rollback triggers, like mismatch rate above threshold or a missed freshness SLA, and keep the legacy path warm until the window closes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring.&lt;/strong&gt; Foundry health checks can cover freshness, build status, and schema. Monitor the reconciliation results as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ownership.&lt;/strong&gt; Name who fixes a broken build on each side during the transition. Ambiguity here causes most parallel-run incidents.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Practical Migration Example
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A hypothetical scenario.&lt;/em&gt; A retailer runs a nightly ETL job that pulls customers from a CRM, orders from an ERP, and payments from a payment gateway, then loads a reporting warehouse star schema used by dozens of reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legacy pipeline.&lt;/strong&gt; Full extracts land in staging, stored procedures build dimensions and &lt;code&gt;fact_orders&lt;/code&gt;, and reports read the warehouse directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Foundry pipeline.&lt;/strong&gt; Data Connection syncs land raw data, incremental where the source exposes a reliable change column. Python transforms build changelogs, apply the schema contracts, and produce current-state customer, order, and payment datasets. Data Expectations enforce primary keys and non-negative amounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel validation.&lt;/strong&gt; Both pipelines run nightly through at least one month-end close, since period-end adjustments and late payments often surface edge cases that daily runs don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reconciliation.&lt;/strong&gt; Early runs show value mismatches on &lt;code&gt;amount&lt;/code&gt;. The cause is a legacy procedure that rounds at line level while the Foundry transform rounds at order level. Finance chooses line-level rounding, and the Foundry logic is adjusted to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumer migration.&lt;/strong&gt; Operational dashboards move first through view redirection, then finance reports after a clean month-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legacy retirement.&lt;/strong&gt; Once query logs show no reads from the legacy tables for an agreed period, the jobs are disabled, archived, and eventually removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Migration Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Migrating everything at once,&lt;/strong&gt; which removes the ability to compare and roll back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring downstream dependencies,&lt;/strong&gt; especially consumers of intermediate tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating schema changes as column edits&lt;/strong&gt; instead of contract changes with consumers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validating only row counts.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switching consumers before reconciliation is stable&lt;/strong&gt; across a full business cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rollback criteria,&lt;/strong&gt; so rollback becomes a debate during an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear ownership&lt;/strong&gt; during the parallel period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retiring legacy jobs too early,&lt;/strong&gt; before quarterly or annual processes have run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Palantir Consulting Fits
&lt;/h2&gt;

&lt;p&gt;Some organizations bring in &lt;a href="https://kasadara.com/palantir-consulting/" rel="noopener noreferrer"&gt;Palantir consulting&lt;/a&gt; or implementation partners for specific parts of the program: migration architecture, legacy dependency analysis, incremental pipeline design, schema strategy, reconciliation frameworks, deployment planning, and operational handover. The most useful engagements leave the internal team able to own and extend the pipelines afterward. If you use outside help, make knowledge transfer an explicit deliverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source inventory:&lt;/strong&gt; systems, extraction methods, volumes, change signals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; all consumers, including intermediate-table readers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformation logic:&lt;/strong&gt; documented, including undocumented fixes in legacy code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incremental strategy:&lt;/strong&gt; append versus snapshot, late data, recompute triggers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema compatibility:&lt;/strong&gt; contracts, explicit mappings, versioning rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data quality:&lt;/strong&gt; expectations enforced at build time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciliation:&lt;/strong&gt; multi-level checks with agreed tolerances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel execution:&lt;/strong&gt; same inputs, same schedule, through a full business cycle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; freshness, build health, reconciliation trends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutover:&lt;/strong&gt; consumer order, read-path switching, communication plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback:&lt;/strong&gt; measurable triggers and a warm legacy path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy retirement:&lt;/strong&gt; read-log evidence, archival, decommission date&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Takeaway
&lt;/h2&gt;

&lt;p&gt;Moving ETL to Palantir Foundry is not mainly a code-translation exercise. The real work is controlling correctness, dependencies, and operational risk while the business keeps running. Incremental processing keeps the new pipeline efficient, schema contracts keep it stable, and reconciliation gives you the evidence to cut over. Migrate in slices, compare relentlessly, and retire the old system only when the data says it's safe.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>etl</category>
      <category>palantir</category>
      <category>python</category>
    </item>
    <item>
      <title>Designing a Palantir Foundry Ontology That Holds Up in Production: Object Types, Link Types, and Action Governance</title>
      <dc:creator>Kavi Writes</dc:creator>
      <pubDate>Tue, 29 Sep 2026 10:40:04 +0000</pubDate>
      <link>https://dev.to/kaviwrites/designing-a-palantir-foundry-ontology-that-holds-up-in-production-object-types-link-types-and-5gel</link>
      <guid>https://dev.to/kaviwrites/designing-a-palantir-foundry-ontology-that-holds-up-in-production-object-types-link-types-and-5gel</guid>
      <description>&lt;p&gt;A Foundry pilot usually works. A small team maps a few datasets into the Ontology, builds an operational app, and a planner can finally see open orders next to supplier delays in one place.&lt;/p&gt;

&lt;p&gt;Then the second and third use cases arrive. A Customer object means "billing account" to one team and "legal entity" to another. A link that looked like obvious double-counts shipments. An Action that updates a status field has no clear owner, and nobody can say who changed what last Tuesday, or why.&lt;/p&gt;

&lt;p&gt;These are rarely platform limitations. They are design decisions made quickly during a proof of concept and then inherited by production. This article covers how to design a Palantir Foundry Ontology that survives that transition: Object Types, Link Types, properties, and the Action governance that connects the model to real workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Production-Ready Foundry Ontology Actually Needs
&lt;/h2&gt;

&lt;p&gt;The Foundry Ontology is a semantic layer over your datasets. It maps rows and columns to business concepts through four main building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Object Types:&lt;/strong&gt; the entities people work with, such as a Work Order, Supplier, or Aircraft.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Properties:&lt;/strong&gt; the attributes of those entities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Link Types:&lt;/strong&gt; the relationships between Object Types.&lt;br&gt;
Action Types: defined, permissioned ways to change objects and links.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applications built on Foundry, from operational apps to SDK-based custom software to AI workflows, read and write through this layer. That is what makes it powerful, and it is also why design mistakes spread quickly.&lt;/p&gt;

&lt;p&gt;In a pilot, the Ontology is a convenience. In production, it behaves like a public API with many consumers. It needs stable meaning, named owners, controlled change, secure access, and writes you can audit. The rest of this article is about building those properties in from the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start from what people act on
&lt;/h3&gt;

&lt;p&gt;A useful test for a candidate Object Type: would someone open it, assign it, approve it, or track it? If the answer is no, it is probably a property, a lookup, or a pipeline detail.&lt;/p&gt;

&lt;p&gt;Avoid creating one Object Type per source table. ERP systems often split one business entity across header, item, and status tables. The Ontology should present the entity users recognize, with the joining done upstream in pipelines.&lt;/p&gt;

&lt;p&gt;Granularity takes judgment. Should a Work Order and its individual operations be one Object Type or two? Split them when they have different lifecycles, owners, or access rules. Keep them together when users only ever treat them as one unit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choose primary keys that never move
&lt;/h3&gt;

&lt;p&gt;The primary key is the most consequential decision on an Object Type. Links, user edits, and downstream references all depend on it, so changing it later is expensive.&lt;/p&gt;

&lt;p&gt;A good key is unique, deterministic, and stable across pipeline rebuilds. Avoid row numbers, surrogate IDs that are regenerated with each run, and natural keys that are reused, e.g., reissued badge numbers. When objects come from multiple source systems, prefix the key with the system and site (for example, SAP-DE01-4500012345) to prevent collisions after a merge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be selective with properties
&lt;/h3&gt;

&lt;p&gt;Include the properties people filter on, decide with, or need an AI workflow to reason about. Leave the other 180 source columns in the dataset. Use business names, explicit units, UTC timestamps, and a clear title property so objects are readable in search results and apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Link Types Right
&lt;/h2&gt;

&lt;p&gt;A Link Type connects two Object Types with a defined cardinality. One-to-many links are typically resolved through a foreign key property, while many-to-many links are backed by a join dataset.&lt;/p&gt;

&lt;p&gt;The goal is to model relationships the business talks about, not every foreign key in the source schema. "Supplier supplies Part" is meaningful. "Table A references column B in a staging table" is not. Name both directions clearly, such as Supplied Parts from the Supplier side and Approved Suppliers from the Part side.&lt;/p&gt;

&lt;h3&gt;
  
  
  When a link should become an object
&lt;/h3&gt;

&lt;p&gt;Some relationships carry their own data: effective dates, contract prices, approval status. If you need that, consider modeling the relationship as its own Object Type, like a Supplier Part Approval between Supplier and Part. You pay for an extra hop in queries, but you gain history, a lifecycle, and separate permissions. For a simple association with no attributes, a plain link is the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify cardinality against real data
&lt;/h3&gt;

&lt;p&gt;A link declared one-to-many that is actually many-to-many in the data will not always fail loudly. It can produce misleading counts and incomplete traversals instead. Test cardinality assumptions in the backing pipelines before you publish the link.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Data Model to Operational Model
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data quality belongs upstream
&lt;/h3&gt;

&lt;p&gt;The Ontology reflects whatever its backing datasets contain. Duplicate keys, null foreign keys, and orphaned link targets show up as confusing objects in front of users. Put checks where the data is produced: key uniqueness, required fields, and referential integrity for every link, using Foundry's data health checks and pipeline expectations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ownership and lineage
&lt;/h3&gt;

&lt;p&gt;Give every Object Type a named business owner who decides what it means and a technical owner who maintains its pipeline. Foundry's lineage shows how data flows from source to backing dataset, which helps with impact analysis. Lineage does not explain meaning, though, so write real descriptions for Object Types and properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  Change over time
&lt;/h3&gt;

&lt;p&gt;Decide early whether an Object Type represents current state, history, or both. A common pattern keeps the entity as current state and adds a separate event Object Type, such as Status Change, for history.&lt;/p&gt;

&lt;p&gt;Treat schema changes like API versioning. Adding a property is usually safe. Renaming or removing properties, changing types, or altering a primary key can break apps and SDK consumers. Deprecate first, notify consumers, and use review workflows such as ontology branching or proposals where your environment supports them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Designing for AI applications
&lt;/h3&gt;

&lt;p&gt;AI workflows that reason over the Ontology depend on the same clarity humans do: precise names, useful descriptions, and unambiguous links. When an AI workflow is allowed to propose or make changes, the Action becomes the safety boundary. It should get the same permissions, validation, and audit trail as any human user, not a shortcut around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Actions With Governance in Mind
&lt;/h2&gt;

&lt;p&gt;An Action Type is a parameterized, permissioned transaction against the Ontology. It can create, modify, or delete objects and links, and it can trigger side effects such as notifications or webhooks. Simple Actions are configured with rules; complex ones can be backed by functions. Actions are how the Ontology stops being a read-only view and starts supporting decisions.&lt;/p&gt;

&lt;p&gt;They are also where most production risk lives. Ontology governance that controls reads but not writes is only half done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Actions narrow
&lt;/h3&gt;

&lt;p&gt;A single "Update Work Order" Action with 30 optional fields is hard to secure and harder to audit. Separate Actions like Reassign Work Order and Close Work Order each have one purpose, one set of eligible users, and one clear validation rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five governance questions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;What to define&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Who can submit, separate from who can view. Grant through groups, not individuals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;Submission criteria on the user and parameters, enforced server-side, not just in the UI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;A named owner, a documented purpose, and the expected callers: people, automations, or AI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditability&lt;/td&gt;
&lt;td&gt;Who changed what, when, and why. Add a required justification parameter for sensitive changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval&lt;/td&gt;
&lt;td&gt;Whether a second person must confirm before the change takes effect.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Approvals need explicit design
&lt;/h3&gt;

&lt;p&gt;For changes that need sign-off, a portable pattern is a request object with a status, plus a separate approval Action limited to approvers. Submission criteria can block self-approval by checking that the approver is not the requester. If your environment offers a native approval capability, evaluate it against this pattern, but make the approval path visible in the model either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Enterprise Example
&lt;/h2&gt;

&lt;p&gt;Consider a hypothetical manufacturer that wants to manage supplier quality problems across several plants. Today, incidents live in spreadsheets, and putting a supplier on hold means an email thread with procurement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Object Types and Link Types
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supplier&lt;/strong&gt;, keyed by a global vendor ID, not a per-plant ERP code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part&lt;/strong&gt;, linked many-to-many to Supplier through an approved supplier list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plant&lt;/strong&gt;, the site where incidents are raised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality Incident&lt;/strong&gt;, linked to one Part, one Plant, and one Supplier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supplier Hold Request&lt;/strong&gt;, linked to one Supplier and to the incidents that justify it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Actions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Who can submit&lt;/th&gt;
&lt;th&gt;Key validation&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Log Quality Incident&lt;/td&gt;
&lt;td&gt;Plant quality engineers&lt;/td&gt;
&lt;td&gt;Part is active at the selected Plant; severity required&lt;/td&gt;
&lt;td&gt;Creates an incident and its links&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request Supplier Hold&lt;/td&gt;
&lt;td&gt;Quality leads&lt;/td&gt;
&lt;td&gt;At least one linked incident; justification required&lt;/td&gt;
&lt;td&gt;Creates a request with status &lt;em&gt;Pending&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approve Supplier Hold&lt;/td&gt;
&lt;td&gt;Procurement leads&lt;/td&gt;
&lt;td&gt;Approver is not the requester; request is &lt;em&gt;Pending&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Sets the Supplier's hold status and notifies buyers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release Supplier Hold&lt;/td&gt;
&lt;td&gt;Procurement leads&lt;/td&gt;
&lt;td&gt;Supplier is currently on hold; reason required&lt;/td&gt;
&lt;td&gt;Clears the hold and records the release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An AI assistant could summarize open incidents for a supplier and draft a hold request. Only a procurement lead can submit the approval, so the most consequential step stays with a human.&lt;/p&gt;

&lt;p&gt;One decision remains outside the Ontology: which system owns the hold status. If the ERP blocks purchase orders, the approved hold must reach it, for example through a webhook side effect or an outbound sync. Define the system of record explicitly, or two systems will disagree about whether a supplier is blocked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Ontology Design Mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mirroring the source schema&lt;/strong&gt;. The Ontology becomes a harder-to-query copy of the ERP instead of a model of the business.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstable primary keys&lt;/strong&gt;. Keys that shift on rebuild break links and orphan user edits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Catch-all Object Types&lt;/strong&gt;. A single Asset type covering vehicles, buildings, and software licenses forces dozens of mostly-null properties and muddled permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generic write Actions&lt;/strong&gt;. "Edit Object" style Actions make validation and audit nearly meaningless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governing reads but not writes&lt;/strong&gt;. Row-level access is carefully designed, while any editor can submit any Action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No owners or descriptions&lt;/strong&gt;. Six months later, nobody knows whether status means ERP status or operational status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Using the Ontology as a reporting layer&lt;/strong&gt;. Heavy aggregations usually belong in datasets and analytics tools, with the Ontology focused on entities people act on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking changes without a consumer inventory&lt;/strong&gt;. Renaming a property can quietly break apps, automations, and SDK clients you did not know existed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Production Readiness Checklist
&lt;/h2&gt;

&lt;p&gt;Use this before promoting an Object Type, Link Type, or Action to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each Object Type represents something users open, assign, approve, or track.&lt;/li&gt;
&lt;li&gt;Primary keys are unique, deterministic, and stable across rebuilds and source systems.&lt;/li&gt;
&lt;li&gt;Properties include explicit units and business names. Timezones are all the same.&lt;/li&gt;
&lt;li&gt;The link cardinality is tested on real data and both directions are explicitly stated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Data and ownership&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backing datasets have health checks for key uniqueness, required fields, and link integrity.&lt;/li&gt;
&lt;li&gt;Every Object Type has a business owner, a technical owner, and a written description.&lt;/li&gt;
&lt;li&gt;History requirements are decided: current state, event objects, or both.&lt;/li&gt;
&lt;li&gt;Consumers are known, and there is a deprecation process for breaking changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Actions&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each Action has a single purpose and a named owner.&lt;/li&gt;
&lt;li&gt;Submission permissions are group-based and distinct from read access.&lt;/li&gt;
&lt;li&gt;Validation is performed server-side via submission criteria or function logic.&lt;/li&gt;
&lt;li&gt;Sensitive Actions require a justification and, where needed, a second approver.&lt;/li&gt;
&lt;li&gt;AI-initiated changes go through the same Actions and controls as human changes.&lt;/li&gt;
&lt;li&gt;The system of record is defined for any value that is written back to external systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Takeaways
&lt;/h2&gt;

&lt;p&gt;A production Foundry Ontology is a contract, not a diagram. Object Types should reflect the entities people act on, Link Types should capture relationships the business actually uses, and primary keys should be treated as permanent from day one.&lt;/p&gt;

&lt;p&gt;Actions deserve the most care, because they are where the model changes the business. Keep them narrow, permission them deliberately, validate server-side, and make approvals and audit trails explicit.&lt;/p&gt;

&lt;p&gt;None of these rules is absolute. Granularity, history, and the choice between a link and an intermediate object depend on your workflows. Whether the work is done by an internal platform team or with a &lt;a href="https://kasadara.com/palantir-consulting/" rel="noopener noreferrer"&gt;Palantir consulting partner&lt;/a&gt;, the useful test is the same: can a new team understand, trust, and safely change this Ontology without asking the people who built it?&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>ai</category>
      <category>palantir</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
