Trama

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 for first-time users Hands-on adoption Copy-and-run samples

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:

{
  "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 bodyentity escaping
application/x-www-form-urlencoded bodyform encoding
URL, headers, any other bodythe 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+)

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 node

Split / 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