Why TramAI Workflows
If you are evaluating AI orchestration on the JVM, this page explains why TramAI workflows are a fundamentally different approach than chain or pipeline abstractions.
Workflows, Not Chains
Most JVM AI frameworks offer chains or pipelines: you wire together abstract step objects, push state through an opaque context map, and pray it works end to end. If the chain fails midway, you restart from scratch.
TramAI workflows take a different approach. A workflow is an explicit, typed, checkpointed state machine. You define the steps, the state type, and the result selector in Kotlin code. The framework handles persistence, replay, guardrails, and distributed execution.
The Problem with Pipeline Abstractions
Opaque composition. Chains are abstract objects you wire together but do not control. The execution model is buried inside the framework.
Untyped state. State passes through untyped context maps or string Dicts. There is no compiler to catch a missing key or a type mismatch.
No persistence. A chain runs once. If it fails at step 7 of 10, you restart from step 1.
No execution guardrails. No built-in stop policies, no definition-compatibility checks. A runaway chain loops until the process runs out of memory.
No security. No SSRF protection on HTTP calls, no shell command validation, no prompt injection defense.
How TramAI Workflows Fix This
TramAI workflows provide a typed, deterministic, and durable execution model for multi-step AI and integration logic.
Twelve Explicit Step Types
TramAI's workflow DSL defines 12 step types, each with a clear purpose:
| Step | Purpose |
|---|---|
aiStep | Call any AI-backed Kotlin function |
localStep | Deterministic state transform |
gateStep | Conditional pass/block with explicit reason |
delayStep | Timed wait with checkpointed wakeup |
branchStep | Route execution by state value |
parallelStep | Fan-out, parallel invoke, merge results |
httpStep | HTTP request with retry and SSRF protection |
shellStep | Shell command with allowlist/denylist and timeout |
hermesStep | Prompt step via the Hermes agent CLI |
codexStep | Prompt step via the Codex CLI |
mcpStep | MCP protocol tool call with server validation |
pluginStep | External plugin step executor |
Typed State Generics
Workflows use a typed state generic S. Your data class is the state. The compiler verifies every transform:
data class SupportTicket(
val customerId: String,
val ticket: Ticket,
val customerData: CustomerData? = null,
val category: String? = null,
val approved: Boolean = false,
)
val triageWorkflow = workflow<SupportTicket>("triage-workflow") {
// ...
}.build { state -> TicketResult(state.ticket.id, state.category) }
Built-in Checkpoint and Resume
Workflows persist their state to a WorkflowCheckpointStore after every top-level step. If a process dies mid-run, you resume from the last checkpoint, not from the beginning:
val result = triageWorkflow.run(
initialState = ticket,
persistence = WorkflowPersistence(
checkpointStore = JdbcWorkflowCheckpointStore(dataSource),
stateCodec = JsonStateCodec(),
),
)
// On resume after crash:
val result = triageWorkflow.resume(
context = WorkflowContext(workflowId = "run-123"),
persistence = /* same config */,
)
Checkpoints carry a SHA-256 digest of the full step topology. If the workflow definition changed between save and resume, TramAI throws a WorkflowResumeException -- no silent data corruption.
Explicit Replay Policy
Every step has a ReplayPolicy that tells the runtime how to handle this step during resume or worker recovery. There are four values:
- PURE -- deterministic, always safe to replay (used by localStep, gateStep, delayStep, branchStep)
- IDEMPOTENT -- safe to re-execute because the step itself is idempotent (used by GET/HEAD/PUT/DELETE httpStep, and legacy aiStep by default)
- EXTERNALLY_IDEMPOTENT -- needs an external idempotency key, auto-detected from Idempotency-Key headers on POST/PATCH httpStep calls
- NON_REPLAYABLE -- skip this step during resume (default for hermesStep, codexStep, mcpStep, shellStep, and new aiStep overloads with WorkflowContext access)
aiStep differs from other steps: it does not apply the framework-owned prompt defenses used by hermesStep and codexStep. If your invoke calls an LLM, prompt injection handling belongs in that application-owned invocation path.
Framework-Agnostic Steps
Every step calls YOUR code, not framework types. aiStep takes an invoke lambda that can call any Kotlin function -- your own service, a TramAI @AiService, or a raw HTTP client. The workflow framework does not own the AI invocation path.
Built-in Security
Hermes, Codex, HTTP, shell, and MCP steps have security built in:
- Prompt sanitization via
DefaultPromptSanitizer-- detects delimiter tricks, jailbreak fragments, and control characters - Instruction defense via
DefaultInstructionDefense-- wraps prompts with defensive system boundaries - Output validation via
DefaultOutputValidator-- rejects responses that leak system instructions or probe boundaries - Shell command policies --
allowedCommandsanddeniedCommandssets onShellStepConfig - SSRF protection --
allowedHostsonHttpStepConfig, private address rejection - MCP tool allowlist --
toolAllowlistonMcpStepConfigcontrols which tools a step may call
Distributed Execution with TramaiWorker
Workflows are not limited to a single JVM. TramaiWorker provides distributed execution with:
- Lease-based concurrency control --
WorkflowLeasewith claim/renew/release lifecycle prevents two workers from executing the same workflow - Lease-fenced checkpoint stores --
WorkflowLeaseCheckpointFenceatomically gates checkpoint writes behind active lease ownership, preventing split-brain scenarios - Worker registry with heartbeat tracking -- workers register with pool name, version, capability labels, and host; stale workers are detected via missed heartbeats
- Partition-based work assignment --
ModHashPartitionStrategy(default) or customPartitionAssignmentStrategyroutes workflows deterministically across the worker pool - Step attempt tracking --
StepAttemptRecordStorerecords every step attempt with replay policy, worker ID, and lease token for crash recovery - Graceful shutdown with drain timeout -- workers stop accepting new work, drain active executions, and unregister before stopping
val worker = TramaiWorker(
config = WorkerConfig(workerId = "worker-1", poolName = "default"),
leaseStore = JdbcWorkflowLeaseStore(dataSource),
checkpointStore = JdbcWorkflowCheckpointStore(dataSource),
workflowRegistry = mapOf("triage-workflow" to triageWorkflow),
)
worker.start()
Execution Guardrails
StopPolicy sets hard bounds on every workflow run:
val workflow = workflow<MyState>("processing") {
// step definitions
}.build(
stopPolicy = StopPolicy(
maxStepExecutions = 500,
maxParallelBranches = 32,
),
) { state -> state.toResult() }
Attempting to exceed these bounds throws WorkflowLimitExceededException.
Observability Seam
WorkflowObserver provides lifecycle hooks for every step and every workflow event -- started, completed, failed, scheduled ticks, events. No manual instrumentation needed.
Comparison: TramAI Workflows vs Chain/Pipeline Frameworks
| Dimension | LangChain4j Chains / Spring AI Pipelines | TramAI Workflows |
|---|---|---|
| Step types | Load/Split/Embed/Store (document chains), Function Calling (agents) | aiStep, localStep, gateStep, delayStep, branchStep, parallelStep, httpStep, shellStep, hermesStep, codexStep, mcpStep, pluginStep |
| State typing | Untyped context map / Dict | Typed generic S -- your data class |
| Persistence | None | Checkpoint/resume via WorkflowPersistence |
| Failure recovery | Restart entire chain | Resume from last checkpoint |
| Distributed execution | Deployment-dependent | TramaiWorker with lease fencing, worker registry, partition strategy |
| Security features | None | SSRF protection, command allowlist, prompt sanitization, tool allowlist |
| Step-level observability | Manual instrumentation | Built-in WorkflowObserver lifecycle hooks |
| Execution guardrails | None | StopPolicy (max steps, max branches), definition-compatibility digest |
| Framework dependency | Code depends on chain types | Steps call YOUR code, not framework types |
| Scheduling | Manual | WorkflowScheduleDefinition in DSL |
Concrete Example: Support Ticket Triage
This workflow uses httpStep, aiStep, branchStep, and gateStep together to triage support tickets:
data class SupportTicket(
val customerId: String,
val ticket: Ticket,
val customerData: CustomerData? = null,
val category: String? = null,
val priority: String? = null,
val approved: Boolean = false,
)
val triageWorkflow = workflow<SupportTicket>("triage-workflow") {
httpStep(
name = "fetch-customer-data",
request = { state, _ ->
HttpRequest(
method = "GET",
url = "https://api.example.com/customers/${state.customerId}",
)
},
merge = { state, response, _ ->
state.copy(customerData = parseCustomerData(response.body ?: ""))
},
)
aiStep(
name = "classify",
input = { state -> ClassifyInput(state.ticket) },
invoke = classifier::classify,
merge = { state, result -> state.copy(category = result.category, priority = result.priority) },
)
branchStep(
name = "route",
select = { state -> state.category ?: "unknown" },
configure = {
branch("billing") {
localStep(name = "apply-billing-rules") { state, _ ->
state.copy(approved = state.ticket.value < 500)
}
}
branch("technical") {
aiStep(
name = "suggest-fix",
input = { state -> DiagnoseInput(state.ticket) },
invoke = diagnostics::suggestFix,
merge = { state, suggestion ->
state.copy(ticket = state.ticket.copy(suggestion = suggestion))
},
)
}
default {
localStep(name = "mark-unclassified") { state, _ ->
state.copy(category = "unrouted")
}
}
},
)
gateStep(
name = "require-approval",
decide = { state, _ ->
if (state.ticket.value > 1000 && !state.approved) {
GateDecision.reject("Ticket value ${state.ticket.value} exceeds $1000 threshold without approval")
} else {
GateDecision.allow()
}
},
)
}.build { state -> TicketResult(state.ticket.id, state.category) }
Java API
TramAI workflows are also buildable from Java via the TramaiWorkflow builder API. The Kotlin DSL provides the most concise syntax, but the underlying Workflow<S, R> class is pure JVM bytecode.
Honest Limitations
TramAI workflows are not a silver bullet. Here is what they do not do:
Workflows are not autonomous agents. They are explicit, deterministic state machines. There is no open-ended reasoning loop, no chain-of-thought planner, no agent that decides its own next action. If you need an agent that reflects, plans, and chooses tools dynamically, TramAI workflows are not that.
Smaller ecosystem. LangChain4j has more community-built modules for document loading, splitting, embedding, vector stores, and memory. TramAI focuses on the orchestration core. Additional modules are growing but not at parity.
No visual dashboard. WorkflowObserver provides the instrumentation seam for monitoring, but no built-in UI exists. You feed the observer events into your own monitoring stack.
No autonomous retry on business logic failure. The framework retries infrastructure failures (network, timeout). If a step merges state in a way your application considers a business failure, that is your code's responsibility.
Where to Go Next
- Get Started -- build your first workflow in 5 minutes
- Workflow API Reference -- full step DSL reference (v0.3.1)
- Structured Output -- typed AI calls with automatic schema generation
