How-to guide
Run a distributed transaction
Define a saga that writes to several systems, watch it execute, and see it roll back on failure.
Prerequisites
- Write-enabled capabilities with compensation declared for each table the saga touches
- A user or API key with permission to write to those tables
A saga is a JSON definition of ordered writes across any number of registered sources. Kiwi Fabric runs independent steps in parallel, passes values between dependent steps, and, if any step fails, compensates every completed step in reverse order.
Define a saga#
This example inserts a category in SQL Server, then uses the key it generated to create an inventory record in MongoDB, while a review is written in parallel:
{
"steps": [
{
"id": "new-category",
"table": "Categories",
"operation": "insert",
"payload": { "CategoryName": "New Beverages", "Description": "Small-batch and imported drinks" }
},
{
"id": "init-inventory",
"table": "inventory",
"operation": "insert",
"dependsOn": ["new-category"],
"payload": {
"productId": { "$ref": "new-category.Category_id" },
"warehouse": "WH-EAST",
"quantity": 0
}
},
{
"id": "seed-review",
"table": "reviews",
"operation": "insert",
"payload": { "reviewId": 90101, "productId": 1, "rating": 5, "title": "Great selection" }
}
]
}
| Field | Meaning |
|---|---|
id |
Unique name of the step, used by dependsOn and $ref |
table |
The registered capability to write to |
operation |
insert, update, delete or rawsql (see the step kinds below) |
dependsOn |
Step ids that must finish first. Steps without dependencies run together in the first wave |
payload |
Column values. { "$ref": "stepId.keyName" } takes a value produced by an earlier step, such as a generated identity |
Each step can also carry an idempotency key, which makes a retry after a timeout safe.
Step kinds#
| Kind | What it does | How it is undone |
|---|---|---|
| Insert | Inserts a row | Deletes by the generated identity captured at write time |
| Upsert | Inserts or updates by identity column | Restores the pre-upsert row |
| Update | Updates rows matching a filter | Restores the affected rows from a snapshot |
| Delete | Deletes matching rows | Re-inserts the deleted rows from a snapshot |
| Raw SQL | Runs arbitrary SQL on a named source | You must supply compensation SQL |
| REST call | Calls an external HTTP endpoint | You must supply a compensation URL |
| Execute stored procedure | Runs a registered write procedure | The compensation procedure registered with the capability |
| Query | Runs a federated SELECT and passes the result to later steps |
Not needed (read-only) |
Updates and deletes use a filter tree with eq, neq, gt, gte, lt, lte, like, in, between, isNull, and, or and not. A numeric column can be changed relatively, for example subtracting 5 from a stock count, instead of overwritten.
Run it#
kiwi fabric saga-run --file my-saga.json
The output shows the saga id, success or failure, the duration, a table of steps (table, mode, success, milliseconds, error) and, if applicable, a compensation gap report.
Over HTTP use POST /api/saga for one JSON response, or POST /api/saga/stream for Server-Sent Events (Planned, WaveStarted, StepCompleted, Compensating, CompensationStepCompleted and Result frames):
curl -N -X POST http://localhost:8080/api/saga/stream \
-H "Content-Type: application/json" -H "Authorization: Bearer <your-api-key>" \
-d @my-saga.json
In the Web UI, the Saga Runner shows a live step-by-step view: which steps run concurrently, which have finished, and, on failure, the compensation steps firing in reverse order.

Switch between the Visual builder and the JSON editor at any time; Execute Saga runs the definition you are viewing.

Check the result later#
kiwi fabric saga-status --id <saga-id>
kiwi fabric saga-gaps --id <saga-id>
Save a saga as a template#
Recurring transaction patterns can be stored once and run by name with runtime values. In the Web UI, use Save as Template in the Saga Runner, or the Saga Templates page, and mark parameters with {{paramName}} in the saga JSON:

From the CLI:
kiwi fabric template-add --file place-order.json
kiwi fabric template-run --name place-order --param orderId=42,customerId=7
Build it with the SDK#
The .NET, Java and Node.js SDKs have a fluent saga builder. The Python SDK takes typed request models, or the saga JSON itself. Pick your language:
using Kiwify.Kiwi.Fabric.Client.Builders;
var request = new SagaRequestBuilder("saga-place-order")
.Insert("create-order", "sample_orders", new()
{
["customer_id"] = 1, ["product"] = "Laptop", ["quantity"] = 1,
["total"] = 999.99, ["status"] = "pending"
})
.Insert("queue-fulfilment", "fulfilment", new()
{
["order_id"] = SagaRef.To("create-order", "id"),
["warehouse"] = "WH-EAST"
}, dependsOn: ["create-order"])
.Update("deduct-credit", "sample_customers",
SetClause.RelativeSubtract("credit_limit", 999.99),
Filter.Eq("id", 1),
dependsOn: ["create-order"])
.Build();
var result = await client.ExecuteSagaAsync(request);
import com.kiwify.fabric.client.builder.Filter;
import com.kiwify.fabric.client.builder.SagaBuilder;
import com.kiwify.fabric.client.builder.SetClause;
import com.kiwify.fabric.client.model.saga.SagaRef;
import java.util.List;
import java.util.Map;
var request = new SagaBuilder("saga-place-order")
.insert("create-order", "sample_orders", Map.of(
"customer_id", 1, "product", "Laptop", "quantity", 1,
"total", 999.99, "status", "pending"))
.insert("queue-fulfilment", "fulfilment", Map.of(
"order_id", SagaRef.to("create-order", "id"), "warehouse", "WH-EAST"),
List.of("create-order"))
.update("deduct-credit", "sample_customers",
SetClause.relativeSubtract("credit_limit", 999.99),
Filter.eq("id", 1),
List.of("create-order"))
.build();
var result = client.sagas.execute(request);
import { SagaBuilder, Filter, SetClause, SagaRef } from "kiwifabric";
const request = new SagaBuilder("saga-place-order")
.insert("create-order", "sample_orders", {
customer_id: 1, product: "Laptop", quantity: 1, total: 999.99, status: "pending",
})
.insert("queue-fulfilment", "fulfilment", {
order_id: SagaRef.to("create-order", "id"), warehouse: "WH-EAST",
}, ["create-order"])
.update("deduct-credit", "sample_customers",
SetClause.relativeSubtract("credit_limit", 999.99),
Filter.eq("id", 1),
["create-order"])
.build();
const result = await client.sagas.execute(request);
from kiwifabric.models.saga import (
FilterRequest, SagaRequest, SagaStepRequest, SetClauseRequest, saga_ref,
)
request = SagaRequest(
id="saga-place-order",
steps=[
SagaStepRequest(
id="create-order", table="sample_orders", operation="insert",
payload={"customer_id": 1, "product": "Laptop", "quantity": 1,
"total": 999.99, "status": "pending"},
),
SagaStepRequest(
id="queue-fulfilment", table="fulfilment", operation="insert",
depends_on=["create-order"],
payload={"order_id": saga_ref("create-order", "id"), "warehouse": "WH-EAST"},
),
SagaStepRequest(
id="deduct-credit", table="sample_customers", operation="update",
depends_on=["create-order"],
set_clauses=[SetClauseRequest(type="relative", column="credit_limit",
operation="subtract", delta=999.99)],
filter=FilterRequest(type="eq", column="id", value=1),
),
],
)
result = client.sagas.run(request)
SagaRef.to(step, key) (saga_ref in Python) passes a key generated by an earlier step, such as an auto-increment id, into a later one. If any step fails, the completed steps are compensated in reverse order.
Note
Describe the transaction in plain English and let an AI assistant draft the JSON for you. See Generate SQL and sagas with AI.
Expected result
- On success, every step completes and generated keys flow between steps
- On failure, completed steps are undone in reverse order