<?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>Building External Applications on Palantir Foundry with the Ontology SDK and TypeScript</title>
      <dc:creator>Kavi Writes</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:05:50 +0000</pubDate>
      <link>https://dev.to/kaviwrites/building-external-applications-on-palantir-foundry-with-the-ontology-sdk-and-typescript-2na4</link>
      <guid>https://dev.to/kaviwrites/building-external-applications-on-palantir-foundry-with-the-ontology-sdk-and-typescript-2na4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Many enterprise applications run into the same problem: the data is somewhere, the business logic is somewhere else, and access rules are re-implemented in every new app. Building on a governed data platform moves that work to one place. The application asks for business objects, and the platform decides what the caller is allowed to see and change.&lt;/p&gt;

&lt;p&gt;Palantir Foundry approaches this through its Ontology, a model of business entities (orders, customers, assets, shipments), the relationships between them, and the operations people are allowed to perform on them. The Ontology SDK (OSDK) exposes that model to code. With the TypeScript OSDK, an application that runs outside Foundry can query objects, follow relationships, and apply actions with typed code instead of hand-built HTTP calls.&lt;/p&gt;

&lt;p&gt;This article covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the Ontology is and which of its parts an external app works with.&lt;/li&gt;
&lt;li&gt;When an external application makes sense, and when a Foundry-native app is the better choice.&lt;/li&gt;
&lt;li&gt;How the TypeScript OSDK packages fit together and how authentication works.&lt;/li&gt;
&lt;li&gt;A small server-side example that reads Ontology objects with filtering and pagination.&lt;/li&gt;
&lt;li&gt;The security, governance, and production concerns that matter once the demo works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All code follows the patterns in Palantir's public documentation for TypeScript OSDK 2.x. Object type names, property names, and package names are placeholders. Your Foundry environment will generate its own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope note:&lt;/strong&gt; Foundry enrollments differ in enabled features, network restrictions, and administrator settings. Treat this article as a guide to the documented model, and confirm details in your stack's Developer Console documentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Understanding Foundry and the Ontology
&lt;/h2&gt;

&lt;p&gt;Foundry is Palantir's data platform. It integrates datasets, virtual tables, and models from source systems. Palantir's documentation describes the &lt;a href="https://www.palantir.com/docs/foundry/ontology/overview/" rel="noopener noreferrer"&gt;Ontology&lt;/a&gt; as an operational layer on top of those assets: it maps raw data to real-world things such as plants, equipment, customer orders, or financial transactions.&lt;/p&gt;

&lt;p&gt;The Ontology has two kinds of building blocks. The &lt;em&gt;semantic&lt;/em&gt; elements describe what exists. The &lt;em&gt;kinetic&lt;/em&gt; elements describe what can change and who can change it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Supply-chain example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.palantir.com/docs/foundry/object-link-types/object-types-overview/" rel="noopener noreferrer"&gt;Object type&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A schema for a real-world entity, backed by one or more data sources&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Customer&lt;/code&gt;, &lt;code&gt;Shipment&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Property&lt;/td&gt;
&lt;td&gt;A typed field on an object type&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;order.status&lt;/code&gt;, &lt;code&gt;order.promisedDate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.palantir.com/docs/foundry/object-link-types/link-types-overview/" rel="noopener noreferrer"&gt;Link type&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A relationship between object types&lt;/td&gt;
&lt;td&gt;An &lt;code&gt;Order&lt;/code&gt; belongs to one &lt;code&gt;Customer&lt;/code&gt;; it has many &lt;code&gt;Shipment&lt;/code&gt; objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.palantir.com/docs/foundry/action-types/overview/" rel="noopener noreferrer"&gt;Action type&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A governed operation that edits objects, with parameters, rules, and submission criteria&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Reschedule Shipment&lt;/code&gt;, &lt;code&gt;Flag Order At Risk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.palantir.com/docs/foundry/functions/overview/" rel="noopener noreferrer"&gt;Function&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Business logic authored in code and published to the Ontology&lt;/td&gt;
&lt;td&gt;A function that scores late-delivery risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.palantir.com/docs/foundry/interfaces/interface-overview/" rel="noopener noreferrer"&gt;Interface&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A shared shape that several object types can implement&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;Trackable&lt;/code&gt; interface implemented by &lt;code&gt;Shipment&lt;/code&gt; and &lt;code&gt;Container&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For an application developer, the important point is this: you do not query the warehouse tables behind &lt;code&gt;Order&lt;/code&gt;. You query the &lt;code&gt;Order&lt;/code&gt; object type. Its properties, links, and permissions are defined once in Foundry and apply to every consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why Build an External Application?
&lt;/h2&gt;

