Skip to main content
Workflow versioning allows you to update workflow logic while existing workflow instances continue running with their original code. This is essential for evolving your application without disrupting long-running workflows.

Why Versioning?

Workflows can run for extended periods (days, weeks, or months). During this time, you may need to:
  • Fix bugs in workflow logic
  • Add new features
  • Optimize performance
  • Change business rules
Versioning ensures that:
  • Existing workflow instances complete with their original logic
  • New workflow instances use the latest implementation
  • No breaking changes affect running workflows

How Versioning Works

Infinitic uses a simple naming convention for workflow versions:
  1. Create your workflow interface (unchanged)
  2. Implement multiple versions with _N suffix (where N is the version number)
  3. New workflows automatically use the highest version number
  4. Running workflows continue with their original version

Creating Versioned Workflows

1

Define the workflow interface

Create your workflow interface as usual:
interface OrderWorkflow {
  fun processOrder(orderId: String): OrderResult
}
2

Create the initial implementation

Create the first version (no suffix):
class OrderWorkflowImpl : Workflow(), OrderWorkflow {
  private val paymentService = newService(PaymentService::class.java)
  
  override fun processOrder(orderId: String): OrderResult {
    val payment = paymentService.charge(orderId)
    return OrderResult(payment)
  }
}
3

Create a new version

When you need to update the workflow, create a new version with _N suffix:
class OrderWorkflowImpl_1 : Workflow(), OrderWorkflow {
  private val paymentService = newService(PaymentService::class.java)
  private val emailService = newService(EmailService::class.java)
  
  override fun processOrder(orderId: String): OrderResult {
    val payment = paymentService.charge(orderId)
    
    // New feature: send confirmation email
    emailService.sendConfirmation(orderId)
    
    return OrderResult(payment)
  }
}

Version Selection

Infinitic automatically selects the version:
  • New workflows: Use the highest version number available
  • Running workflows: Continue with their original version
// OrderWorkflowImpl.kt
class OrderWorkflowImpl : Workflow(), OrderWorkflow {
  override fun name(): String = this::class.java.name
}

Naming Convention

The version suffix must follow this pattern:
class MyWorkflowImpl : Workflow(), MyWorkflow {
  // Initial version
}
class MyWorkflowImpl_1 : Workflow(), MyWorkflow {
  // First update
}
class MyWorkflowImpl_2 : Workflow(), MyWorkflow {
  // Second update
}
class MyWorkflowImpl_N : Workflow(), MyWorkflow {
  // Nth update
}

Version Lifecycle

Practical Example

Here’s a complete example showing workflow evolution:

Initial Implementation

// Version 0 (initial)
class PaymentWorkflowImpl : Workflow(), PaymentWorkflow {
  private val paymentService = newService(PaymentService::class.java)
  
  override fun processPayment(amount: Double): PaymentResult {
    val payment = paymentService.charge(amount)
    return PaymentResult(payment.id, payment.status)
  }
}

Version 1: Add Notification

// Version 1: Add email notification
class PaymentWorkflowImpl_1 : Workflow(), PaymentWorkflow {
  private val paymentService = newService(PaymentService::class.java)
  private val emailService = newService(EmailService::class.java)
  
  override fun processPayment(amount: Double): PaymentResult {
    val payment = paymentService.charge(amount)
    
    // New: Send email notification
    emailService.sendReceipt(payment.id)
    
    return PaymentResult(payment.id, payment.status)
  }
}

Version 2: Add Fraud Check

// Version 2: Add fraud detection
class PaymentWorkflowImpl_2 : Workflow(), PaymentWorkflow {
  private val fraudService = newService(FraudService::class.java)
  private val paymentService = newService(PaymentService::class.java)
  private val emailService = newService(EmailService::class.java)
  
  override fun processPayment(amount: Double): PaymentResult {
    // New: Check for fraud
    val fraudCheck = fraudService.analyze(amount)
    if (fraudCheck.isFraudulent) {
      return PaymentResult.rejected("Fraud detected")
    }
    
    val payment = paymentService.charge(amount)
    emailService.sendReceipt(payment.id)
    
    return PaymentResult(payment.id, payment.status)
  }
}

Migration Strategies

Strategy 1: Wait for Natural Completion

Allow old workflows to complete naturally:
// Keep old versions until all instances complete
// OrderWorkflowImpl.kt - keep for running workflows
// OrderWorkflowImpl_1.kt - current version
// OrderWorkflowImpl_2.kt - deploy new version

Strategy 2: Graceful Deprecation

Mark old versions but keep them available:
@Deprecated("Use OrderWorkflowImpl_2 instead")
class OrderWorkflowImpl_1 : Workflow(), OrderWorkflow {
  // Keep for running workflows only
}

