Ontology transactions [Beta]

Beta

Ontology transactions is in the beta phase of development and may not be available on your enrollment. Functionality may change during active development. Contact Palantir Support to request access.

Ontology transactions provide an additional execution model for functions that edit objects in the Ontology. Unlike edits made via regular Ontology edit functions, transaction functions:

  • Provide a read-after-write guarantee for Ontology edits applied in the function. All edits applied within the function are staged in the transaction and will be reflected in Ontology queries and aggregations later in the function.
  • Allow nested calls to other transaction functions making Ontology edits.

This page shows how to write transaction functions and documents unique properties. For more details about how edit functions work, refer to the overview page.

Key differences from regular Ontology edit functions

Transaction-based edit functions differ from regular edit functions in several important ways.

Read-after-write guarantee

Within a transaction function, any Ontology data read will reflect all edits previously made in the transaction function (and all edits made in a calling transaction function within the same action execution). Such edits are staged only within the transaction and are not visible in queries made by other users or through functions outside the context of the transaction. This allows you to query the Ontology from within the transaction function using search requests and aggregations with all staged edits being reflected in the result of the query.

No requirement to return edits at the end of the function

Regular Python and TypeScript V2 Ontology edit functions require returning a batch of Ontology edits as the return value of the function for those edits to be applied. In transaction functions, the Ontology edits are automatically staged in the transaction and will be applied to the Ontology at the end of the function execution when the action completes. This frees up the return value of the function to return other information to the caller.

For example, you can apply an action that executes a TypeScript V2 transaction function which then makes some edits and further calls an AIP Logic function. Queries made by the AIP Logic function will return the Ontology changes made in the TypeScript V2 function; any additional edits made by the AIP Logic function will be added to the same transaction without a need to return them as part of the Logic function. All staged edits will be applied automatically once the action completes.

Transactional execution

All operations within the transaction function, including queries, function calls, and AIP Logic executions, run within the same transaction. The transaction is started by the action that executes the function and is committed (that is, the staged Ontology edits are applied to the Ontology) after the function completes successfully. If the function throws an error, no edits are applied and the transaction is rolled back before being retried by the action.

WriteableClient

Transaction functions use a WriteableClient instead of the standard Client. The WriteableClient provides direct methods for creating, updating, and deleting objects without needing to construct an edit batch.

Define a transaction function

Transaction functions must explicitly declare the entities that will be edited using the Edits type exported from the @osdk/functions package. The first parameter must be a WriteableClient<T> where T is the union of all edit types the function will perform. The return value is no longer constrained to be an array of edits so you can return any value. The following example declares a function that will edit the Employee object type:

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 import { Employee } from "@ontology/sdk"; import { Edits } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; type OntologyEdit = Edits.Object<Employee>; export default async function assignTicket( client: WriteableClient<OntologyEdit>, employeeId: string, ticketId: string ): Promise<void> { // ... }

Create objects

Use the create method on the WriteableClient to create new objects. You must specify the object type and provide a value for the primary key, along with any other properties you want to initialize.

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Employee } from "@ontology/sdk"; import { Edits } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; type OntologyEdit = Edits.Object<Employee>; async function createEmployee( client: WriteableClient<OntologyEdit>, employeeId: string, firstName: string, lastName: string ): Promise<Integer> { await client.create(Employee, { employeeId: employeeId, firstName: firstName, lastName: lastName }); return employeeId; } export default createEmployee;

Creating with generated IDs

When you need to generate an ID and then use it immediately:

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import { Ticket } from "@ontology/sdk"; import { Edits, Integer } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; import { randomUUID } from "crypto"; type OntologyEdit = Edits.Object<Ticket>; async function createTicket( client: WriteableClient<OntologyEdit>, title: string ): Promise<string> { const ticketId = randomUUID(); await client.create(Ticket, { ticketId: ticketId, title: title, status: "open" }); return ticketId; } export default createTicket;

Update objects

Object properties

Use the update method on the WriteableClient to modify object properties:

Copied!
1 await client.update(employee, { lastName: newName });

You can also update an object by referencing its API name and primary key:

Copied!
1 await client.update({ $apiName: "Employee", $primaryKey: 23 }, { lastName: newName });

Interface properties

You can use the update method to modify interface properties of an object through an Ontology interface:

Copied!
1 await client.update(person, { firstName: newFirstName });

Note that an interface property that is implemented by the primary key property of the underlying object cannot be updated.

