Functions

Foundry Functions let you extend the Ontology with custom TypeScript or Python logic that runs securely on Palantir infrastructure. Functions can be invoked from Workshop as Queries, triggered by Actions, or composed into AIP Logic pipelines. TypeScript supports both the modern OSDK (v2) export pattern and the legacy class-based (v1) decorator pattern; Python functions use the @function decorator with FoundryClient.

TypeScript v2 (OSDK Pattern)

TypeScript v2 functions use OSDK-generated object type exports and a module-level export pattern instead of class decorators. Each function is a plain exported async function annotated with the @Function() decorator from the OSDK package. The OSDK generates strongly-typed object interfaces, ObjectSet types, and property accessors from your Ontology schema, giving you full TypeScript autocompletion and compile-time safety. Defining a function requires importing the generated types from your OSDK package and exporting an async function. Return types must be explicitly declared using OSDK primitives or object references. The Ontology client is injected via the OSDK context — you do not instantiate it manually. Computed properties are defined separately from query functions: they are declared on the object type in the Ontology Builder and implemented as TypeScript functions that receive the object instance as their sole argument. Aggregations use the ObjectSet API (.aggregate()) and support count, sum, avg, min, max, and approximateDistinct operations.
Function Decorator Comparison
FeatureTS v1 (@Class)TS v2 (OSDK)Python
StructureClass with decoratorsModule-level exports@function decorator
Type generationManual interfacesOSDK-generated typesFoundryClient stubs
Workshop Query supportYes (@Query)Yes (@Function export)No
Computed propertiesYes (@ComputedProperty)Yes (separate export)No
ObjectSet APILimited (v1 API)Full OSDK ObjectSetFoundryClient search
Recommended for new projectsNoYesPython teams only
TypeScript v2 Function — query objects and return computed results
import { Function, OntologyObjectSet, Integer, Double } from "@osdk/functions";
import { SupplyOrder, Warehouse } from "@my-ontology/osdk";

export interface WarehouseFillRate {
  warehouseId: string;
  totalOrders: Integer;
  pendingOrders: Integer;
  fillRate: Double;
}

@Function()
export async function computeWarehouseFillRates(
  warehouseRegion: string
): Promise<WarehouseFillRate[]> {
  const warehouses = await Warehouse.where({
    region: { $eq: warehouseRegion },
  }).fetchPage({ pageSize: 100 });

  const results: WarehouseFillRate[] = [];

  for (const warehouse of warehouses.data) {
    const allOrders = SupplyOrder.where({
      warehouseId: { $eq: warehouse.warehouseId },
    });

    const [total, pending] = await Promise.all([
      allOrders.aggregate({ $count: true }),
      allOrders
        .where({ status: { $eq: "PENDING" } })
        .aggregate({ $count: true }),
    ]);

    results.push({
      warehouseId: warehouse.warehouseId,
      totalOrders: total.$count,
      pendingOrders: pending.$count,
      fillRate:
        total.$count > 0
          ? (total.$count - pending.$count) / total.$count
          : 0,
    });
  }

  return results;
}

⚠️ Function timeout

Serverless functions have a hard 10-second execution limit for read operations. Functions that iterate over large ObjectSets or make many sequential Ontology calls will hit this limit.

📖 Related Module: Foundry Functions

TypeScript v1 (Class + Decorator Pattern)

TypeScript v1 functions are organized as classes decorated with @OntologyEditFunction or @Function. Individual methods are marked with @Query (for read-only Workshop-callable functions), @OntologyEditFunction (for functions that modify the Ontology), or @Function (for general use). The class receives an Ontology client via constructor injection. You query objects using the v1 ObjectSet API, which is less expressive than the OSDK v2 API but functionally complete. The @Query decorator makes a method callable from Workshop's Function-backed variable widgets. Migrating from v1 to v2 mid-project is possible but disruptive — all function signatures, types, and calling conventions change. If a project already has significant v1 code, staying on v1 is often the pragmatic choice until a full migration window is available.
v1 Decorators Reference
DecoratorPurposeExample
@Function()Marks a class as a Foundry Function container@Function() class MyFunctions {}
@Query()Exposes a method as a Workshop-callable read query@Query() async getOrders(): Promise<Order[]>
@OntologyEditFunction()Marks a method as an Ontology mutation (creates/updates/deletes objects)@OntologyEditFunction() async createOrder(...)
@ComputedProperty()Declares a computed property on an object type@ComputedProperty() computeTotal(order: Order): Double
@Parameter()Adds metadata and validation to a function parameter@Parameter({ description: "..." }) orderId: string
TypeScript v1 — class with @Query and @OntologyEditFunction
import {
  Function,
  OntologyEditFunction,
  Query,
  Parameter,
  Edits,
  Integer,
} from "@foundry/functions-api";
import { Order, ObjectSet } from "@foundry/ontology-api";

