TramAI - governed AI workflows for Java and Kotlin

Adding a Workflow Step

Workflow steps are a builder DSL over a sealed internal step contract. There is no public WorkflowStep interface for external implementation. Decide first which of the three changes you are making:

ChangeMechanism
A new built-in stepNew sealed subclass of InternalWorkflowStep<S> + a DSL function on AbstractWorkflowBuilder<S> + exhaustive-when updates
A new external stepRegister an ExternalStepExecutor with ExternalStepExecutorRegistry
Reuse an existing step shapeNo code change; compose with branchStep, parallelStep, or pluginStep

The Extension Points

Runtime step contract — InternalWorkflowStep<S> (sealed), tramai-orchestration/src/main/kotlin/dev/tramai/orchestration/WorkflowStepExecution.kt:80-89:

  • val name
  • suspensionMode (defaults to NONE)
  • suspend fun execute(request: WorkflowStepExecutionRequest<S>): WorkflowStepExecutionResult<S>

The result is sealed: Completed(state) or Suspended (WorkflowStepExecution.kt:26-32).

Builder DSL — WorkflowBuilder.kt:16 extends AbstractWorkflowBuilder<S> (:71, which owns the step list at :72, appendStep at :377, and stepsSnapshot at :384). Step functions: localStep :75, aiStep (4 overloads) :96-161, httpStep :180, shellStep :190, hermesStep :206/:220, codexStep :235/:249, mcpStep :264/:280, pluginStep :297/:310, gateStep :324, delayStep :334, branchStep :349, parallelStep :363.

External step SPI — ExternalStepExecutorRegistry.register(factory) (WorkflowStepExecution.kt:195), with ExternalStepExecutorFactory :173, ExternalStepExecutor :178, ExternalStepExecutorResolver :186-190. A missing executor raises ExternalStepExecutorNotRegisteredException (:182).

Files That Change

  1. New step class — a sealed subclass of InternalWorkflowStep<S> in tramai-orchestration/src/main/kotlin/dev/tramai/orchestration/. Templates: LocalWorkflowStep :93, AiWorkflowStep :104, GateWorkflowStep :123, PluginWorkflowStep :140; DelayWorkflowStep lives in WorkflowDelayCoordinator.kt:23.
  2. DSL function — added to AbstractWorkflowBuilder in WorkflowBuilder.kt.
  3. Exhaustive-when wiring — compiler-enforced, and the only reason a "new step" is more than a class:
    • replay descriptor when — Workflow.kt:120-136
    • definition digest — WorkflowDefinitionCompatibility.kt:19-46 (SHA-256 over the canonical definition)
    • worker binding — WorkflowBindingRegistry.bind(workflow, persistence) (WorkflowBindingRegistry.kt:82)
    • suspension rule — WorkflowBuilder.kt:484-496 (nested suspension rejected)
  4. Tests in tramai-orchestration/src/test/kotlin/.
  5. docs/workflow-api-stability-boundary.md — a new public DSL addition must be classified there.

Mandatory Contract Tests

ContractTest
Step-level behaviourFollow the WorkflowStepFailureBoundaryTest / WorkflowReplayDecisionPolicyTest patterns
Definition digestWorkflowDefinitionDigestGoldenTest — a digest change must be deliberate and updated in the same commit
Binary compatibility fixtureBinaryCompatibilityFixtureTest against src/test/resources/binary-compat/BinaryCompatFixture.kt
Checkpoint migrationWorkflowCheckpointLegacyMigrationContractTest
Suspension mode guardWorkflowStepExecutionArchitectureTest — ASM check that the suspensionMode getter returns the constant directly
CancellationverifyCancellationSafety (cancellation scanner) plus WorkflowCancellationContractTest
Public DSL classificationverifyWorkflowApiStabilityBoundary

If you also add a store, add its TCK too: WorkflowCheckpointStoreTck.kt, WorkflowLeaseStoreTck.kt, WorkflowLeaseCheckpointFenceTck.kt in tramai-testing, plus their enrolment guards. See Adding a Store.

Compatibility

  • Definition digest changes are breaking. The SHA-256 canonical digest drives recovery and fencing, so a new step with different digest semantics invalidates in-flight checkpoints. Update the golden test deliberately.
  • Public DSL is public API. New DSL functions are covered by docs/workflow-api-stability-boundary.md, enforced by verifyWorkflowApiStabilityBoundary, and apiCheck enforces the Binary Compatibility Validator dump.
  • Replay must round-trip. The replayDescriptor when must handle the new step, or replay of in-flight workflows fails.
  • A new suspending step is a contract change. Suspension semantics are frozen by the ASM guard; only suspensionMode = TOP_LEVEL_CHECKPOINT steps may suspend, and only at top level.

Verification

./gradlew :tramai-orchestration:test
./gradlew verifyWorkflowApiStabilityBoundary
./gradlew verifyCancellationSafety
./gradlew apiCheck
./gradlew verifyPr
./gradlew verifyChangePolicy -PchangeClass=runtime-behaviour   # or public-api

What Not To Change In This Pull Request

  • WorkflowExecutionSupervisor, WorkerLifecycleController, WorkerShutdownCoordinator — steps execute through the runner and never touch these directly.
  • Store SPIs (WorkflowCheckpointStore, WorkflowLeaseStore, StepAttemptRecordStore) — they change only when you add a store implementation.
  • config/quality/0.6.0-baseline.json — never edited in the same pull request.

Every step routes through WorkflowStepExecutor.executeStep, which owns the step counter budget, observer events, failure sanitisation (safe public errors, no internal exception leakage), and CancellationException passthrough.

Common Mistakes

SymptomCause
Build fails after adding the step classAn exhaustive-when wiring point was not updated
verifyWorkflowApiStabilityBoundary failsThe new DSL function was not classified in docs/workflow-api-stability-boundary.md
WorkflowDefinitionDigestGoldenTest failsExpected for a digest change — update the golden deliberately, do not delete the assertion
verify060Architecture failsNew dependency edge violates config/quality/module-boundaries.yml (published → internal, published → applications-examples, published → excluded, self-edges, cycles)
WorkflowStepExecutionArchitectureTest failsA suspending step was added, or suspensionMode no longer returns the constant directly