Delete objects

You can delete an object by calling the delete method on the WriteableClient:

Copied!
1 await client.delete(ticket);

Objects may also be deleted using a primary key instead of an instance:

Copied!
1 await client.delete({ $apiName: "Ticket", $primaryKey: 12 });

Use the link and unlink methods on the WriteableClient to add or remove many to many links between objects:

Copied!
1 2 3 4 5 // Assign a ticket to an employee await client.link(employee, "assignedTickets", ticket); // Unassign a ticket from an employee await client.unlink(employee, "assignedTickets", ticket);

You can also reference either side of the link with an API name and primary key:

Copied!
1 2 3 4 5 await client.link( { $apiName: "Employee", $primaryKey: 23 }, "assignedTickets", { $apiName: "Ticket", $primaryKey: 12 } );

To edit one to many links, edit the foreign key property using a create or update object edit.

Read-after-write within transactions

One of the key advantages of transaction functions is the ability to read data that was just written in the same transaction. This is useful for implementing workflows that require immediate consistency.

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { Employee, Ticket } from "@ontology/sdk"; import { Edits, Integer } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; import { randomUUID } from "crypto"; type OntologyEdit = Edits.Object<Ticket> | Edits.Link<Employee, "assignedTickets">; async function assignTicketAndCheckWorkload( client: WriteableClient<OntologyEdit>, employeeId: Integer, title: string ): Promise<{ ticketId: string, totalAssignedTickets: number }> { const ticketId = randomUUID(); // Create the ticket await client.create(Ticket, { ticketId: ticketId, title: title, status: "open" }); // Assign the ticket to the employee await client.link( { $apiName: "Employee", $primaryKey: employeeId }, "assignedTickets", { $apiName: "Ticket", $primaryKey: ticketId } ); // Query the employee's total workload including the newly assigned ticket // This works because of the read-after-write guarantee const result = await client.aggregate(Ticket, (tickets) => tickets .where(ticket => ticket.assignedTo.employeeId.exactMatch(employeeId)) .where(ticket => ticket.status.exactMatch("open")) .count() ); return { ticketId: ticketId, totalAssignedTickets: result }; } export default assignTicketAndCheckWorkload;

Executing functions within transactions

When you execute other functions or queries inside a transaction function, those operations run within the same transaction. This means any reads to the ontology will reflect changes made to the transaction prior. This applies to:

  • Other TypeScript functions
  • AIP Logic functions
  • Ontology queries

Any edits made by nested function calls are also part of the same transaction and will be committed or rolled back together.

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import { Employee, Ticket } from "@ontology/sdk"; import { Edits, Integer } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; type OntologyEdit = Edits.Object<Ticket> | Edits.Link<Employee, "assignedTickets">; async function bulkAssignTickets( client: WriteableClient<OntologyEdit>, employeeId: Integer, ticketIds: string[] ): Promise<Integer> { let assignedCount = 0; for (const ticketId of ticketIds) { // Each link operation is part of the same transaction await client.link( { $apiName: "Employee", $primaryKey: employeeId }, "assignedTickets", { $apiName: "Ticket", $primaryKey: ticketId } ); // Update ticket status await client.update( { $apiName: "Ticket", $primaryKey: ticketId }, { status: "assigned" } ); assignedCount++; } // All edits will be committed together when the function completes return assignedCount; } export default bulkAssignTickets;

Transaction lifecycle

Understanding when transactions start and commit is important for building reliable functions:

  1. Transaction start: The transaction begins when the action executes the function.
  2. Function execution: All operations (creates, updates, deletes, reads, nested function calls) occur within the transaction.
  3. Transaction commit: If the function completes successfully, the transaction is committed before the action finishes.
  4. Rollback on error: If the function throws an error, the transaction is rolled back and no edits are applied. The function is retried by the action.
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { Employee } from "@ontology/sdk"; import { Edits, Integer } from "@osdk/functions"; import { WriteableClient } from "@osdk/functions/experimental"; async function updateEmployeeWithValidation( client: WriteableClient<Edits.Object<Employee>>, employeeId: Integer, newSalary: number ): Promise<void> { // Validate input if (newSalary < 0) { // Transaction will be rolled back throw new Error("Salary cannot be negative"); } // Update the employee await client.update( { $apiName: "Employee", $primaryKey: employeeId }, { salary: newSalary } ); // If we reach here, the transaction will be committed } export default updateEmployeeWithValidation;