@Function()
export class OrderManagement {
  @Query()
  async getOpenOrdersForCustomer(
    @Parameter({ description: "Customer identifier" }) customerId: string
  ): Promise<Order[]> {
    return Objects.search()
      .order()
      .filter((order) => order.customerId.exactMatch(customerId))
      .filter((order) => order.status.exactMatch("OPEN"))
      .all();
  }

  @OntologyEditFunction()
  async closeOrder(
    @Parameter({ description: "Order to close" }) order: Order,
    @Parameter({ description: "Closing reason" }) reason: string
  ): Promise<void> {
    order.status = "CLOSED";
    order.closingReason = reason;
    order.closedAt = new Date().toISOString();
  }

  @OntologyEditFunction()
  async bulkCloseOrders(
    @Parameter({ description: "Orders to close" }) orders: ObjectSet<Order>,
    @Parameter({ description: "Closing reason" }) reason: string
  ): Promise<Integer> {
    const orderList = await orders.all();
    for (const order of orderList) {
      order.status = "CLOSED";
      order.closingReason = reason;
      order.closedAt = new Date().toISOString();
    }
    return orderList.length;
  }
}

⚠️ Stale search results after edit

After an @OntologyEditFunction writes changes, a subsequent @Query that searches for the updated object via full-text or filter may not return it. The Ontology search index has an eventual-consistency lag of several seconds.

📖 Related Module: Foundry Functions

Python Functions

Python Functions are defined using the @function decorator from the functions package. Each decorated function is automatically registered as a Foundry Function. The function receives typed parameters that are declared using Python type hints — Foundry maps these to Ontology primitive and object types at registration time. FoundryClient is the primary interface for querying the Ontology from Python. It provides .objects, .actions, and .queries namespaces. Object queries return typed Python dataclasses generated by the Foundry SDK toolchain. Python functions can be wrapped in compute modules for batch execution, scheduled runs, or ML inference workflows. Testing Python functions uses pytest with mock FoundryClient objects. The foundry-dev-tools package provides local testing utilities that stub out Ontology calls without requiring a live Foundry connection. Python functions cannot be exposed as Workshop Queries — Workshop's Function-backed variable system only supports TypeScript functions. Python functions are best suited for data science teams, ML inference pipelines, and backend compute jobs that do not need to drive Workshop UI directly.
Python Function Parameter Types
TypePython TypeNotes
StringstrUTF-8 string; max 1MB in function params
IntegerintMaps to Foundry Integer (64-bit signed)
DoublefloatMaps to Foundry Double (IEEE 754)
BooleanboolTrue/False
Datedatetime.dateISO 8601 date; no time component
Timestampdatetime.datetimeUTC datetime with time component
ObjectOntologyObject subclassGenerated class from SDK; must be imported
ObjectSetIterable[OntologyObject]Passed as list; size limits apply
List[T]List[T]Homogeneous list of any supported primitive
Python function with @function decorator querying objects
from functions import function
from foundry import FoundryClient
from ontology_sdk import SupplyOrder, Warehouse
from datetime import date
from typing import List
from dataclasses import dataclass

@dataclass
class RegionSummary:
    region: str
    open_order_count: int
    total_value: float
    oldest_order_date: str