&lt;p&gt;Build an external application when the users, the runtime, or the integration needs sit outside what Foundry's own app builders cover. Typical cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal business portals&lt;/strong&gt; that combine Ontology data with systems Foundry does not own, such as an ERP screen or a ticketing tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational dashboards&lt;/strong&gt; with a custom UX or design system that a low-code builder cannot reproduce.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow interfaces&lt;/strong&gt; where planners or field staff apply governed actions ("reschedule this shipment") from a tool they already use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend services&lt;/strong&gt; such as schedulers or integration jobs that read Ontology objects and push results to other systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not always the right choice. Foundry's own tools, such as Workshop, already sit on the Ontology and need no separate auth, hosting, or release pipeline. If the requirement is a standard operational screen for Foundry users, start there.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Where code runs&lt;/th&gt;
&lt;th&gt;Server-side logic&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foundry-native app (for example, Workshop)&lt;/td&gt;
&lt;td&gt;Inside Foundry&lt;/td&gt;
&lt;td&gt;Uses Foundry functions and actions&lt;/td&gt;
&lt;td&gt;Standard operational UIs for Foundry users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSDK app &lt;a href="https://www.palantir.com/docs/foundry/developer-console/deploy-custom-application-on-foundry/" rel="noopener noreferrer"&gt;hosted on Foundry&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Static assets served by Foundry; code runs in the browser&lt;/td&gt;
&lt;td&gt;Not supported by the hosting feature&lt;/td&gt;
&lt;td&gt;Custom single-page apps for Foundry users, no extra infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSDK app on your own infrastructure&lt;/td&gt;
&lt;td&gt;Your servers, cloud, or containers&lt;/td&gt;
&lt;td&gt;Yes, under your control&lt;/td&gt;
&lt;td&gt;Integration with other systems, backend jobs, custom runtimes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Palantir's hosting documentation is explicit that hosted sites serve static files only (HTML, CSS, JavaScript, and assets). Node.js backends, Python servers, and server-side rendering are not supported there. If you need any of those, you are building an external application in the full sense of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Understanding the Ontology SDK and TypeScript
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/overview/" rel="noopener noreferrer"&gt;Ontology SDK&lt;/a&gt; is generated per application, not downloaded as one universal library. In Foundry's Developer Console, you create an application, select the object types, action types, and functions it may use, and generate an SDK version. Palantir distributes the TypeScript output through npm, Python through pip or Conda, Java through Maven, and other languages through an OpenAPI spec.&lt;/p&gt;

