Object Types
| 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 Active⚠️ Primary 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
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 Employee⚠️ Breaking 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
| 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
| 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: operationalStatus⚠️ Over-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
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
| 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.