Examples
Use these Trama examples to build saga definitions, run workflows inline or from stored versions, and test retries, compensation, and payload templating. This page is the fastest way to move from understanding Trama to using it.
Best starting points
If you are new to Trama, start with creating a stored definition, then run the inline example, and finally move to the robust sample with retries, compensation, callbacks, and templates.
Create and Store a Definition
curl -X POST http://localhost:8080/workflows/definitions \
-H 'Content-Type: application/json' \
-d '{
"name": "order-saga",
"version": "v1",
"failureHandling": {
"type": "retry",
"maxAttempts": 3,
"delayMillis": 500
},
"entrypoint": "reserve-inventory",
"nodes": [
{
"kind": "task",
"id": "reserve-inventory",
"action": {
"mode": "sync",
"request": {
"url": "http://inventory/reserve",
"verb": "POST",
"body": {
"orderId": "{{payload.orderId}}"
}
}
},
"compensation": {
"url": "http://inventory/release",
"verb": "POST",
"body": {
"orderId": "{{payload.orderId}}"
}
}
}
]
}'Robust Definition Using All Main Fields
This sample covers: backoff, multi-node flow, headers, body templates, explicit successStatusCodes, node response references, and success/failure callbacks.
{
"name": "order-fulfillment",
"version": "v1",
"failureHandling": {
"type": "backoff",
"maxAttempts": 5,
"initialDelayMillis": 200,
"maxDelayMillis": 5000,
"multiplier": 2.0,
"jitterRatio": 0.2
},
"entrypoint": "reserve-inventory",
"nodes": [
{
"kind": "task",
"id": "reserve-inventory",
"action": {
"mode": "sync",
"request": {
"url": "http://inventory/api/v1/reservations",
"verb": "POST",
"headers": {
"X-Correlation-Id": "{{payload.correlationId}}",
"Content-Type": "application/json"
},
"body": {
"orderId": "{{payload.orderId}}",
"customerId": "{{payload.customerId}}"
},
"successStatusCodes": [200, 201, 202]
}
},
"compensation": {
"url": "http://inventory/api/v1/reservations/{{nodes.reserve-inventory.response.body.reservationId}}/cancel",
"verb": "POST",
"headers": {
"X-Correlation-Id": "{{payload.correlationId}}"
},
"body": {
"reason": "compensation"
},
"successStatusCodes": [200, 204]
},
"next": "charge-payment"
},
{
"kind": "task",
"id": "charge-payment",
"action": {
"mode": "sync",
"request": {
"url": "http://payments/api/v1/charges",
"verb": "POST",
"headers": {
"Authorization": "Bearer {{payload.paymentToken}}",
"X-Correlation-Id": "{{payload.correlationId}}"
},
"body": {
"orderId": "{{payload.orderId}}",
"amount": "{{payload.amount}}",
"reservationId": "{{nodes.reserve-inventory.response.body.reservationId}}"
},
"successStatusCodes": [200, 201]
}
},
"compensation": {
"url": "http://payments/api/v1/charges/{{nodes.charge-payment.response.body.chargeId}}/refund",
"verb": "POST",
"headers": {
"Authorization": "Bearer {{payload.paymentToken}}",
"X-Correlation-Id": "{{payload.correlationId}}"
},
"body": {
"orderId": "{{payload.orderId}}",
"reason": "compensation"
},
"successStatusCodes": [200, 202]
}
}
],
"onSuccessCallback": {
"url": "http://notifications/api/v1/order-success",
"verb": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"orderId": "{{payload.orderId}}",
"chargeId": "{{nodes.charge-payment.response.body.chargeId}}"
},
"successStatusCodes": [200, 202, 204]
},
"onFailureCallback": {
"url": "http://notifications/api/v1/order-failed",
"verb": "POST",
"headers": {
"Content-Type": "application/json"
},
"body": {
"orderId": "{{payload.orderId}}",
"message": "saga failed or compensated"
},
"successStatusCodes": [200, 202, 204]
}
}Run Stored Definition
curl -X POST http://localhost:8080/workflows/definitions/order-saga/v1/run \
-H 'Content-Type: application/json' \
-d '{"payload": {"orderId": "ord-123", "amount": 99.5}}'Run Inline Definition
curl -X POST http://localhost:8080/workflows/run \
-H 'Content-Type: application/json' \
-d '{
"definition": {
"name": "inline",
"version": "v1",
"failureHandling": {
"type": "retry",
"maxAttempts": 1,
"delayMillis": 200
},
"entrypoint": "notify",
"nodes": [
{
"kind": "task",
"id": "notify",
"action": {
"mode": "sync",
"request": {
"url": "http://service/run",
"verb": "POST"
}
}
}
]
},
"payload": {}
}'Templates
URLs, headers and bodies are templates using Mustache {{ }} references. Since 3.0, JSON bodies use typed replacement (see JSON bodies below). Available variables:
payload.*(aliasinput.*): the run payload.nodes.<id>.response.body.*: the response of an earlier node;prev.body.*is the previous node's.step.<name or index>.body.*,sagaId,sagaName,sagaVersion.runtime.callback.url/runtime.callback.tokenin async task requests.
{
"url": "http://service/charge?reservation={{nodes.reserve-inventory.response.body.id}}",
"verb": "POST",
"headers": { "Content-Type": "application/json", "X-Tenant": "{{payload.tenant}}" },
"body": {
"amount": "{{payload.amount}}",
"note": "Order {{payload.orderId}}: {{payload.note}}"
}
}With payload {"amount": 49.9, "orderId": 7, "note": "gift"} the body is sent as {"amount": 49.9, "note": "Order 7: gift"}: amount stays a number.
{{ }} escapes values for where they land, so a value such as O'Brien "Co" \path or one with a line break reaches the service intact:
| Where | {{ }} inserts |
|---|---|
JSON body (Content-Type contains json, or no Content-Type and the body starts with { / [) | typed JSON value (3.0+; see below) |
| XML / HTML body | entity escaping |
application/x-www-form-urlencoded body | form encoding |
| URL, headers, any other body | the literal value (line breaks are removed from headers) |
Outside JSON bodies, use {{{ }}} to insert a raw value. There, values are scalars: an object or array renders as text.
JSON bodies (3.0+)
- A whole-value reference keeps its JSON type.
"{{payload.state}}"sends the object,"{{payload.count}}"the number; booleans, null and arrays likewise. - Text around a reference (
"Order {{payload.id}}") accepts scalars only. Put collections in a value of their own. - A missing reference is an error, not an empty value. A missing key, an out-of-range index or a path through null fails with the JSON field and the full reference. A task or compensation then fails without being sent or retried. An explicit
nullstill renders as JSONnull. - Initialize state explicitly. A loop cannot read its own node before that node has run. Seed counters and state in the payload.
- List indexes work with dots or brackets:
{{nodes.join.response.body.branches.0.result.price}}. - No template tricks in JSON: triple braces, sections and lambdas are rejected. Values are never re-read as templates.
{"__trama_literal_json__": VALUE}sendsVALUEas is, without rendering, which is useful for data that contains{{ }}. The receiving service never sees the envelope.
Full rules: typed JSON transport.
Switch Branching
Run an inline saga that routes to a different payment node based on paymentMethod. The choose-payment switch node evaluates a JSON Logic expression against the payload (available as payload or its alias input, alongside nodes, prev and step) and dispatches to pix-payment, card-payment, or a fallback.
curl -X POST http://localhost:8080/workflows/run \
-H 'Content-Type: application/json' \
-d '{
"definition": {
"name": "checkout-v2",
"version": "v1",
"failureHandling": { "type": "retry", "maxAttempts": 2, "delayMillis": 100 },
"entrypoint": "choose-payment",
"nodes": [
{
"kind": "switch",
"id": "choose-payment",
"cases": [
{
"name": "pix",
"when": { "==": [{ "var": "payload.paymentMethod" }, "pix"] },
"target": "pix-payment"
},
{
"name": "card",
"when": { "==": [{ "var": "payload.paymentMethod" }, "card"] },
"target": "card-payment"
}
],
"default": "fallback-payment"
},
{
"kind": "task",
"id": "pix-payment",
"action": {
"mode": "sync",
"request": {
"url": "http://payments/pix",
"verb": "POST",
"body": { "orderId": "{{payload.orderId}}" }
}
},
"compensation": { "url": "http://payments/pix/cancel", "verb": "POST" }
},
{
"kind": "task",
"id": "card-payment",
"action": {
"mode": "sync",
"request": {
"url": "http://payments/card",
"verb": "POST",
"body": { "orderId": "{{payload.orderId}}" }
}
},
"compensation": { "url": "http://payments/card/cancel", "verb": "POST" }
},
{
"kind": "task",
"id": "fallback-payment",
"action": {
"mode": "sync",
"request": { "url": "http://payments/fallback", "verb": "POST" }
}
}
]
},
"payload": { "orderId": "ord-456", "paymentMethod": "pix" }
}'Async Callback
An async task node fires its outbound request and pauses the workflow. Trama injects {{runtime.callback.url}} and {{runtime.callback.token}} into the request body. The external service must POST to the callback URL with the token header to resume execution.
curl -X POST http://localhost:8080/workflows/run \
-H 'Content-Type: application/json' \
-d '{
"definition": {
"name": "payment-async",
"version": "v1",
"failureHandling": { "type": "retry", "maxAttempts": 2, "delayMillis": 200 },
"entrypoint": "authorize",
"nodes": [
{
"kind": "task",
"id": "authorize",
"action": {
"mode": "async",
"request": {
"url": "http://payments/authorize",
"verb": "POST",
"body": {
"orderId": "{{payload.orderId}}",
"callbackUrl": "{{runtime.callback.url}}",
"callbackToken": "{{runtime.callback.token}}"
}
},
"acceptedStatusCodes": [202],
"callback": { "timeoutMillis": 30000 }
},
"compensation": { "url": "http://payments/authorize/cancel", "verb": "POST" },
"next": "capture"
},
{
"kind": "task",
"id": "capture",
"action": {
"mode": "sync",
"request": { "url": "http://payments/capture", "verb": "POST" }
}
}
]
},
"payload": { "orderId": "ord-789", "amount": 150.00 }
}'Once the external service is ready, it resumes the workflow by calling back:
curl -X POST http://localhost:8080/workflows/<executionId>/node/authorize/callback \
-H 'Content-Type: application/json' \
-H 'X-Callback-Token: <token>' \
-d '{ "status": "authorized", "authCode": "AUTH-001" }'By default any accepted callback resumes the workflow. To decide from the callback body, add JSON Logic conditions to the callback block; they see callback.body and the payload (payload / input). A callback matching failureWhen, or not matching successWhen, fails the node: it is retried per failureHandling (the async request is sent again) and compensated once retries are exhausted:
"callback": {
"timeoutMillis": 30000,
"successWhen": { "==": [{ "var": "callback.body.status" }, "authorized"] },
"failureWhen": { "==": [{ "var": "callback.body.status" }, "declined"] }
}Sleep Pause
A sleep node pauses the workflow for a configurable duration before advancing to the next node. Use it for time-based orchestration — e.g., wait 24 hours before sending a follow-up notification, or introduce a cooling-off period between steps.
curl -X POST http://localhost:8080/workflows/run \
-H 'Content-Type: application/json' \
-d '{
"definition": {
"name": "order-with-delay",
"version": "v1",
"failureHandling": { "type": "retry", "maxAttempts": 2, "delayMillis": 200 },
"entrypoint": "confirm",
"nodes": [
{
"kind": "task",
"id": "confirm",
"action": {
"mode": "sync",
"request": { "url": "http://service/confirm", "verb": "POST" }
},
"next": "wait-24h"
},
{
"kind": "sleep",
"id": "wait-24h",
"durationMillis": 86400000,
"next": "send-reminder"
},
{
"kind": "task",
"id": "send-reminder",
"action": {
"mode": "sync",
"request": { "url": "http://service/remind", "verb": "POST" }
}
}
]
},
"payload": { "orderId": "ord-999" }
}'The execution status becomes SLEEPING while waiting. To skip the remaining wait and resume immediately:
curl -X POST http://localhost:8080/workflows/<executionId>/wake
# 202 Accepted — execution re-enqueued at the next nodeSplit / Join
A split node fans out into independent branches — each one runs as its own sub-execution with the full retry, compensation, and (if needed) nested split/join pipeline, exactly like a top-level saga. The matching join node waits for every branch to reach a terminal state before continuing. A partial branch failure does not block the barrier or trigger automatic rollback — the join result reports each branch's outcome, and it is up to the workflow to react (route on failure with a switch node, for example).
curl -X POST http://localhost:8080/workflows/run \
-H 'Content-Type: application/json' \
-d '{
"definition": {
"name": "order-fulfillment",
"version": "v1",
"failureHandling": { "type": "retry", "maxAttempts": 2, "delayMillis": 200 },
"entrypoint": "fan-out",
"nodes": [
{
"kind": "split",
"id": "fan-out",
"branches": ["reserve-stock", "charge-payment"],
"join": "fan-in"
},
{
"kind": "task",
"id": "reserve-stock",
"action": {
"mode": "sync",
"request": { "url": "http://inventory/reserve", "verb": "POST", "body": { "orderId": "{{payload.orderId}}" } }
},
"compensation": { "url": "http://inventory/release", "verb": "POST" }
},
{
"kind": "task",
"id": "charge-payment",
"action": {
"mode": "sync",
"request": { "url": "http://payments/charge", "verb": "POST", "body": { "orderId": "{{payload.orderId}}" } }
},
"compensation": { "url": "http://payments/refund", "verb": "POST" }
},
{
"kind": "join",
"id": "fan-in",
"next": "confirm-order"
},
{
"kind": "task",
"id": "confirm-order",
"action": {
"mode": "sync",
"request": { "url": "http://orders/confirm", "verb": "POST" }
}
}
]
},
"payload": { "orderId": "ord-321" }
}'Branches can nest another split/join pair inside them for free — the runtime does not treat top-level and nested splits differently.
Demo Scripts
scripts/saga_demo/run_saga.py— v1 linear saga scenariosscripts/saga_demo/locust_square_chain_run.py— load testscripts/workflow_demo/run_switch_demo.py— v2 switch branching scenariosscripts/workflow_demo/run_async_demo.py— v2 async callback demoscripts/saga_demo/README.md