Cosmos DB stored procedures and triggers aren’t database-level code that runs on a dedicated SQL engine; they’re JavaScript functions that execute on the application’s behalf within the Cosmos DB gateway.

Let’s see how this plays out with a concrete example. Imagine we have a products container and we want to atomically update a product’s price and also log that change.

// Stored Procedure: updateProductPriceAndLog
function updateProductPriceAndLog(productId, newPrice) {
    var productContext = {
        product_id: productId,
        new_price: newPrice,
        timestamp: new Date()
    };

    // Read the existing product
    var product = get.get(productId);
    if (!product) {
        throw new Error("Product with ID " + productId + " not found.");
    }

    // Update the price
    product.price = newPrice;
    set.set(product);

    // Insert a log entry into a 'priceLogs' container (assuming it exists)
    // Note: This requires the 'priceLogs' container to be accessible within the SP scope.
    // In a real scenario, you might use a separate SP for logging or trigger.
    // For demonstration, we'll simulate an insert.
    // In actual Cosmos DB, you'd need to manage container access or use separate operations.
    // This example focuses on the *concept* of atomic operations within an SP.

    // If we were to log, it would conceptually look like this:
    // var logEntry = {
    //     productId: productId,
    //     oldPrice: product.price, // before update
    //     newPrice: newPrice,
    //     timestamp: new Date()
    // };
    // log.insert(logEntry); // Hypothetical logging operation

    return "Product " + productId + " price updated to " + newPrice;
}

To execute this stored procedure, you’d use the Cosmos DB SDK. For instance, in Node.js:

const cosmosClient = new CosmosClient({ endpoint: endpoint, key: key });
const database = cosmosClient.database(databaseId);
const container = database.container('products');

async function executeStoredProcedure() {
    const productIdToUpdate = 'product-123';
    const newPriceValue = 25.99;

    try {
        const response = await container.storedProcedure("updateProductPriceAndLog").execute(null, [productIdToUpdate, newPriceValue]);
        console.log("Stored Procedure Result:", response.result);
    } catch (error) {
        console.error("Error executing stored procedure:", error);
    }
}

executeStoredProcedure();

The core problem these solve is atomicity for operations that span multiple document reads/writes within a single logical transaction. Without them, if you read a document, perform some logic, and then try to write back, the system could fail between your read and write. The stored procedure guarantees that all operations within it either succeed or fail together. This is crucial for maintaining data consistency, especially in scenarios like financial transactions or inventory management where partial updates are unacceptable.

Internally, Cosmos DB executes these JavaScript procedures in a sandboxed environment. When you call set.set(product), you’re not just writing to a local cache; you’re sending a command to the Cosmos DB service to perform that specific write operation. The gateway manages the execution context, ensuring that the JavaScript code interacts with the data as if it were a single, atomic unit.

The primary levers you control are the JavaScript code itself and the input parameters you pass. The get and set objects are provided by the Cosmos DB runtime and are your interface to reading and writing documents within the current request’s transaction scope. You can read multiple documents, modify them, and then set them back. Importantly, the number of operations (reads/writes) you can perform within a single stored procedure execution is limited by the Request Unit (RU) cost of those operations and the maximum script size.

One thing people often miss is that while stored procedures offer atomicity for operations within that single procedure execution, they don’t automatically handle distributed transactions across multiple containers or multiple stored procedure calls. If your logical transaction requires updates to two different containers, you’ll likely need two separate stored procedure calls, and you’ll need to implement your own reconciliation logic if one fails.

The next concept to explore is how triggers can automate these atomic operations based on document events.

Want structured learning?

Take the full Cosmos-db course →