Orchestration
tramai-orchestration is TramAI's optional workflow layer. It exists for backend workflows that need explicit coordination across multiple AI-backed or deterministic steps without turning TramAI into an agent framework.
It ships with a deliberately narrow, explicit contract. Its module classification in 0.6.0 is preview: the workflow-facing shapes are documented and tested, but they can still change, with a migration entry when they do.
What This Covers
- Workflow DSL:
aiStep,localStep,gateStep,branchStep,parallelStep, and the external-step shapes - Typed workflow state and explicit execution bounds
- Engine and orchestration boundaries
- Workflow-level observability
- Checkpoint, resume, and lease-based ownership
When to Use It
Add tramai-orchestration when you need:
- multi-step workflows:
plan -> execute -> review -> finalize - conditional routing:
route -> specialist -> validate - bounded fan-out: candidate generation followed by judging or reduction
- explicit checkpoint/resume boundaries
Do NOT use it for:
- autonomous agent swarms
- hidden memory systems
- peer-to-peer agent chat
- replacing
tramai-engineas the owner of provider execution, retries, or tool loops
Minimum Setup
Dependency
Pin the BOM and drop the version from the module coordinate:
dependencies {
implementation(platform("dev.tramai:tramai-bom:0.6.0"))
implementation("dev.tramai:tramai-orchestration")
}
tramai-orchestration is enablement-free: there is no opt-in annotation to add, because the workflow DSL is the opt-in.
Basic Workflow
fun buildWorkflow(planner: PlannerService, reviewer: ReviewerService) =
workflow<ReviewState>("review-workflow") {
aiStep(
name = "plan",
input = { state -> state.request },
invoke = planner::plan,
merge = { state, plan -> state.copy(plan = plan) },
)
localStep(
name = "prepare",
transform = { state, _ -> state.copy(prepared = true) },
)
aiStep(
name = "review",
input = { state -> ReviewInput(state.request, state.plan ?: error("missing plan")) },
invoke = reviewer::review,
merge = { state, result -> state.copy(result = result) },
)
}.build { state ->
state.result ?: error("missing result")
}
workflow<S>(name, definitionVersion = "1", configure) returns a WorkflowBuilder<S>. definitionVersion defaults to "1" and is the operator-controlled compatibility boundary for persisted workflows — treat a change to it as intentional.
The important property is that workflow state stays explicit and typed.
Execution Bounds
build takes a StopPolicy:
val workflow = workflow<JobState>("plan-execute-review") { /* steps */ }.build(
stopPolicy = StopPolicy(
maxStepExecutions = 16,
maxParallelBranches = 8,
),
) { state -> state.review ?: error("missing review") }
The counting rule is explicit: each top-level step consumes one maxStepExecutions slot, each branch executed inside a parallelStep consumes one additional slot, and maxParallelBranches is enforced before branch invocation.
Available Step Shapes
All step shapes are declared on the workflow builder:
| Step | Purpose |
|---|---|
localStep(name, transform) | Deterministic application logic; no provider call |
aiStep(name, input, invoke, merge) | One typed AI-backed step over extracted state |
httpStep(name, config, request) | Outbound HTTP with an explicit outbound network policy |
shellStep(...), hermesStep(...), codexStep(...), mcpStep(...) | External process and protocol steps |
pluginStep(name, type, config, merge) | Application-supplied step types resolved by an external-step executor resolver |
gateStep(name, decide) | First-class approval or policy gate |
delayStep(name, duration, unit) | Bounded wait |
branchStep(name, select, configure) | Explicit conditional routing |
parallelStep(name, items, invoke, merge) | Bounded fan-out / fan-in |
localStep(...)
Deterministic application logic. Transforms state without calling a provider.
localStep("validate") { state, _ ->
state.copy(validated = validateInput(state.request))
}
aiStep(...)
One typed AI-backed step over extracted state input. The WorkflowContext-aware overloads take an explicit ReplayPolicy; the legacy (S) -> I shape defaults to IDEMPOTENT.
aiStep(
name = "classify",
input = { state -> ClassifyInput(state.text) },
invoke = classifier::classify,
merge = { state, result -> state.copy(category = result) },
)
gateStep(...)
First-class approval or policy gates. The workflow suspends until a gate decision exists.
gateStep(
name = "approval",
decide = { state, _ ->
if (state.approved) GateDecision.allow()
else GateDecision.reject("approval required")
},
)
branchStep(...)
Explicit conditional routing. select returns the branch key; declare branches (and optionally a default) in the configuration block.
branchStep(
name = "route",
select = { state -> if (state.amount > 10_000) "high-value" else "standard" },
) {
branch("high-value") { aiStep(/* expensive analysis */) }
branch("standard") { aiStep(/* simple processing */) }
default { localStep("fallback") { state, _ -> state.copy(validated = false) } }
}
parallelStep(...)
Bounded fan-out / fan-in over an iterable extracted from state.
parallelStep(
name = "analyze-all",
items = { state -> state.candidates },
invoke = { candidate -> analyzer.analyze(candidate) },
merge = { state, results -> state.copy(analyses = results) },
)
Engine Boundary
tramai-engine owns:
- provider execution and admission
- structured parsing and structured retry
- fallback routing
- circuit breaking
- tool calling
- token budgets
- caching
- operation-level observability
tramai-orchestration owns:
- workflow state
- step ordering
- branching and bounded parallelism
- workflow-level observation
- checkpoint and resume
- optional active ownership through leases
That boundary is why orchestration fits TramAI without changing the product into an agent runtime.
Observability
Workflow execution has its own observation seam through WorkflowObserver.
The repository also includes OpenTelemetryWorkflowObserver in tramai-observability so workflow spans can sit above provider-attempt spans.
Workflow-level OpenTelemetry correlation uses the pair (workflow name, workflow id), not workflowId alone.
Observable workflow events:
- workflow start and completion
- checkpoint load/save events
- lease claim/renew/release events
- step start, completion, and failure
Persistence and Resume
Checkpoints are written at top-level workflow step boundaries. Completed top-level steps can be skipped on resume.
Persistence is storage-agnostic:
WorkflowStateCodec<S>— encode/decode typed stateWorkflowCheckpointStore— store revisioned checkpointsWorkflowPersistence<S>— wire codec + store into one workflow run
Resume is strict and refuses to guess: the checkpoint must belong to the same workflow name and workflowId, must carry the required definition metadata, must match the current definitionVersion, and must match the current definition digest. Changing workflow structure without changing definitionVersion fails loudly on resume rather than half-resuming.
For stores, codecs, file backends, JDBC backends, and lease-based ownership, see Orchestration Persistence.
Current Maturity
| Aspect | Status |
|---|---|
| Shipped | Yes |
| Tested | Yes |
| Optional | Yes |
| Module classification | Preview |
The orchestration layer is useful for explicit backend workflows today, but it is the youngest major public surface in the repository — expect the API stability boundary to apply rather than a frozen signature set.
Limitations
- checkpoints happen only at top-level workflow step boundaries
- nested branch internals are not resumed mid-step
- in-flight parallel work is not resumed mid-branch
- partially emitted provider streams are not resumed token-by-token
- no per-step timeout (a hanging step stalls the entire workflow)
- no per-step retry with backoff
aiStepprovides no framework-owned prompt-injection defense; sanitizing untrusted input is application code
Next Steps
- Orchestration Persistence — state codecs, checkpoint stores, leases, safe failure boundaries
- Scheduling — cron-triggered workflow execution
- Workflow Server — expose workflows over HTTP
- API and Binary Compatibility — what "preview" means for your upgrade path