class OrderWorkflowImpl_2 : Workflow(), OrderWorkflow {
  // New implementation
}

Strategy 3: Feature Flags

Use inline logic to enable features conditionally:
class OrderWorkflowImpl_1 : Workflow(), OrderWorkflow {
  private val featureService = newService(FeatureService::class.java)
  
  override fun processOrder(orderId: String): OrderResult {
    val payment = paymentService.charge(orderId)
    
    // Check feature flag
    val sendEmail = inline {
      featureService.isEnabled("email-confirmation")
    }
    
    if (sendEmail) {
      emailService.sendConfirmation(orderId)
    }
    
    return OrderResult(payment)
  }
}

Testing Different Versions

You can test specific versions by creating instances directly:
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

class VersioningTest : StringSpec({
  
  "Should use latest version by default" {
    val workflow = client.newWorkflow(MyWorkflow::class.java)
    val result = workflow.name()
    result shouldBe "com.example.MyWorkflowImpl_2"
  }
  
  "Old version still works" {
    // You can't directly instantiate old versions from the client,
    // but running workflows continue with their version
    val deferred = client.dispatch(workflow::process)
    // This workflow will use whatever version it was started with
  }
})

Version Cleanup

Once all workflows of an old version have completed:
1

Verify no active instances

Check that no workflows are using the old version:
# Query your workflow state storage
# Ensure no instances reference old version
2

Remove old implementation

Delete or archive the old version file:
# Remove OrderWorkflowImpl.kt
# Keep OrderWorkflowImpl_1.kt and OrderWorkflowImpl_2.kt
3

Update documentation

Document the version history and changes

Best Practices

Don’t delete old versions while any instances are running:
// ✅ Good - keep all versions
// OrderWorkflowImpl.kt (v0 - 5 running instances)
// OrderWorkflowImpl_1.kt (v1 - 120 running instances)
// OrderWorkflowImpl_2.kt (v2 - current, new instances)

// ❌ Bad - deleting v0 while instances still running
// OrderWorkflowImpl_1.kt
// OrderWorkflowImpl_2.kt
Add comments explaining what changed in each version:
/**
 * Version 2: Added fraud detection before payment processing
 * - Calls FraudService before charging
 * - Returns rejection if fraud detected
 * - Maintains backward compatibility with v1 return type
 */
class PaymentWorkflowImpl_2 : Workflow(), PaymentWorkflow {
  // Implementation
}
Ensure new versions maintain interface compatibility:
// Interface must remain stable across versions
interface MyWorkflow {
  fun process(input: String): Result
}

// All versions must implement the same interface
class MyWorkflowImpl : Workflow(), MyWorkflow
class MyWorkflowImpl_1 : Workflow(), MyWorkflow
class MyWorkflowImpl_2 : Workflow(), MyWorkflow
Always increment from the highest existing version:
// ✅ Correct sequence
MyWorkflowImpl      // v0
MyWorkflowImpl_1    // v1
MyWorkflowImpl_2    // v2
MyWorkflowImpl_3    // v3

// ❌ Wrong - skipping versions
MyWorkflowImpl      // v0
MyWorkflowImpl_5    // Don't skip to v5

Monitoring Versions

Track which versions are in use:
class MonitoringWorkflow : Workflow(), MonitoringWorkflowInterface {
  override fun logVersion() {
    val version = this::class.java.simpleName
    inline {
      logger.info("Running workflow version: $version")
    }
  }
}

Common Pitfalls

Don’t change the interfaceThe interface must remain stable across versions:
// ❌ Wrong - changing interface signature
interface MyWorkflow {
  // v0
  fun process(input: String): Result
  
  // v1 - DON'T DO THIS
  fun process(input: String, extra: Int): Result
}

// ✅ Correct - stable interface, different implementation
interface MyWorkflow {
  fun process(input: String): Result
}

class MyWorkflowImpl : Workflow(), MyWorkflow {
  override fun process(input: String): Result {
    // v0 implementation
  }
}

class MyWorkflowImpl_1 : Workflow(), MyWorkflow {
  override fun process(input: String): Result {
    // v1 implementation (different logic, same signature)
  }
}
Don’t delete running versionsKeep all versions that have running instances:
// ❌ Wrong - deleting while instances are running
// Delete OrderWorkflowImpl.kt (but 10 instances still running!)

// ✅ Correct - keep until all instances complete
// Keep OrderWorkflowImpl.kt until all v0 instances finish
// Then safely delete the file

Next Steps

Workflow Overview

Learn more about workflow fundamentals

Defining Workflows

Create new workflows

Testing

Test your workflows and versions

Deployment

Deploy workflow versions

Build docs developers (and LLMs) love