TramAI

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:

StepPurpose
aiStepCall any AI-backed Kotlin function
localStepDeterministic state transform
gateStepConditional pass/block with explicit reason
delayStepTimed wait with checkpointed wakeup
branchStepRoute execution by state value
parallelStepFan-out, parallel invoke, merge results
httpStepHTTP request with retry and SSRF protection
shellStepShell command with allowlist/denylist and timeout
hermesStepPrompt step via the Hermes agent CLI
codexStepPrompt step via the Codex CLI
mcpStepMCP protocol tool call with server validation
pluginStepExternal 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 -- allowedCommands and deniedCommands sets on ShellStepConfig
  • SSRF protection -- allowedHosts on HttpStepConfig, private address rejection
  • MCP tool allowlist -- toolAllowlist on McpStepConfig controls 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 -- WorkflowLease with claim/renew/release lifecycle prevents two workers from executing the same workflow
  • Lease-fenced checkpoint stores -- WorkflowLeaseCheckpointFence atomically 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 custom PartitionAssignmentStrategy routes workflows deterministically across the worker pool
  • Step attempt tracking -- StepAttemptRecordStore records 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

DimensionLangChain4j Chains / Spring AI PipelinesTramAI Workflows
Step typesLoad/Split/Embed/Store (document chains), Function Calling (agents)aiStep, localStep, gateStep, delayStep, branchStep, parallelStep, httpStep, shellStep, hermesStep, codexStep, mcpStep, pluginStep
State typingUntyped context map / DictTyped generic S -- your data class
PersistenceNoneCheckpoint/resume via WorkflowPersistence
Failure recoveryRestart entire chainResume from last checkpoint
Distributed executionDeployment-dependentTramaiWorker with lease fencing, worker registry, partition strategy
Security featuresNoneSSRF protection, command allowlist, prompt sanitization, tool allowlist
Step-level observabilityManual instrumentationBuilt-in WorkflowObserver lifecycle hooks
Execution guardrailsNoneStopPolicy (max steps, max branches), definition-compatibility digest
Framework dependencyCode depends on chain typesSteps call YOUR code, not framework types
SchedulingManualWorkflowScheduleDefinition 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