Introduction
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.
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.
This article covers:
- What the Ontology is and which of its parts an external app works with.
- When an external application makes sense, and when a Foundry-native app is the better choice.
- How the TypeScript OSDK packages fit together and how authentication works.
- A small server-side example that reads Ontology objects with filtering and pagination.
- The security, governance, and production concerns that matter once the demo works.
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.
Scope note: 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.
1. Understanding Foundry and the Ontology
Foundry is Palantir's data platform. It integrates datasets, virtual tables, and models from source systems. Palantir's documentation describes the Ontology 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.
The Ontology has two kinds of building blocks. The semantic elements describe what exists. The kinetic elements describe what can change and who can change it.
| Concept | What it is | Supply-chain example |
|---|---|---|
| Object type | A schema for a real-world entity, backed by one or more data sources |
Order, Customer, Shipment
|
| Property | A typed field on an object type |
order.status, order.promisedDate
|
| Link type | A relationship between object types | An Order belongs to one Customer; it has many Shipment objects |
| Action type | A governed operation that edits objects, with parameters, rules, and submission criteria |
Reschedule Shipment, Flag Order At Risk
|
| Function | Business logic authored in code and published to the Ontology | A function that scores late-delivery risk |
| Interface | A shared shape that several object types can implement | A Trackable interface implemented by Shipment and Container
|
For an application developer, the important point is this: you do not query the warehouse tables behind Order. You query the Order object type. Its properties, links, and permissions are defined once in Foundry and apply to every consumer.
2. Why Build an External Application?
Build an external application when the users, the runtime, or the integration needs sit outside what Foundry's own app builders cover. Typical cases:
- Internal business portals that combine Ontology data with systems Foundry does not own, such as an ERP screen or a ticketing tool.
- Operational dashboards with a custom UX or design system that a low-code builder cannot reproduce.
- Workflow interfaces where planners or field staff apply governed actions ("reschedule this shipment") from a tool they already use.
- Backend services such as schedulers or integration jobs that read Ontology objects and push results to other systems.
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.
| Option | Where code runs | Server-side logic | Good fit |
|---|---|---|---|
| Foundry-native app (for example, Workshop) | Inside Foundry | Uses Foundry functions and actions | Standard operational UIs for Foundry users |
| OSDK app hosted on Foundry | Static assets served by Foundry; code runs in the browser | Not supported by the hosting feature | Custom single-page apps for Foundry users, no extra infrastructure |
| OSDK app on your own infrastructure | Your servers, cloud, or containers | Yes, under your control | Integration with other systems, backend jobs, custom runtimes |
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.
3. Understanding the Ontology SDK and TypeScript
The Ontology SDK 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.
A TypeScript OSDK project normally combines these packages:
| Package | Source | Role |
|---|---|---|
Generated package (for example, @your-app/sdk) |
Your Foundry stack's npm registry | Typed definitions for the object types, actions, and functions you selected |
@osdk/client |
Public npm | Runtime client: createClient, queries, actions, the Osdk types |
@osdk/oauth |
Public npm | Token providers: createPublicOauthClient and createConfidentialOauthClient
|
@osdk/foundry.* (optional) |
Public npm | Platform SDK bindings for non-Ontology Foundry APIs |
Since TypeScript OSDK 2.0, the runtime client is separate from the generated code. Palantir's migration guide 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.
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 TypeScript OSDK reference 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.
Two rules keep you out of trouble:
- Use the docs for your SDK version. Developer Console generates documentation for your specific Ontology and has a version picker. Public examples target 2.x.
- Treat each SDK version as an API contract. Regenerating after an Ontology change can rename or remove types your code uses.
4. Architecture and Integration Flow
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.
flowchart LR
U["End user"] --> UI["Web or mobile UI"]
UI -->|"app session"| BE["Your backend service"]
BE -->|"validated, authorized request"| SDK["OSDK client<br/>@osdk/client + generated SDK"]
SDK -->|"token request"| AUTH["Foundry OAuth"]
SDK -->|"typed queries and actions"| API["Foundry Ontology APIs"]
API --> ONT["Object types, links,<br/>action types, functions"]
ONT --> DATA["Backing datasets"]
UI -. "browser-only option:<br/>public client, user permissions" .-> API
Each component has one job:
| Component | Responsibility |
|---|---|
| External TypeScript UI | Presentation and user interaction. Holds no secrets. |
| Backend service | Authenticates your users, validates input, applies app-level authorization, holds the OAuth client secret, shapes responses. |
| OAuth (Foundry) | Issues tokens: client credentials for a service user, or authorization code for a signed-in Foundry user. |
| OSDK client | Builds typed requests, handles paging, surfaces structured errors. |
| Ontology | Enforces resource restrictions, object permissions, and action rules, then reads or writes the backing data. |
A typical read follows six steps:
- The user opens your app and authenticates with your identity provider.
- The UI calls your backend with an app session.
- The backend validates the input and checks that this user may request this data.
- The OSDK client obtains or reuses a Foundry token.
- Foundry checks the token's resource scope and the caller's permissions, then runs the query.
- The backend maps the result to a response shape and returns it.
The alternatives are legitimate. A browser-only app 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 compute modules. Choose based on where secrets can live and whose permissions should apply.
5. Setting Up the Development Environment
The example in this article is a server-side Node.js service that uses application permissions. The setup follows Palantir's guide to bootstrapping a back-end TypeScript application.
Prerequisites
- Access to a Foundry enrollment with Developer Console, and permission to create applications. Palantir's permissions page lists the roles involved, including the third-party application administrator workflow your organization may control.
- An object type to read. This article assumes an
Orderobject type withregion,status, andpromisedDateproperties. Substitute your own. - 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.
Create the Developer Console application
- Create a new application. Choose Backend service as the application type and Application's permissions as the permission type.
- 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.
- Copy the client secret when it is shown. Developer Console displays it once; losing it means rotating it, which breaks running deployments.
- Grant the generated service user read access to the selected object types. Without this, queries fail or return nothing.
- Generate the first SDK version and note the package name and registry URL.
Install packages
Configure npm to resolve your generated package from Foundry's registry. Keep the token in an environment variable, never in the file:
# .npmrc
//<REGISTRY-URL-FROM-OVERVIEW-PAGE>:_authToken=${FOUNDRY_TOKEN}
<PACKAGE-NAME>:registry=https://<REGISTRY-URL-FROM-OVERVIEW-PAGE>
export FOUNDRY_TOKEN=<token-from-developer-console> # not committed
npm install <PACKAGE-NAME> @osdk/client @osdk/oauth
npm install --save-dev typescript @types/node
Palantir's guides use @latest tags. For anything beyond a prototype, commit package-lock.json and upgrade deliberately.
Browser apps differ
A browser application cannot keep a secret. Palantir's docs require client-facing applications to use user permissions with the authorization code grant, through createPublicOauthClient and a registered redirect URL. For a browser project, start with npm create @osdk/app@latest as shown in the TypeScript bootstrap guide.
6. Building a Practical TypeScript Example
Use case: 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.
The API calls below (createConfidentialOauthClient, createClient, where, fetchPage, PalantirApiError) come from Palantir's documentation. Order, its properties, the "AT_RISK" value, and @your-app/sdk are placeholders for your generated SDK.
Project structure
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
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
Configuration: fail fast, never hard-code
// src/config.ts
function required(name: string): string {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
const foundryUrl = new URL(required("FOUNDRY_URL"));
if (foundryUrl.protocol !== "https:") {
throw new Error("FOUNDRY_URL must use https");
}
export const config = {
foundryUrl: foundryUrl.origin,
ontologyRid: required("ONTOLOGY_RID"),
clientId: required("FOUNDRY_CLIENT_ID"),
clientSecret: required("FOUNDRY_CLIENT_SECRET"),
} as const;
Auth and client
// src/foundry.ts
import { createClient } from "@osdk/client";
import { createConfidentialOauthClient } from "@osdk/oauth";
import { config } from "./config.js";
export const auth = createConfidentialOauthClient(
config.clientId,
config.clientSecret,
config.foundryUrl,
);
export const client = createClient(config.foundryUrl, config.ontologyRid, auth);
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.
Query with filtering and pagination
// src/orders.ts
import { Order } from "@your-app/sdk"; // replace with your generated package
import { client } from "./foundry.js";
const REGION_PATTERN = /^[A-Z]{2,8}$/;
const MAX_PAGE_SIZE = 100;
export interface AtRiskOrder {
orderId: string;
status: string | undefined;
promisedDate: string | undefined;
}
export interface AtRiskOrderPage {
orders: AtRiskOrder[];
nextPageToken: string | undefined;
}
export async function getAtRiskOrders(
region: string,
pageSize: number,
pageToken?: string,
): Promise<AtRiskOrderPage> {
if (!REGION_PATTERN.test(region)) {
throw new RangeError("Invalid region code");
}
const size = Math.min(Math.max(1, Math.trunc(pageSize)), MAX_PAGE_SIZE);
const page = await client(Order)
.where({
$and: [
{ region: { $eq: region } },
{ status: { $eq: "AT_RISK" } },
],
})
.fetchPage({
$select: ["status", "promisedDate"],
$orderBy: { promisedDate: "asc" },
$pageSize: size,
$nextPageToken: pageToken,
});
return {
orders: page.data.map((order) => ({
orderId: String(order.$primaryKey),
status: order.status,
promisedDate: order.promisedDate,
})),
nextPageToken: page.nextPageToken,
};
}
Entry point and error handling
// src/index.ts
import { PalantirApiError } from "@osdk/client";
import { auth } from "./foundry.js";
import { getAtRiskOrders } from "./orders.js";
const MAX_PAGES = 5; // demo guard against unbounded loops
async function main(): Promise<void> {
const region = process.argv[2] ?? "EMEA";
await auth.signIn(); // as in Palantir's back-end guide
let pageToken: string | undefined;
let pages = 0;
do {
const page = await getAtRiskOrders(region, 50, pageToken);
for (const order of page.orders) {
console.log(`${order.orderId}\t${order.status ?? "-"}\t${order.promisedDate ?? "-"}`);
}
pageToken = page.nextPageToken;
pages += 1;
} while (pageToken !== undefined && pages < MAX_PAGES);
}
main().catch((error: unknown) => {
if (error instanceof PalantirApiError) {
// Log identifiers, not parameters: parameters can contain business data.
console.error(
`Foundry API error ${error.errorName ?? "unknown"} ` +
`(status ${error.statusCode ?? "?"}, instance ${error.errorInstanceId ?? "n/a"})`,
);
} else if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error("Unknown error");
}
process.exitCode = 1;
});
What the important parts do
-
where+$andcompiles to a server-side filter. Only matching objects cross the network. -
$selectlimits returned properties. The primary key and object metadata still come back. -
$pageSizeand$nextPageTokenimplement cursor pagination.nextPageTokenisundefinedon the last page. - The DTO mapping decouples callers from generated types and avoids passing through properties nobody asked for.
-
PalantirApiErrorcarrieserrorName,statusCode, anderrorInstanceId, per the osdk-ts docs. The instance ID is what Palantir support asks for.
For long-running services, follow the token-refresh guidance in your Developer Console documentation rather than relying on a single signIn() at startup.
Run and test
# package.json scripts: "build": "tsc", "start": "node --env-file=.env dist/index.js"
npm run build
npm start -- EMEA
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 RangeError, 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.
Extending to actions
Writes go through action types, not direct edits. If you add an action type (here, a placeholder flagOrderAtRisk) to the SDK, the documented pattern is:
const result = await client(flagOrderAtRisk).applyAction(
{ orderId: "ORD-1001", reason: "Carrier delay" },
{ $returnEdits: true },
);
if (result.type !== "edits") {
// handle a rejected submission
}
The action's submission criteria still run on the server, for the service user.
7. Authentication, Security, and Governance
Authentication proves who is calling. Authorization decides what that caller may read or change. A valid OAuth token settles the first question only.
Pick the permission model first
Developer Console offers two permission types, and the choice shapes your whole design:
| User permissions | Application permissions | |
|---|---|---|
| OAuth flow | Authorization code grant | Client credentials grant |
| Whose access applies | The signed-in Foundry user | A generated service user |
| OSDK helper | createPublicOauthClient |
createConfidentialOauthClient |
| Allowed in browser apps | Yes (required for client-facing apps) | No: needs a secret, so server-side only |
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.
What Foundry enforces, and what it does not
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.
Palantir's OSDK overview states the limit plainly: these controls filter what can be read. 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 Order and your backend serves those orders to any logged-in portal user, Foundry is no longer deciding who sees what. Your backend is.
Practical rules:
- Prefer user permissions when end users see the data, so Foundry's policies apply per person.
- If you use a service user, implement your own authorization layer, and do not share caches across users with different entitlements.
- Grant least privilege twice: 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.
-
Keep secrets server-side in a secret manager or a git-ignored
.envfor local work. Rotating a secret in Developer Console breaks running deployments, so treat rotation as a planned release. - Validate input before it reaches a query or action. The OSDK builds structured filters rather than query strings, but an unvalidated parameter can still widen a filter or submit a bad action.
- Mind downstream copies. 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.
8. Common Challenges and Best Practices
Each row below is either documented by Palantir or follows directly from documented behavior. Check configuration and permissions before debugging code.
| Symptom | Likely cause | What to do |
|---|---|---|
npm install returns 404 for the generated package |
npm resolves from another registry | Run npm config delete registry, check every .npmrc, confirm the registry URL and FOUNDRY_TOKEN (guide) |
Generated code imports FoundryClient or PublicClientAuth
|
SDK was generated with the 1.x generator | Generate a new SDK version with the TypeScript OSDK 2.x generator |
| Auth works but queries fail or return nothing | Service user or end user lacks read access, or a resource is missing from the app's access restrictions | Check grants; act on Developer Console's "outdated dependencies" warning |
| Old clients break after a new SDK version | A removed resource was also dropped from access restrictions | Keep removed resources in restrictions until every client has upgraded |
ObjectsExceededLimit error |
Object type backed by Object Storage v1 (10,000-result cap), or an aggregation over 10,000 results | Filter on the server, aggregate instead of listing, review the object type's backing store |
| Duplicate or missing rows across pages | Data changed between page requests | Use the documented $snapshot fetch option when consistency matters |
| TLS errors behind a corporate proxy | Node does not trust the proxy's CA | Set NODE_EXTRA_CA_CERTS; never disable certificate verification |
Beyond the table:
-
Avoid N+1 link traversal. Calling
$linkper object in a loop multiplies requests.pivotTo()traverses links on a whole object set, andaggregate()computes counts and averages on the server. -
Retry reads, not writes. Retry idempotent queries on transient errors with backoff and a cap. Do not blindly retry
applyAction; a retry can apply an edit twice unless the action is designed to be idempotent. -
Log identifiers, not data.
errorInstanceIdis enough for support. Never log tokens, secrets, or object payloads. -
Respect the type system's edges. The TypeScript OSDK reference notes that aggregation results such as
avg,min, andmaxareundefinedwhen nothing matches. Handle that branch. - Test at two levels. Unit-test your mapping and validation with stubbed data, and keep a small integration suite against a non-production Ontology. Palantir publishes guides for unit testing and integration testing TypeScript OSDK code.
9. How This Fits into Enterprise Data and AI Transformation
An Ontology-backed external application connects three things that usually live apart: business context (what an Order is and how it relates to a Shipment), 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.
The same governed layer serves AI-enabled processes where your enrollment supports them. The TypeScript OSDK documents vector similarity search through nearestNeighbors() for object types with embedding properties. Palantir's Ontology MCP 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.
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.
This is also where Palantir consulting 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.
10. Conclusion
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: createClient, a token provider, and object set queries such as where and fetchPage. 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.
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.
This article was drafted with AI assistance. The author reviewed the technical content against the linked documentation before publishing.
References
Official Palantir documentation consulted for this article:
- Ontology building: Overview
- Ontology SDK (OSDK): Overview
- TypeScript OSDK reference
- TypeScript OSDK migration guide (1.x to 2.0)
- Developer Console: Permissions
- Bootstrap a back-end TypeScript application
- Bootstrap a TypeScript application
- Add OSDK to an existing application
- Host an OSDK application on Foundry
- Ontology MCP: Overview
- osdk-ts docs: Handling errors with PalantirApiError
Top comments (0)