&lt;p&gt;A TypeScript OSDK project normally combines these packages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generated package (for example, &lt;code&gt;@your-app/sdk&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Your Foundry stack's npm registry&lt;/td&gt;
&lt;td&gt;Typed definitions for the object types, actions, and functions you selected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@osdk/client&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public npm&lt;/td&gt;
&lt;td&gt;Runtime client: &lt;code&gt;createClient&lt;/code&gt;, queries, actions, the &lt;code&gt;Osdk&lt;/code&gt; types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@osdk/oauth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public npm&lt;/td&gt;
&lt;td&gt;Token providers: &lt;code&gt;createPublicOauthClient&lt;/code&gt; and &lt;code&gt;createConfidentialOauthClient&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@osdk/foundry.*&lt;/code&gt; (optional)&lt;/td&gt;
&lt;td&gt;Public npm&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/palantir/foundry-platform-typescript" rel="noopener noreferrer"&gt;Platform SDK&lt;/a&gt; bindings for non-Ontology Foundry APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since TypeScript OSDK 2.0, the runtime client is separate from the generated code. Palantir's &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk-migration/" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; says this lets you update the client library without regenerating the SDK, and that newer features such as interfaces and media sets require 2.0.&lt;/p&gt;

&lt;p&gt;TypeScript adds real value here. Because types come from your Ontology's metadata, a misspelled property name or an unsupported aggregation metric fails at compile time, not in production. The &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk/" rel="noopener noreferrer"&gt;TypeScript OSDK reference&lt;/a&gt; notes limits to this: some checks use a property's base type, so a few invalid queries on array properties still compile and fail at runtime. Types reduce errors; they do not replace tests.&lt;/p&gt;

&lt;p&gt;Two rules keep you out of trouble:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the docs for your SDK version.&lt;/strong&gt; Developer Console generates documentation for your specific Ontology and has a version picker. Public examples target 2.x.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat each SDK version as an API contract.&lt;/strong&gt; Regenerating after an Ontology change can rename or remove types your code uses.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  4. Architecture and Integration Flow
&lt;/h2&gt;

&lt;p&gt;For enterprise integrations, a server-mediated design is a sound default. That is a recommendation, not a platform requirement. The diagram shows that pattern, with the browser-only alternative as a dotted line.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  U["End user"] --&amp;gt; UI["Web or mobile UI"]
  UI --&amp;gt;|"app session"| BE["Your backend service"]
  BE --&amp;gt;|"validated, authorized request"| SDK["OSDK client&amp;lt;br/&amp;gt;@osdk/client + generated SDK"]
  SDK --&amp;gt;|"token request"| AUTH["Foundry OAuth"]
  SDK --&amp;gt;|"typed queries and actions"| API["Foundry Ontology APIs"]
  API --&amp;gt; ONT["Object types, links,&amp;lt;br/&amp;gt;action types, functions"]
  ONT --&amp;gt; DATA["Backing datasets"]
  UI -. "browser-only option:&amp;lt;br/&amp;gt;public client, user permissions" .-&amp;gt; API&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each component has one job:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External TypeScript UI&lt;/td&gt;
&lt;td&gt;Presentation and user interaction. Holds no secrets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend service&lt;/td&gt;
&lt;td&gt;Authenticates your users, validates input, applies app-level authorization, holds the OAuth client secret, shapes responses.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth (Foundry)&lt;/td&gt;
&lt;td&gt;Issues tokens: client credentials for a service user, or authorization code for a signed-in Foundry user.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSDK client&lt;/td&gt;
&lt;td&gt;Builds typed requests, handles paging, surfaces structured errors.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ontology&lt;/td&gt;
&lt;td&gt;Enforces resource restrictions, object permissions, and action rules, then reads or writes the backing data.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A typical read follows six steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user opens your app and authenticates with your identity provider.&lt;/li&gt;
&lt;li&gt;The UI calls your backend with an app session.&lt;/li&gt;
&lt;li&gt;The backend validates the input and checks that this user may request this data.&lt;/li&gt;
&lt;li&gt;The OSDK client obtains or reuses a Foundry token.&lt;/li&gt;
&lt;li&gt;Foundry checks the token's resource scope and the caller's permissions, then runs the query.&lt;/li&gt;
&lt;li&gt;The backend maps the result to a response shape and returns it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The alternatives are legitimate. A &lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-add-to-existing-typescript/" rel="noopener noreferrer"&gt;browser-only app&lt;/a&gt; using a public OAuth client calls Foundry directly under the user's own permissions, with no backend to run. An application can also combine a client-facing and a backend type. Palantir also documents running OSDK code inside Foundry through &lt;a href="https://www.palantir.com/docs/foundry/compute-modules/osdk-integration/" rel="noopener noreferrer"&gt;compute modules&lt;/a&gt;. Choose based on where secrets can live and whose permissions should apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Setting Up the Development Environment
&lt;/h2&gt;

&lt;p&gt;The example in this article is a server-side Node.js service that uses application permissions. The setup follows Palantir's guide to &lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-bootstrapping-server-side-typescript/" rel="noopener noreferrer"&gt;bootstrapping a back-end TypeScript application&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Access to a Foundry enrollment with Developer Console, and permission to create applications. Palantir's &lt;a href="https://www.palantir.com/docs/foundry/developer-console/permissions/" rel="noopener noreferrer"&gt;permissions page&lt;/a&gt; lists the roles involved, including the third-party application administrator workflow your organization may control.&lt;/li&gt;
&lt;li&gt;An object type to read. This article assumes an &lt;code&gt;Order&lt;/code&gt; object type with &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;promisedDate&lt;/code&gt; properties. Substitute your own.&lt;/li&gt;
&lt;li&gt;Node.js. Palantir's guides state Node 18 or higher. Node 18 and 20 have reached end of life, so use a currently supported LTS release.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the Developer Console application
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a new application. Choose &lt;strong&gt;Backend service&lt;/strong&gt; as the application type and &lt;strong&gt;Application's permissions&lt;/strong&gt; as the permission type.&lt;/li&gt;
&lt;li&gt;On the resources page, choose to generate an Ontology SDK, select your Ontology, and add only the object types and action types this service needs. The Ontology cannot be changed after generation.&lt;/li&gt;
&lt;li&gt;Copy the client secret when it is shown. Developer Console displays it once; losing it means rotating it, which breaks running deployments.&lt;/li&gt;
&lt;li&gt;Grant the generated service user read access to the selected object types. Without this, queries fail or return nothing.&lt;/li&gt;
&lt;li&gt;Generate the first SDK version and note the package name and registry URL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Install packages
&lt;/h3&gt;

&lt;p&gt;Configure npm to resolve your generated package from Foundry's registry. Keep the token in an environment variable, never in the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# .npmrc
&lt;/span&gt;&lt;span class="na"&gt;//&amp;lt;REGISTRY-URL-FROM-OVERVIEW-PAGE&amp;gt;:&lt;/span&gt;&lt;span class="py"&gt;_authToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${FOUNDRY_TOKEN}&lt;/span&gt;
&lt;span class="na"&gt;&amp;lt;PACKAGE-NAME&amp;gt;:&lt;/span&gt;&lt;span class="py"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;REGISTRY-URL-FROM-OVERVIEW-PAGE&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FOUNDRY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;token-from-developer-console&amp;gt;   &lt;span class="c"&gt;# not committed&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;PACKAGE-NAME&amp;gt; @osdk/client @osdk/oauth
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; typescript @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Palantir's guides use &lt;code&gt;@latest&lt;/code&gt; tags. For anything beyond a prototype, commit &lt;code&gt;package-lock.json&lt;/code&gt; and upgrade deliberately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser apps differ
&lt;/h3&gt;

&lt;p&gt;A browser application cannot keep a secret. Palantir's docs require client-facing applications to use &lt;strong&gt;user permissions&lt;/strong&gt; with the authorization code grant, through &lt;code&gt;createPublicOauthClient&lt;/code&gt; and a registered redirect URL. For a browser project, start with &lt;code&gt;npm create @osdk/app@latest&lt;/code&gt; as shown in the &lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-bootstrapping-typescript/" rel="noopener noreferrer"&gt;TypeScript bootstrap guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Building a Practical TypeScript Example
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; a logistics team's integration service needs the list of at-risk orders for one region, oldest promise date first, so it can push them into an external ticketing tool. The service reads through a service user, pages through results, and never exposes raw Ontology objects to callers.&lt;/p&gt;

&lt;p&gt;The API calls below (&lt;code&gt;createConfidentialOauthClient&lt;/code&gt;, &lt;code&gt;createClient&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;fetchPage&lt;/code&gt;, &lt;code&gt;PalantirApiError&lt;/code&gt;) come from Palantir's documentation. &lt;code&gt;Order&lt;/code&gt;, its properties, the &lt;code&gt;"AT_RISK"&lt;/code&gt; value, and &lt;code&gt;@your-app/sdk&lt;/code&gt; are placeholders for your generated SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order-risk-service/
├── .env            # secrets, git-ignored
├── .npmrc          # registry config, token via env var
├── package.json    # "type": "module"
├── tsconfig.json
└── src/
    ├── config.ts   # validated environment
    ├── foundry.ts  # auth + OSDK client
    ├── orders.ts   # query logic
    └── index.ts    # entry point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NodeNext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"noUncheckedIndexedAccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configuration: fail fast, never hard-code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required environment variable: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;foundryUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOUNDRY_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;foundryUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOUNDRY_URL must use https&lt;/span&gt;&lt;span class="dl"&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;foundryUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;foundryUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ontologyRid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ONTOLOGY_RID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOUNDRY_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FOUNDRY_CLIENT_SECRET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;as const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auth and client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/foundry.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@osdk/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createConfidentialOauthClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@osdk/oauth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./config.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConfidentialOauthClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientSecret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundryUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foundryUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ontologyRid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client takes three things: the stack URL, the Ontology RID (a resource identifier, shown in Developer Console), and a token provider. Create it once per process and reuse it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query with filtering and pagination
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/orders.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@your-app/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// replace with your generated package&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./foundry.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REGION_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{2,8}&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PAGE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AtRiskOrder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;promisedDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AtRiskOrderPage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AtRiskOrder&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;nextPageToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAtRiskOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AtRiskOrderPage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;REGION_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid region code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;MAX_PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;$and&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AT_RISK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&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;fetchPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;$select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promisedDate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;$orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;promisedDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;$pageSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;$nextPageToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$primaryKey&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;promisedDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promisedDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="na"&gt;nextPageToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPageToken&lt;/span&gt;&lt;span class="p"&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;h3&gt;
  
  
  Entry point and error handling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PalantirApiError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@osdk/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./foundry.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getAtRiskOrders&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./orders.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_PAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// demo guard against unbounded loops&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EMEA&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// as in Palantir's back-end guide&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAtRiskOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\t&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\t&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promisedDate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;pageToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPageToken&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;pages&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageToken&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_PAGES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;PalantirApiError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log identifiers, not parameters: parameters can contain business data.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Foundry API error &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorName&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s2"&gt;`(status &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, instance &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorInstanceId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;n/a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&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;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unknown error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What the important parts do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;where&lt;/code&gt; + &lt;code&gt;$and&lt;/code&gt;&lt;/strong&gt; compiles to a server-side filter. Only matching objects cross the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$select&lt;/code&gt;&lt;/strong&gt; limits returned properties. The primary key and object metadata still come back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$pageSize&lt;/code&gt; and &lt;code&gt;$nextPageToken&lt;/code&gt;&lt;/strong&gt; implement cursor pagination. &lt;code&gt;nextPageToken&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; on the last page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The DTO mapping&lt;/strong&gt; decouples callers from generated types and avoids passing through properties nobody asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PalantirApiError&lt;/code&gt;&lt;/strong&gt; carries &lt;code&gt;errorName&lt;/code&gt;, &lt;code&gt;statusCode&lt;/code&gt;, and &lt;code&gt;errorInstanceId&lt;/code&gt;, per the &lt;a href="https://palantir.github.io/osdk-ts/react/platform-apis" rel="noopener noreferrer"&gt;osdk-ts docs&lt;/a&gt;. The instance ID is what Palantir support asks for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For long-running services, follow the token-refresh guidance in your Developer Console documentation rather than relying on a single &lt;code&gt;signIn()&lt;/code&gt; at startup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run and test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# package.json scripts: "build": "tsc", "start": "node --env-file=.env dist/index.js"&lt;/span&gt;
npm run build
npm start &lt;span class="nt"&gt;--&lt;/span&gt; EMEA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected behavior: one tab-separated line per at-risk order, oldest promise date first, at most 250 rows. Then test the failure paths on purpose: a wrong client secret should produce an auth error, a lowercase region a &lt;code&gt;RangeError&lt;/code&gt;, and an object type the service user cannot read should produce an error or an empty result. Do not treat "no rows" as proof that no data exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extending to actions
&lt;/h3&gt;

&lt;p&gt;Writes go through action types, not direct edits. If you add an action type (here, a placeholder &lt;code&gt;flagOrderAtRisk&lt;/code&gt;) to the SDK, the documented pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flagOrderAtRisk&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;applyAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ORD-1001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Carrier delay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$returnEdits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// handle a rejected submission&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action's submission criteria still run on the server, for the service user.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Authentication, Security, and Governance
&lt;/h2&gt;

&lt;p&gt;Authentication proves who is calling. Authorization decides what that caller may read or change. A valid OAuth token settles the first question only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pick the permission model first
&lt;/h3&gt;

&lt;p&gt;Developer Console offers &lt;a href="https://www.palantir.com/docs/foundry/developer-console/permissions/" rel="noopener noreferrer"&gt;two permission types&lt;/a&gt;, and the choice shapes your whole design:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;User permissions&lt;/th&gt;
&lt;th&gt;Application permissions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OAuth flow&lt;/td&gt;
&lt;td&gt;Authorization code grant&lt;/td&gt;
&lt;td&gt;Client credentials grant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whose access applies&lt;/td&gt;
&lt;td&gt;The signed-in Foundry user&lt;/td&gt;
&lt;td&gt;A generated service user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSDK helper&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createPublicOauthClient&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;createConfidentialOauthClient&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed in browser apps&lt;/td&gt;
&lt;td&gt;Yes (required for client-facing apps)&lt;/td&gt;
&lt;td&gt;No: needs a secret, so server-side only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An application can use both, for example user permissions for most data plus a service user for a reference data set every user may see.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Foundry enforces, and what it does not
&lt;/h3&gt;

&lt;p&gt;Foundry applies two layers on every OSDK request. First, the token is limited to the resources selected for the application, called resource access restrictions. Second, the caller's own permissions apply, including object security policies and restricted views.&lt;/p&gt;

&lt;p&gt;Palantir's &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/overview/" rel="noopener noreferrer"&gt;OSDK overview&lt;/a&gt; states the limit plainly: these controls filter what can be &lt;em&gt;read&lt;/em&gt;. They do not follow the response payload or anything your application does with it. This matters most with application permissions. If a service user can read every &lt;code&gt;Order&lt;/code&gt; and your backend serves those orders to any logged-in portal user, Foundry is no longer deciding who sees what. Your backend is.&lt;/p&gt;

&lt;p&gt;Practical rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer user permissions&lt;/strong&gt; when end users see the data, so Foundry's policies apply per person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you use a service user&lt;/strong&gt;, implement your own authorization layer, and do not share caches across users with different entitlements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grant least privilege twice:&lt;/strong&gt; select only the object types, actions, and functions the app needs in its SDK, and give the service user only the read and action rights it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep secrets server-side&lt;/strong&gt; in a secret manager or a git-ignored &lt;code&gt;.env&lt;/code&gt; for local work. Rotating a secret in Developer Console breaks running deployments, so treat rotation as a planned release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate input before it reaches a query or action.&lt;/strong&gt; The OSDK builds structured filters rather than query strings, but an unvalidated parameter can still widen a filter or submit a bad action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind downstream copies.&lt;/strong&gt; Data exported to logs, caches, or other systems leaves Foundry's controls. Palantir points to markings and classification-based access controls for data that must stay protected as it flows on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Common Challenges and Best Practices
&lt;/h2&gt;

&lt;p&gt;Each row below is either documented by Palantir or follows directly from documented behavior. Check configuration and permissions before debugging code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;npm install&lt;/code&gt; returns 404 for the generated package&lt;/td&gt;
&lt;td&gt;npm resolves from another registry&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;npm config delete registry&lt;/code&gt;, check every &lt;code&gt;.npmrc&lt;/code&gt;, confirm the registry URL and &lt;code&gt;FOUNDRY_TOKEN&lt;/code&gt; (&lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-add-to-existing-typescript/" rel="noopener noreferrer"&gt;guide&lt;/a&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generated code imports &lt;code&gt;FoundryClient&lt;/code&gt; or &lt;code&gt;PublicClientAuth&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SDK was generated with the 1.x generator&lt;/td&gt;
&lt;td&gt;Generate a new SDK version with the TypeScript OSDK 2.x generator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth works but queries fail or return nothing&lt;/td&gt;
&lt;td&gt;Service user or end user lacks read access, or a resource is missing from the app's access restrictions&lt;/td&gt;
&lt;td&gt;Check grants; act on Developer Console's "outdated dependencies" warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Old clients break after a new SDK version&lt;/td&gt;
&lt;td&gt;A removed resource was also dropped from access restrictions&lt;/td&gt;
&lt;td&gt;Keep removed resources in restrictions until every client has upgraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ObjectsExceededLimit&lt;/code&gt; error&lt;/td&gt;
&lt;td&gt;Object type backed by Object Storage v1 (10,000-result cap), or an aggregation over 10,000 results&lt;/td&gt;
&lt;td&gt;Filter on the server, aggregate instead of listing, review the object type's backing store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate or missing rows across pages&lt;/td&gt;
&lt;td&gt;Data changed between page requests&lt;/td&gt;
&lt;td&gt;Use the documented &lt;code&gt;$snapshot&lt;/code&gt; fetch option when consistency matters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS errors behind a corporate proxy&lt;/td&gt;
&lt;td&gt;Node does not trust the proxy's CA&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;NODE_EXTRA_CA_CERTS&lt;/code&gt;; never disable certificate verification&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Beyond the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Avoid N+1 link traversal.&lt;/strong&gt; Calling &lt;code&gt;$link&lt;/code&gt; per object in a loop multiplies requests. &lt;code&gt;pivotTo()&lt;/code&gt; traverses links on a whole object set, and &lt;code&gt;aggregate()&lt;/code&gt; computes counts and averages on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry reads, not writes.&lt;/strong&gt; Retry idempotent queries on transient errors with backoff and a cap. Do not blindly retry &lt;code&gt;applyAction&lt;/code&gt;; a retry can apply an edit twice unless the action is designed to be idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log identifiers, not data.&lt;/strong&gt; &lt;code&gt;errorInstanceId&lt;/code&gt; is enough for support. Never log tokens, secrets, or object payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respect the type system's edges.&lt;/strong&gt; The &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk/" rel="noopener noreferrer"&gt;TypeScript OSDK reference&lt;/a&gt; notes that aggregation results such as &lt;code&gt;avg&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, and &lt;code&gt;max&lt;/code&gt; are &lt;code&gt;undefined&lt;/code&gt; when nothing matches. Handle that branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test at two levels.&lt;/strong&gt; Unit-test your mapping and validation with stubbed data, and keep a small integration suite against a non-production Ontology. Palantir publishes guides for &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk-testing/" rel="noopener noreferrer"&gt;unit testing&lt;/a&gt; and &lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk-integration-testing/" rel="noopener noreferrer"&gt;integration testing&lt;/a&gt; TypeScript OSDK code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. How This Fits into Enterprise Data and AI Transformation
&lt;/h2&gt;

&lt;p&gt;An Ontology-backed external application connects three things that usually live apart: business context (what an &lt;code&gt;Order&lt;/code&gt; is and how it relates to a &lt;code&gt;Shipment&lt;/code&gt;), operational workflow (who may reschedule it and under what rules), and the tools people already use. The OSDK is the contract between them. When the Ontology changes, you regenerate the SDK and the compiler shows where your application is affected.&lt;/p&gt;

&lt;p&gt;The same governed layer serves AI-enabled processes where your enrollment supports them. The TypeScript OSDK documents vector similarity search through &lt;code&gt;nearestNeighbors()&lt;/code&gt; for object types with embedding properties. Palantir's &lt;a href="https://www.palantir.com/docs/foundry/ontology-mcp/overview/" rel="noopener noreferrer"&gt;Ontology MCP&lt;/a&gt; exposes an application's object types, actions, and query functions as Model Context Protocol tools for external AI agents, with application restrictions limiting what an agent can do. Palantir's documentation also notes that sending Ontology data to LLMs hosted outside its AI Platform must comply with your organization's policies. That decision belongs to governance, not to the developer alone.&lt;/p&gt;

&lt;p&gt;None of this makes transformation automatic. An SDK cannot fix an Ontology that models the business poorly, an unclear ownership model for action types, or a permission design nobody reviewed. Those decisions need as much attention as the code.&lt;/p&gt;

&lt;p&gt;This is also where &lt;strong&gt;&lt;a href="https://kasadara.com/palantir-consulting/" rel="noopener noreferrer"&gt;Palantir consulting&lt;/a&gt;&lt;/strong&gt; work typically contributes, whether delivered by Palantir's own teams or by independent implementation partners. Useful contributions are concrete: Ontology design reviews before SDK generation, choosing between user and application permissions, integration architecture with existing systems, permission and marking design, and production-readiness reviews covering secrets, observability, and SDK upgrade policy. Evaluate any provider on those specifics and on evidence from comparable work.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Conclusion
&lt;/h2&gt;

&lt;p&gt;An external TypeScript application talks to Foundry through a generated, typed SDK scoped to the Ontology resources you select. The code is the small part: &lt;code&gt;createClient&lt;/code&gt;, a token provider, and object set queries such as &lt;code&gt;where&lt;/code&gt; and &lt;code&gt;fetchPage&lt;/code&gt;. The decisions that matter come earlier. Choose user or application permissions, select the minimum resources, decide who is responsible for authorization once data leaves Foundry, and decide whether a Foundry-native tool would serve the need with less to operate.&lt;/p&gt;

&lt;p&gt;A realistic next step: in a non-production Foundry environment, create a Backend service application with one read-only object type, run the example above against it, and then test the failure paths on purpose. Once that works, decide between user and application permissions for your real use case before writing more code.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted with AI assistance. The author reviewed the technical content against the linked documentation before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Official Palantir documentation consulted for this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/ontology/overview/" rel="noopener noreferrer"&gt;Ontology building: Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/overview/" rel="noopener noreferrer"&gt;Ontology SDK (OSDK): Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk/" rel="noopener noreferrer"&gt;TypeScript OSDK reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/ontology-sdk/typescript-osdk-migration/" rel="noopener noreferrer"&gt;TypeScript OSDK migration guide (1.x to 2.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/developer-console/permissions/" rel="noopener noreferrer"&gt;Developer Console: Permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-bootstrapping-server-side-typescript/" rel="noopener noreferrer"&gt;Bootstrap a back-end TypeScript application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-bootstrapping-typescript/" rel="noopener noreferrer"&gt;Bootstrap a TypeScript application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/developer-console/how-to-add-to-existing-typescript/" rel="noopener noreferrer"&gt;Add OSDK to an existing application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/developer-console/deploy-custom-application-on-foundry/" rel="noopener noreferrer"&gt;Host an OSDK application on Foundry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.palantir.com/docs/foundry/ontology-mcp/overview/" rel="noopener noreferrer"&gt;Ontology MCP: Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://palantir.github.io/osdk-ts/react/platform-apis" rel="noopener noreferrer"&gt;osdk-ts docs: Handling errors with PalantirApiError&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>tutorial</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <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>
