Object Types
An Object Type declaration tells Foundry three things: which dataset backs it, which column is the primary key, and which columns are exposed as properties. Once registered, every downstream consumer — Workshop, Functions, OSDK, AIP Logic — can query, filter, and aggregate objects without touching raw SQL or Spark.
Primary key selection is the single most consequential decision when defining an Object Type. The key must be unique per row, stable over the lifetime of the object, and ideally meaningful to business users. When a natural business identifier exists and satisfies those constraints, prefer it. When it does not, generate a synthetic key in the upstream transform (SHA-256 of stable columns, or a UUID seeded from the pipeline).
Properties declare the full set of columns that consumers can read. You do not need to expose every column — projecting only what is useful keeps the Ontology surface clean and reduces the risk of leaking sensitive columns to broad audiences. Each property carries a type drawn from Foundry's property type vocabulary, a display name, and an optional description used in search and documentation.
| Type | Example | Notes |
|---|---|---|
| string | "Acme Corp" | UTF-8, arbitrary length; use for identifiers and free text |
| integer | 42 | 32-bit signed integer; use for counts and discrete quantities |
| double | 3.14159 | 64-bit IEEE 754 float; use for measurements and ratios |
| boolean | true | True/false flag; backed by boolean column in dataset |
| date | "2024-03-15" | Calendar date without time; ISO 8601 format (YYYY-MM-DD) |
| timestamp | "2024-03-15T14:30:00Z" | UTC instant; ISO 8601 with timezone; use for events |
| geohash | "9q8yy" | Geohash string encoding a lat/lon bounding box; enables spatial queries |
| struct | { lat: 37.7, lon: -122.4 } | Nested object with named fields; backed by struct column |
| array | ["tag1", "tag2"] | Ordered list of a single element type; backed by array column |
objectType:
apiName: employee
displayName: Employee
primaryKey: employee_id
datasource:
datasetRid: ri.foundry.main.dataset.abc123
branchName: master
properties:
- apiName: employeeId
columnName: employee_id
type: string
displayName: Employee ID
description: Unique HR system identifier for the employee
- apiName: fullName
columnName: full_name
type: string
displayName: Full Name
- apiName: department
columnName: department
type: string
displayName: Department
- apiName: hireDate
columnName: hire_date
type: date
displayName: Hire Date
- apiName: baseSalary
columnName: base_salary
type: double
displayName: Base Salary
- apiName: isActive
columnName: is_active
type: boolean
displayName: Is ActivePrimary key collisions
If two rows in the backing dataset share the same primary key value, Foundry does not raise an error at registration time — it silently retains one row and discards the other (last-write semantics per sync). This means your object count will be lower than your row count and queries will return stale or incomplete data with no obvious signal. Always deduplicate the backing dataset upstream in your Transform before registering it as an Object Type datasource. Add a uniqueness assertion to your pipeline to catch regressions.
Link Types
A Link Type connects a source Object Type to a destination Object Type and declares a cardinality: one-to-one, one-to-many, or many-to-many. The cardinality determines both the semantics exposed to consumers and the backing storage strategy.
For one-to-one and one-to-many links where the foreign key naturally lives in one of the object datasets (e.g., an Order row carries a customer_id column), you can use a foreign key backing — Foundry derives the edges directly from that column. For many-to-many relationships, you need a dedicated link dataset: a two-column dataset where each row represents one edge, containing the primary keys of the source and destination objects.
Link Types are traversed in Functions via the Objects API (e.g., order.customer()) and in Workshop via the relationship panel. They also power AIP Logic graph context, letting AI agents reason across connected objects. Prefer Link Types over denormalizing data into string properties — links remain consistent when the referenced object is updated, while a copied string property becomes stale.
| Type | Use When | Backing |
|---|---|---|
| one-to-one | Each source object relates to exactly one destination and vice versa (e.g., Employee → EmployeeProfile) | Foreign key on either dataset |
| one-to-many | One source object relates to multiple destinations (e.g., Department → Employees) | Foreign key on the "many" side dataset |
| many-to-many | Both sides can have multiple relationships (e.g., Employee ↔ Project) | Dedicated link dataset with two PK columns |
linkType:
apiName: employeeToProject
displayName: Employee to Project
objectTypeA: employee
objectTypeB: project
cardinality: MANY_TO_MANY
datasource:
type: LINK_DATASET
datasetRid: ri.foundry.main.dataset.def456
branchName: master
objectAColumn: employee_id
objectBColumn: project_id
reverseApiName: projectToEmployee
reverseDisplayName: Project to EmployeeBreaking change on cardinality edit
Editing the cardinality of an existing Link Type is a breaking change. Foundry must re-derive all edges, which temporarily unregisters the datasource backing the link. Any Workshop application or Function that relies on traversal will fail during that window. Plan cardinality changes during a maintenance window, test in a non-production branch first, and communicate downstream impact to Workshop app owners before making the change.
Action Types
An Action Type defines what mutation is allowed, what parameters the caller must supply, what validations must pass before the write executes, and which datasets or object storage backends receive the written data. Every invocation is recorded in the Foundry audit log with the caller identity, timestamp, and parameter values, making Action Types the preferred write path for regulated industries.
Actions can be simple (create a row, edit a property) or complex (function-backed). A function-backed action delegates its logic to a Foundry Function written in TypeScript or Python, allowing arbitrary computation before committing writes. The function receives the action parameters, performs validation and business logic, and returns a set of object edits that Foundry commits atomically.
Validation rules run before submission. They can check that required parameters are non-null, that property values fall within allowed ranges, or that referenced objects exist. Submission criteria are higher-level conditions — for example, "this action can only be submitted if the current user has the Approver role." Both layers prevent invalid state from reaching the backing dataset.
| Operation | Description | Parameters |
|---|---|---|
| CREATE | Inserts a new object into the backing dataset | All required properties for the new object |
| EDIT | Updates one or more properties on an existing object | Primary key of the target object + properties to update |
| DELETE | Removes an existing object from the backing dataset | Primary key of the target object |
| CREATE_LINK | Adds an edge between two existing objects in a link dataset | Primary keys of both objects |
| DELETE_LINK | Removes an edge from a link dataset | Primary keys of both objects |
import { Action, ActionEditResponse, Edits, Integer } from "@foundry/functions-api";
import { Employee } from "@foundry/ontology-api";
export interface UpdateEmployeeSalaryParams {
employee: Employee;
newSalary: Double;
effectiveDate: LocalDate;
justification: string;
}
export class SalaryActions {
@Action()
@Edits(Employee)
public async updateEmployeeSalary(
params: UpdateEmployeeSalaryParams
): Promise<ActionEditResponse> {
const { employee, newSalary, effectiveDate, justification } = params;
if (newSalary <= 0) {
throw new Error("Salary must be a positive value.");
}
if (newSalary > employee.baseSalary * 2) {
throw new Error(
"Salary increase exceeds 100% — requires executive approval."
);
}
employee.baseSalary = newSalary;
employee.lastSalaryUpdateDate = effectiveDate;
return {
edits: [employee],
};
}
}Action validation failure
When an Action Type fails silently or returns a generic validation error, check three things in order: (1) required properties — every property marked required in the Action definition must be supplied by the caller, even if it has a default in the dataset; (2) marking constraints — if a property uses an allowed-values list, the submitted value must exactly match one of the allowed values including case; (3) submission criteria — role-based or condition-based submission criteria are evaluated server-side and may reject the call even when all parameters are valid. Enable action audit logging to see the specific rule that fired.
Interfaces
An Interface is a declaration of named, typed properties with no backing dataset of its own. Each participating Object Type must supply a mapping from one of its concrete properties to each Interface property. At query time, Foundry fans out the interface query to all implementing Object Types and unions the results, returning objects of any implementing type that match the filter.
The canonical use case is asset tracking: Aircraft, Vehicle, and Vessel are distinct Object Types with different schemas, but all share location, assetId, and status as meaningful properties. Defining an Asset interface with those three properties lets an AIP agent query all assets in a region without knowing the specific type.
Interfaces are not inheritance. An Object Type that implements an interface is not a subtype — it simply agrees to expose the interface's properties under their interface-level API names. This means an Object Type can implement multiple unrelated interfaces simultaneously, and changing the interface does not automatically propagate to implementing types.
| Dimension | Interface | Object Type |
|---|---|---|
| Backing data | None — purely a contract | Required — a Foundry dataset |
| Can be queried directly | Yes — fans out to all implementations | Yes — queries one dataset |
| Has a primary key | No | Yes — required |
| Can define actions | No | Yes |
| Can have link types | No (links are on Object Types) | Yes |
| Supports polymorphism | Yes — core purpose | No |
| When to use | When multiple types share the same properties semantically | When modeling a distinct real-world entity |
interface:
apiName: asset
displayName: Asset
description: Any trackable physical asset in the enterprise
properties:
- apiName: assetId
type: string
displayName: Asset ID
- apiName: currentLocation
type: geohash
displayName: Current Location
- apiName: status
type: string
displayName: Status
---
objectType:
apiName: vehicle
displayName: Vehicle
primaryKey: vehicle_vin
implements:
- interfaceApiName: asset
propertyMappings:
- interfaceProperty: assetId
objectTypeProperty: vehicleVin
- interfaceProperty: currentLocation
objectTypeProperty: lastKnownGeohash
- interfaceProperty: status
objectTypeProperty: operationalStatusOver-using interfaces
Interfaces add cognitive overhead — every implementing Object Type must maintain property mappings, and adding a new interface property requires updating all implementations simultaneously or accepting partial coverage. Only create an interface when two or more Object Types genuinely share the same semantic properties: same meaning, same type, same unit. Do not create an interface just because two types happen to have a column with the same name but different semantics (e.g., "status" meaning order state on an Order vs. employment status on an Employee).
Best Practices
Model the domain, not the data. Your Ontology should reflect how your business talks about entities, not how they are stored in source systems. A source database might split a customer across three normalized tables; the Ontology should present a single Customer object with the joined properties that users care about. Use Transforms to prepare the backing dataset, not to work around a poorly designed Object Type.
Meaningful API names are critical because they cannot be changed without breaking consumers. Use lowerCamelCase for property API names and Object Type API names. Choose names that describe the business concept, not the column name. A column called emp_sal_usd_ann should become annualSalaryUsd. API names are permanent — treat them like a public API.
Prefer link types over denormalization. When an Order needs to reference its Customer, do not copy customer_name onto the Order object. Model a Link Type between Order and Customer. This ensures that when a customer's name changes, all orders automatically reflect the update. Denormalized copies create eventual consistency problems that are hard to debug.
Use interfaces for polymorphism. When Workshop or AIP needs to display or query multiple object types in the same panel, an interface is the right tool. Without it, every consumer must handle each type separately.
Version your Ontology incrementally. Add new properties and object types freely — they are additive and non-breaking. Remove or rename only when all downstream consumers have migrated. Use the Ontology usage panel to identify consumers before deprecating anything.
Keep primary keys stable. A primary key that changes causes the old object to be deleted and a new one to be created in Foundry's index. Any Links pointing to the old key become dangling edges. Favorites, comments, and audit records tied to the old object RID are lost. Never use a mutable business attribute (e.g., email address) as a primary key.
| Element | Convention | Example |
|---|---|---|
| Object Type API name | lowerCamelCase singular noun | salesOpportunity |
| Object Type display name | Title Case singular noun | Sales Opportunity |
| Property API name | lowerCamelCase, descriptive | annualSalaryUsd |
| Property display name | Title Case, human-readable | Annual Salary (USD) |
| Link Type API name | lowerCamelCase, sourceToDestination | opportunityToAccount |
| Action Type API name | lowerCamelCase verb phrase | closeOpportunity |
| Interface API name | lowerCamelCase noun, often abstract | trackableAsset |
Storing computed values as properties
Computed values — totals, averages, scores, derived statuses — should not be stored as properties on an Object Type. When you bake computation into the backing dataset, the value is stale the moment the inputs change and you pay for recomputation on every pipeline run even when no consumer has requested the value. Instead, implement computed properties as Foundry Functions. A Function marked as a computed property is evaluated on-demand, always reflects the current state of its inputs, and can be composed with other Functions. Reserve stored properties for raw, sourced facts that do not change without a source system update.
Anti-Patterns
Creating an Object Type for every source table is the most common mistake made by teams migrating from database-centric thinking. Not every table represents a meaningful business entity. Junction tables, audit log tables, configuration tables, and staging tables rarely belong in the Ontology. Ask: "Would a business user recognize this as a real thing they care about?" If not, it belongs in the data pipeline layer, not the Ontology.
Embedding foreign keys as string properties is a subtle form of the same mistake. If an Order has a customer_id property of type string, you have recreated a relational join in string form. Every consumer must manually resolve the ID to a Customer object, duplicating logic and coupling consumers to the raw identifier scheme. Use Link Types instead.
Over-indexing properties hurts write performance. Foundry maintains search indexes for every indexed property. Each sync that changes those properties must update the index. On large datasets with high-churn columns, this adds significant latency and cost to pipeline runs. Only index properties that users or applications actually filter or search on. Audit your indexes regularly.
Using string properties for structured data prevents type-safe queries and spatial operations. GPS coordinates stored as "37.774929,-122.419416" cannot be used in geospatial filters. Use geohash or struct property types. Similarly, storing JSON blobs as strings prevents Foundry from indexing or filtering on individual fields — use struct types with named fields instead.
| Anti-Pattern | Problem | Correct Pattern |
|---|---|---|
| Object Type for every source table | Pollutes Ontology with non-entities; confuses consumers | Only model entities a business user would recognize; join tables in Transforms |
| Foreign key as string property | Consumers must manually resolve IDs; logic duplicated everywhere | Model a Link Type between the two Object Types |
| Over-indexing all properties | Slow pipeline syncs; high index storage cost | Index only properties actively used in filters and search |
| String property for GPS coordinates | Cannot use spatial query operators; no geohash clustering | Use geohash property type for spatial indexing and queries |
| JSON blob as string property | Cannot filter or index individual fields; type-unsafe | Use struct property type with named, typed sub-fields |
| Mutable attribute as primary key | Key changes cause object re-creation; dangling links; lost history | Use a stable natural key or a synthetic generated UUID/SHA-256 |
| Computed value as stored property | Stale data between pipeline runs; wasted recomputation | Implement as a Foundry Function computed property evaluated on demand |
Over-indexing properties
Foundry indexes every property you mark as searchable or filterable. Each index entry must be written on every sync that touches that property, even if no downstream query has ever used the index. On datasets with millions of rows and dozens of indexed properties, this multiplies sync duration and increases storage costs with no user-visible benefit. Audit your Object Type indexes by checking the Ontology property panel — if a property has no active filters or search usage in Workshop or Functions, remove its index. You can always re-add it later if requirements change.
Decision Trees
Which primary key strategy?
Is there a natural business identifier?
Knowledge Check
Test your understanding with 3 questions. You need 2/3 to pass.