Transactions

Transactions ensure that multiple database operations either all succeed or all fail together. Keel handles transactions automatically for write operations, so your data stays consistent even when errors occur.

Automatic transactions

Keel wraps your function code in a database transaction based on the type of function you're writing. For functions that modify data, all database operations within your function execute inside a single transaction. If any error occurs (whether from your code, a permission check, or a database constraint), Keel rolls back the entire transaction and no changes persist to your database.

Here's which function types have transactions enabled by default:

Function typeTransactions enabled
beforeQuery hookNo
afterQuery hookNo
beforeWrite hookYes
afterWrite hookYes
read custom functionNo
write custom functionYes
JobNo
SubscriberNo

Read operations (get, list, and read functions) don't use transactions because they don't modify data.

How rollback works

When an error occurs inside a transaction, Keel automatically rolls back all database changes made during that function call. This happens whether the error comes from:

  • An exception thrown in your code
  • A failed permission check
  • A database constraint violation (like a unique constraint)
  • Any other runtime error
functions/createOrder.ts
import { CreateOrder, models } from "@teamkeel/sdk";
 
export default CreateOrder(async (ctx, inputs) => {
  // This record gets created
  const order = await models.order.create({
    reference: inputs.reference,
    customerId: inputs.customerId,
  });
 
  // If this throws an error, the order creation above is rolled back
  await models.orderLine.create({
    orderId: order.id,
    productId: inputs.productId,
    quantity: inputs.quantity,
  });
 
  return order;
});

If the orderLine creation fails for any reason, the order record won't exist in the database either.

Configuring transactions

You can override the default transaction behaviour by setting the dbTransaction property in your function's config. This is useful when you need transactions for a read operation or want to disable them for a write operation.

Enabling transactions

To enable transactions for a function that doesn't have them by default:

functions/generateReport.ts
import { GenerateReport, models } from "@teamkeel/sdk";
 
// Enable transactions for this read function
GenerateReport.config = {
  dbTransaction: true,
};
 
export default GenerateReport(async (ctx, inputs) => {
  // All reads here see a consistent snapshot of the database
  const orders = await models.order.findMany({
    where: { status: { equals: "pending" } },
  });
 
  const inventory = await models.stockItem.findMany({});
 
  return { orders, inventory };
});

Disabling transactions

To disable transactions for a function that has them by default:

functions/processOrders.ts
import { ProcessOrders, models } from "@teamkeel/sdk";
 
// Disable transactions for this write function
ProcessOrders.config = {
  dbTransaction: false,
};
 
export default ProcessOrders(async (ctx, inputs) => {
  // Each operation commits independently
  for (const orderId of inputs.orderIds) {
    await models.order.update(
      { id: orderId },
      { status: "processed" }
    );
  }
});
⚠️

Be careful when disabling transactions. If an error occurs partway through, some changes will persist while others won't, potentially leaving your data in an inconsistent state.

Configuring hooks

For action hooks, set the config on the function itself:

functions/updateInventory.ts
import { UpdateInventory, models } from "@teamkeel/sdk";
 
const hooks = {
  beforeWrite: async (ctx, inputs, values) => {
    // Custom logic here
    return values;
  },
};
 
// Configure transaction behaviour for this hook
UpdateInventory.config = {
  dbTransaction: false,
};
 
export default UpdateInventory(hooks);

When to use transactions

Enable transactions when you need:

  • Multiple related records to be created or updated together
  • Data consistency guarantees across multiple operations
  • Protection against partial updates when errors occur

Consider disabling transactions when:

  • Processing large batches where partial progress is acceptable
  • Each operation is independent and can succeed or fail on its own
  • You want failed items to not affect successfully processed items

Example: Creating an order with order lines

Here's a practical example showing how transactions protect data integrity when creating related records:

schema.keel
model Order {
  fields {
    reference Text @unique
    customer Customer
    status OrderStatus @default(OrderStatus.Pending)
  }
}
 
model OrderLine {
  fields {
    order Order
    product Product
    quantity Number
    unitPrice Decimal
  }
}
 
enum OrderStatus {
  Pending
  Confirmed
  Shipped
}
functions/placeOrder.ts
import { PlaceOrder, models } from "@teamkeel/sdk";
 
export default PlaceOrder(async (ctx, inputs) => {
  // Create the order header
  const order = await models.order.create({
    reference: inputs.reference,
    customerId: inputs.customerId,
  });
 
  // Create each order line
  for (const line of inputs.lines) {
    // Check stock availability
    const product = await models.product.findOne({ id: line.productId });
 
    if (!product || product.stockQuantity < line.quantity) {
      // This error rolls back the entire transaction
      // including the order header and any previously created lines
      throw new Error(`Insufficient stock for product ${line.productId}`);
    }
 
    await models.orderLine.create({
      orderId: order.id,
      productId: line.productId,
      quantity: line.quantity,
      unitPrice: product.price,
    });
 
    // Decrement stock
    await models.product.update(
      { id: line.productId },
      { stockQuantity: product.stockQuantity - line.quantity }
    );
  }
 
  return order;
});

Because this is a write function, all operations happen within a transaction. If any product has insufficient stock:

  1. The order header is rolled back
  2. All order lines are rolled back
  3. No stock quantities are decremented
  4. The database remains in its original state