@function
def summarize_open_orders_by_region(
    region: str,
    as_of_date: date,
) -> List[RegionSummary]:
    client = FoundryClient()

    warehouses = (
        client.objects.Warehouse
        .filter(Warehouse.region == region)
        .iterate()
    )

    results = []
    for warehouse in warehouses:
        orders = list(
            client.objects.SupplyOrder
            .filter(SupplyOrder.warehouse_id == warehouse.warehouse_id)
            .filter(SupplyOrder.status == "OPEN")
            .filter(SupplyOrder.created_date <= as_of_date)
            .iterate()
        )

        if not orders:
            continue

        results.append(
            RegionSummary(
                region=region,
                open_order_count=len(orders),
                total_value=sum(o.order_value or 0.0 for o in orders),
                oldest_order_date=str(min(o.created_date for o in orders)),
            )
        )

    return results

⚠️ Python functions can't be Queries

Workshop variable widgets that are backed by Functions only support TypeScript functions registered as Queries. Python functions cannot be surfaced as Workshop Queries regardless of their signature.

📖 Related Module: Foundry Functions

Execution Modes

Every Foundry Function execution runs in one of two security contexts: user-scoped or project-scoped. User-scoped execution is the default — the function reads and writes to the Ontology using the same permissions as the user who triggered it. This is the safest option and is appropriate for the vast majority of functions. Project-scoped execution grants the function access to all data in the project using a service account. This is required when regular users need to trigger admin-level operations that they would not normally have direct access to perform — for example, a "submit for approval" workflow where the function must write to a restricted object type on the user's behalf. AIP Logic integrates with both execution modes. Functions used as AIP Logic nodes run in the context of the AIP Logic pipeline's configured execution scope. Function-backed Actions inherit the execution scope configured on the Action Type in the Ontology Builder. Deployed container execution removes the serverless 10-second timeout and is available for both TypeScript and Python functions. Deployed functions run in a persistent container that is started on demand and reused across invocations, making them suitable for ML inference, heavy data processing, or any operation that requires more than 10 seconds.
Execution Mode Comparison
ModeRuns AsUse WhenSecurity
User-scoped (default)Triggering userStandard read/write within the user's existing accessLeast privilege; function can only access what the user can
Project-scopedProject service accountAdmin workflows triggered by regular users; elevated write access requiredGrants full project data access; use sparingly
Serverless (default runtime)Either scopeShort operations under 10 secondsAuto-scaled; no persistent state between invocations
Deployed containerEither scopeLong-running operations, ML inference, warm-start latency mattersPersistent container; higher resource cost
Configuring project-scoped execution for an admin workflow
// In the Ontology Builder, the Action Type "adminDeleteStaleDrafts" is configured
// with project-scoped execution. The TypeScript function below is backed by that Action.
// Regular users trigger the Action; the function runs with project permissions.

import { Function, OntologyEditFunction, Parameter } from "@foundry/functions-api";
import { DraftOrder, ObjectSet } from "@foundry/ontology-api";

// This function runs with PROJECT permissions, not the triggering user's permissions.
// The Ontology Builder > Action Type > Execution Scope must be set to "Project" for this
// to take effect. Do not set project-scoped execution unless the workflow explicitly
// requires the function to access data the triggering user cannot access directly.

@Function()
export class AdminOrderCleanup {
  @OntologyEditFunction()
  async deleteStaleAdminDrafts(
    @Parameter({ description: "Cutoff date for stale drafts (ISO string)" })
    cutoffDateIso: string
  ): Promise<void> {
    const cutoff = new Date(cutoffDateIso);

    const staleDrafts: ObjectSet<DraftOrder> = Objects.search()
      .draftOrder()
      .filter((d) => d.status.exactMatch("DRAFT"))
      .filter((d) => d.createdAt.lt(cutoff.toISOString()));

    const drafts = await staleDrafts.all();
    for (const draft of drafts) {
      draft.status = "DELETED";
      draft.deletedAt = new Date().toISOString();
    }
  }
}

⚠️ Project-scoped overprivilege

Project-scoped execution grants the function access to all data in the project using a service account. If applied broadly, it means any user who can trigger the function gains indirect access to data they are not authorized to see or modify directly. This violates the principle of least privilege and can create compliance gaps.

📖 Related Module: Foundry Functions

Testing Patterns

