TypeScript v2 (OSDK Pattern)
@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.| Feature | TS v1 (@Class) | TS v2 (OSDK) | Python |
|---|---|---|---|
| Structure | Class with decorators | Module-level exports | @function decorator |
| Type generation | Manual interfaces | OSDK-generated types | FoundryClient stubs |
| Workshop Query support | Yes (@Query) | Yes (@Function export) | No |
| Computed properties | Yes (@ComputedProperty) | Yes (separate export) | No |
| ObjectSet API | Limited (v1 API) | Full OSDK ObjectSet | FoundryClient search |
| Recommended for new projects | No | Yes | Python teams only |
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.
TypeScript v1 (Class + Decorator Pattern)
@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.| Decorator | Purpose | Example |
|---|---|---|
| @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 |
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.
Python Functions
@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.| Type | Python Type | Notes |
|---|---|---|
| String | str | UTF-8 string; max 1MB in function params |
| Integer | int | Maps to Foundry Integer (64-bit signed) |
| Double | float | Maps to Foundry Double (IEEE 754) |
| Boolean | bool | True/False |
| Date | datetime.date | ISO 8601 date; no time component |
| Timestamp | datetime.datetime | UTC datetime with time component |
| Object | OntologyObject subclass | Generated class from SDK; must be imported |
| ObjectSet | Iterable[OntologyObject] | Passed as list; size limits apply |
| List[T] | List[T] | Homogeneous list of any supported primitive |
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.
Execution Modes
| Mode | Runs As | Use When | Security |
|---|---|---|---|
| User-scoped (default) | Triggering user | Standard read/write within the user's existing access | Least privilege; function can only access what the user can |
| Project-scoped | Project service account | Admin workflows triggered by regular users; elevated write access required | Grants full project data access; use sparingly |
| Serverless (default runtime) | Either scope | Short operations under 10 seconds | Auto-scaled; no persistent state between invocations |
| Deployed container | Either scope | Long-running operations, ML inference, warm-start latency matters | Persistent container; higher resource cost |
// 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.
Testing Patterns
@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.| Package | Language | Purpose |
|---|---|---|
| @osdk/functions-testing | TypeScript | Object stub builders for OSDK v2 functions; in-memory ObjectSet with filter support |
| jest | TypeScript | Test runner and assertion library for all TypeScript function tests |
| @foundry/functions-testing-shim | TypeScript v1 | Shim that provides mock Objects search API for v1 class-based functions |
| pytest | Python | Standard Python test runner; works with MagicMock for FoundryClient stubs |
| pytest-mock | Python | mocker fixture for clean mock injection in Python function tests |
| foundry-dev-tools | Python | Local Foundry client for integration testing against a real (non-prod) stack |
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.
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.