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.
validate — structural check
The structural check parses the definition and verifies its integrity without executing anything:
- Definition is valid JSON and uses the v2
nodesformat - All node IDs are unique
- The
entrypointreferences an existing node - All
nextandtargetreferences resolve to existing nodes - Switch nodes have a
defaulttarget and at least one case - Async task nodes have a
callbackblock with a positivetimeoutMillis - Task nodes have a
urlon their request
./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
OKIf 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": { ... } }
}
}| Field | Required | Description |
|---|---|---|
payload | yes | Initial execution payload, available as {{payload.*}} in templates |
steps.<id>.status | sync nodes | HTTP status code the mock returns |
steps.<id>.body | sync nodes | Response body made available as {{nodes.<id>.response.body.*}} |
steps.<id>.acceptedStatus | async nodes | HTTP status the mock returns for the initial request (typically 202) |
steps.<id>.callbackBody | async nodes | Callback 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
| Code | Meaning |
|---|---|
0 | All checks passed |
1 | Validation 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"