TypeScript v2 functions are tested using Jest alongside @osdk/functions-testing, which provides object stub builders. Stubs let you construct in-memory Ontology objects that implement the full OSDK interface without hitting a live Foundry environment. ObjectSet stubs support .where(), .fetchPage(), and .aggregate() with in-memory data. TypeScript v1 functions are tested using Jest with manual mocks for the Objects search API. The v1 testing approach requires more boilerplate but follows the same pattern: mock the Ontology client at the constructor level and provide fake data. Python functions are tested with pytest. The recommended pattern is to inject a mock FoundryClient using unittest.mock.MagicMock or the pytest-mock mocker fixture. Stub the .objects.<ObjectType>.filter().iterate() call chain to return controlled test data. Edit functions — both TypeScript and Python — must always use stub objects in tests. If an edit function test runs against a real Ontology client, it will create, update, or delete real objects in your development or production environment. Stubs capture the mutations (property assignments) and let you assert on them without side effects.
Testing Packages
PackageLanguagePurpose
@osdk/functions-testingTypeScriptObject stub builders for OSDK v2 functions; in-memory ObjectSet with filter support
jestTypeScriptTest runner and assertion library for all TypeScript function tests
@foundry/functions-testing-shimTypeScript v1Shim that provides mock Objects search API for v1 class-based functions
pytestPythonStandard Python test runner; works with MagicMock for FoundryClient stubs
pytest-mockPythonmocker fixture for clean mock injection in Python function tests
foundry-dev-toolsPythonLocal Foundry client for integration testing against a real (non-prod) stack
Jest test using @osdk/functions-testing object stubs
import { createStub, createObjectSet } from "@osdk/functions-testing";
import { SupplyOrder, Warehouse } from "@my-ontology/osdk";
import { computeWarehouseFillRates } from "../src/computeWarehouseFillRates";

describe("computeWarehouseFillRates", () => {
  it("returns correct fill rates for a given region", async () => {
    const warehouse1 = createStub(Warehouse, {
      warehouseId: "WH-001",
      region: "APAC",
    });

    const order1 = createStub(SupplyOrder, {
      orderId: "ORD-1",
      warehouseId: "WH-001",
      status: "FULFILLED",
    });
    const order2 = createStub(SupplyOrder, {
      orderId: "ORD-2",
      warehouseId: "WH-001",
      status: "PENDING",
    });
    const order3 = createStub(SupplyOrder, {
      orderId: "ORD-3",
      warehouseId: "WH-001",
      status: "FULFILLED",
    });

    // Register stubs so the ObjectSet API returns them
    const warehouseSet = createObjectSet(Warehouse, [warehouse1]);
    const orderSet = createObjectSet(SupplyOrder, [order1, order2, order3]);

    jest
      .spyOn(Warehouse, "where")
      .mockReturnValue(warehouseSet as any);
    jest
      .spyOn(SupplyOrder, "where")
      .mockReturnValue(orderSet as any);

    const results = await computeWarehouseFillRates("APAC");

    expect(results).toHaveLength(1);
    expect(results[0].warehouseId).toBe("WH-001");
    expect(results[0].totalOrders).toBe(3);
    expect(results[0].pendingOrders).toBe(1);
    expect(results[0].fillRate).toBeCloseTo(0.6667, 3);
  });

  it("returns fill rate of 0 for a warehouse with no orders", async () => {
    const warehouse = createStub(Warehouse, {
      warehouseId: "WH-002",
      region: "EMEA",
    });

    jest
      .spyOn(Warehouse, "where")
      .mockReturnValue(createObjectSet(Warehouse, [warehouse]) as any);
    jest
      .spyOn(SupplyOrder, "where")
      .mockReturnValue(createObjectSet(SupplyOrder, []) as any);

    const results = await computeWarehouseFillRates("EMEA");

    expect(results[0].fillRate).toBe(0);
  });
});

⚠️ Testing edits without stubs

Edit functions that modify Ontology objects will execute their mutations against a real Ontology if the test does not use stub objects. Running such a test against a development or staging environment creates real objects, updates real properties, or deletes real data — potentially corrupting datasets or triggering downstream pipelines.

📖 Related Module: Foundry Functions

Decision Trees

Which function language and version?

Does the function need to be called from Workshop as a Query?

Knowledge Check

Test your understanding with 3 questions. You need 2/3 to pass.