TramAI - governed AI workflows for Java and Kotlin

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-engine as 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:

StepPurpose
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 state
  • WorkflowCheckpointStore — store revisioned checkpoints
  • WorkflowPersistence<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

AspectStatus
ShippedYes
TestedYes
OptionalYes
Module classificationPreview

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
  • aiStep provides no framework-owned prompt-injection defense; sanitizing untrusted input is application code

Next Steps