Trama

Tools

Trama ships a CLI tool that lets you validate a saga definition and simulate its execution entirely offline — no running orchestrator needed. Use it to catch structural errors and logic bugs before deploying a definition, or to gate merges in CI.

Best for developers Offline tooling CI-friendly

validate — structural check

The structural check parses the definition and verifies its integrity without executing anything:

./gradlew trama-validate --args="definition.json --validate-only"

Output:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  trama validate  ·  checkout-full-demo / v1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[1/1] Structural validation
  ✓ 5 nodes  (3 task, 1 switch, 1 async)
  ✓ all node references resolve

OK

If errors are found, each one is printed and the process exits with code 1:

[1/1] Structural validation
  ✗ node 'pix-paymet' referenced by 'choose-payment' does not exist
  ✗ switch 'choose-payment' has no default target

2 error(s) found. Fix them before running a simulation.

validate — dry-run simulation

Provide a scenario file to run a second phase: the tool walks the node graph using mock responses, renders all Mustache templates with real variable substitution, evaluates JSON Logic switch conditions, and prints the full execution trace. This catches bugs that structural checks cannot — wrong variable names in templates, switch conditions that never match, or a path that terminates unexpectedly.

The simulator reuses the same template engine and condition evaluator as the production orchestrator, so the trace reflects exactly what would happen at runtime.

./gradlew trama-validate --args="definition.json scenario.json"

Example: sync flow (PIX payment)

Given this definition (abbreviated):

{
  "name": "checkout-full-demo",
  "version": "v1",
  "entrypoint": "validate",
  "nodes": [
    {
      "kind": "task",
      "id": "validate",
      "action": {
        "mode": "sync",
        "request": {
          "url": "http://localhost:5003/step/validate",
          "verb": "POST",
          "body": { "orderId": "{{payload.orderId}}", "amount": "{{payload.amount}}" }
        }
      },
      "next": "choose-payment"
    },
    {
      "kind": "switch",
      "id": "choose-payment",
      "cases": [
        { "name": "pix",  "when": { "==": [{ "var": "input.paymentMethod" }, "pix"]  }, "target": "pix-payment"  },
        { "name": "card", "when": { "==": [{ "var": "input.paymentMethod" }, "card"] }, "target": "card-payment" }
      ],
      "default": "pix-payment"
    },
    {
      "kind": "task",
      "id": "pix-payment",
      "action": {
        "mode": "sync",
        "request": {
          "url": "http://localhost:5003/step/pix-payment",
          "verb": "POST",
          "body": { "orderId": "{{payload.orderId}}", "method": "pix" }
        }
      },
      "next": "notify"
    },
    {
      "kind": "task",
      "id": "notify",
      "action": {
        "mode": "sync",
        "request": {
          "url": "http://localhost:5003/step/notify",
          "verb": "POST",
          "body": { "orderId": "{{payload.orderId}}" }
        }
      }
    }
  ]
}

And this scenario:

{
  "payload": {
    "orderId": "ord-demo-001",
    "amount":  "99.90",
    "paymentMethod": "pix"
  },
  "steps": {
    "validate":    { "status": 200, "body": { "valid": true } },
    "pix-payment": { "status": 200, "body": { "charged": true, "method": "pix" } },
    "notify":      { "status": 200, "body": { "notified": true } }
  }
}

Output:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  trama validate  ·  checkout-full-demo / v1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[1/2] Structural validation
  ✓ 5 nodes  (3 task, 1 switch, 1 async)
  ✓ all node references resolve

[2/2] Execution simulation
  payload: {orderId=ord-demo-001, amount=99.90, paymentMethod=pix}

  → validate             [sync]
      POST    http://localhost:5003/step/validate
      body    {"orderId":"ord-demo-001","amount":"99.90"}
      ← 200   ✓  {"valid":true}

  → choose-payment       [switch]
      matched "pix"  →  pix-payment

  → pix-payment          [sync]
      POST    http://localhost:5003/step/pix-payment
      body    {"orderId":"ord-demo-001","method":"pix"}
      ← 200   ✓  {"charged":true,"method":"pix"}

  → notify               [sync]
      POST    http://localhost:5003/step/notify
      body    {"orderId":"ord-demo-001"}
      ← 200   ✓  {"notified":true}

  ✓ SUCCEEDED

All checks passed.

Example: async node (card payment)

For async task nodes the simulator renders the outbound request, marks it as [async], injects a placeholder callback body from the scenario, and pauses — mirroring exactly what the production orchestrator does:

  → card-payment         [async]
      POST    http://localhost:5003/async-step/card-payment
      body    {"orderId":"ord-demo-002","method":"card",
               "callbackUrl":"https://trama/sagas/abc/node/card-payment/callback",
               "callbackToken":"<hmac-signed-token>"}
      ← 202   accepted (execution pauses here in production)
      callback ← {"status":"approved","authCode":"AUTH-9871"}

The scenario entry for an async node uses acceptedStatus and callbackBody instead of status/body:

"card-payment": {
  "acceptedStatus": 202,
  "callbackBody": { "status": "approved", "authCode": "AUTH-9871" }
}

Scenario file format

{
  "payload": {
    "<key>": "<value>"
  },
  "steps": {
    "<nodeId>": { "status": 200, "body": { ... } },
    "<asyncNodeId>": { "acceptedStatus": 202, "callbackBody": { ... } }
  }
}
FieldRequiredDescription
payloadyesInitial execution payload, available as {{payload.*}} in templates
steps.<id>.statussync nodesHTTP status code the mock returns
steps.<id>.bodysync nodesResponse body made available as {{nodes.<id>.response.body.*}}
steps.<id>.acceptedStatusasync nodesHTTP status the mock returns for the initial request (typically 202)
steps.<id>.callbackBodyasync nodesCallback payload injected when simulation resumes after the async pause

Only nodes that actually execute need entries in steps. Unreached branches (e.g. the card path when testing PIX) are ignored.

Exit codes & CI usage

CodeMeaning
0All checks passed
1Validation error or simulation failed

Because the tool exits non-zero on failure, it integrates naturally with any CI system. Example GitHub Actions step:

- name: Validate saga definition
  run: |
    ./gradlew trama-validate \
      --args="definitions/checkout.json scenarios/checkout-pix.json"

Add --validate-only if you want the structural check only (faster, no scenario file needed):

- name: Structural check
  run: ./gradlew trama-validate --args="definitions/checkout.json --validate-only"