Skip to main content
Workflows orchestrate services by calling their methods. Infinitic provides both synchronous and asynchronous execution patterns.

Creating Service Stubs

Before calling a service, create a stub using the newService() method:
class MyWorkflow : Workflow(), MyWorkflowInterface {
  // Create service stub
  private val emailService = newService(EmailService::class.java)
  
  override fun sendWelcomeEmail(userId: String) {
    // Call service method
    emailService.send(userId, "Welcome!")
  }
}

Synchronous Calls

The simplest way to call a service is synchronously. The workflow waits for the service to complete before continuing:
class OrderWorkflow : Workflow(), OrderWorkflowInterface {
  private val inventoryService = newService(InventoryService::class.java)
  private val paymentService = newService(PaymentService::class.java)
  
  override fun processOrder(orderId: String): OrderResult {
    // Sequential execution
    val items = inventoryService.getItems(orderId)
    val total = items.sumOf { it.price }
    val payment = paymentService.charge(orderId, total)
    
    return OrderResult(orderId, payment)
  }
}

Asynchronous Calls with dispatch()

For parallel execution or when you don’t need the result immediately, use dispatch() to call services asynchronously:
class OrderWorkflow : Workflow(), OrderWorkflowInterface {
  private val emailService = newService(EmailService::class.java)
  private val smsService = newService(SmsService::class.java)
  private val pushService = newService(PushService::class.java)
  
  override fun notifyUser(userId: String, message: String) {
    // Start all notifications in parallel
    val emailDeferred = dispatch(emailService::send, userId, message)
    val smsDeferred = dispatch(smsService::send, userId, message)
    val pushDeferred = dispatch(pushService::send, userId, message)
    
    // Wait for all to complete
    emailDeferred.await()
    smsDeferred.await()
    pushDeferred.await()
  }
}

Working with Deferred Objects

When you call dispatch(), you get a Deferred<T> object that represents the future result.

await()

Call await() to get the result (blocks until complete):
class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val service = newService(DataService::class.java)
  
  override fun processData(input: String): String {
    val deferred = dispatch(service::process, input)
    
    // Do other work here...
    
    // Wait for result
    val result = deferred.await()
    return result
  }
}

status()

Check the status of a deferred operation:
import io.infinitic.workflows.DeferredStatus

class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val service = newService(MyService::class.java)
  
  override fun checkProgress(): String {
    val deferred = dispatch(service::longRunningTask)
    
    return when (deferred.status()) {
      DeferredStatus.ONGOING -> "Still running"
      DeferredStatus.COMPLETED -> "Completed"
      DeferredStatus.FAILED -> "Failed"
      DeferredStatus.CANCELED -> "Canceled"
      DeferredStatus.TIMED_OUT -> "Timed out"
      DeferredStatus.UNKNOWN -> "Unknown"
    }
  }
}

Convenience Status Methods

val deferred = dispatch(service::process, input)

if (deferred.isOngoing()) {
  // Still running
}

if (deferred.isCompleted()) {
  // Completed successfully
}

if (deferred.isFailed()) {
  // Failed with error
}

if (deferred.isCanceled()) {
  // Was canceled
}

Parallel Execution

Dispatch multiple tasks and wait for all to complete:
class DataWorkflow : Workflow(), DataWorkflowInterface {
  private val apiService = newService(ApiService::class.java)
  
  override fun fetchAllData(userId: String): CombinedData {
    // Start all requests in parallel
    val profileDeferred = dispatch(apiService::getProfile, userId)
    val ordersDeferred = dispatch(apiService::getOrders, userId)
    val prefsDeferred = dispatch(apiService::getPreferences, userId)
    
    // Wait for all results
    val profile = profileDeferred.await()
    val orders = ordersDeferred.await()
    val prefs = prefsDeferred.await()
    
    return CombinedData(profile, orders, prefs)
  }
}

Combining Deferred with AND

Use and() to wait for multiple deferred objects to complete:
import io.infinitic.workflows.and

class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val service = newService(MyService::class.java)
  
  override fun processMultiple(): List<String> {
    val d1 = dispatch(service::process, "input1")
    val d2 = dispatch(service::process, "input2")
    val d3 = dispatch(service::process, "input3")
    
    // Wait for all three to complete
    val results = (d1 and d2 and d3).await()
    return results // List<String>
  }
  
  override fun processMany(): List<String> {
    val deferreds = (1..10).map { 
      dispatch(service::process, "input$it")
    }
    
    // Wait for all
    return deferreds.and().await()
  }
}

Racing with OR

Use or() to wait for the first deferred to complete:
import io.infinitic.workflows.or

class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val primaryService = newService(PrimaryService::class.java)
  private val backupService = newService(BackupService::class.java)
  
  override fun fetchWithFallback(query: String): String {
    val primary = dispatch(primaryService::fetch, query)
    val backup = dispatch(backupService::fetch, query)
    
    // Return whichever completes first
    return when (val result = (primary or backup).await()) {
      is String -> result
      else -> throw Exception("Unexpected result type")
    }
  }
}

Service Stubs with Tags

Route service calls to specific workers using tags:
class MyWorkflow : Workflow(), MyWorkflowInterface {
  // Regular service
  private val regularService = newService(ProcessingService::class.java)
  
  // Service routed to high-priority workers
  private val priorityService = newService(
    ProcessingService::class.java,
    tags = setOf("priority-high", "gpu-enabled")
  )
  
  override fun processData(data: Data, isPriority: Boolean) {
    if (isPriority) {
      priorityService.process(data)
    } else {
      regularService.process(data)
    }
  }
}

Service Stubs with Metadata

Pass metadata with service calls for tracing or context:
class MyWorkflow : Workflow(), MyWorkflowInterface {
  override fun processOrder(orderId: String) {
    // Create service with metadata
    val service = newService(
      OrderService::class.java,
      meta = mapOf(
        "orderId" to orderId.toByteArray(),
        "workflowId" to workflowId.toByteArray(),
        "traceId" to generateTraceId().toByteArray()
      )
    )
    
    service.process(orderId)
  }
}

Void Methods

For service methods that return void or Unit, use dispatchVoid():
class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val loggingService = newService(LoggingService::class.java)
  
  override fun logEvent(event: String) {
    // Dispatch void method
    val deferred = dispatchVoid(loggingService::log, event)
    
    // Wait for completion
    deferred.await()
  }
}

Error Handling

Service errors propagate to the workflow. Handle them with try-catch:
class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val paymentService = newService(PaymentService::class.java)
  private val notificationService = newService(NotificationService::class.java)
  
  override fun processPayment(userId: String, amount: Double): PaymentResult {
    try {
      val payment = paymentService.charge(userId, amount)
      notificationService.sendSuccess(userId)
      return PaymentResult.success(payment)
      
    } catch (e: InsufficientFundsException) {
      notificationService.sendFailure(userId, "Insufficient funds")
      return PaymentResult.failure("Insufficient funds")
      
    } catch (e: Exception) {
      notificationService.sendFailure(userId, "Payment failed")
      throw e // Re-throw for workflow retry
    }
  }
}

Best Practices

When tasks don’t depend on each other, dispatch them in parallel:
// ❌ Sequential (slow)
val result1 = service1.call()
val result2 = service2.call()
val result3 = service3.call()

// ✅ Parallel (fast)
val d1 = dispatch(service1::call)
val d2 = dispatch(service2::call)
val d3 = dispatch(service3::call)
val results = (d1 and d2 and d3).await()
Initialize service stubs once as class properties:
// ✅ Good
class MyWorkflow : Workflow(), MyWorkflowInterface {
  private val service = newService(MyService::class.java)
}

// ❌ Bad - creates new stub on every call
class MyWorkflow : Workflow(), MyWorkflowInterface {
  override fun process() {
    val service = newService(MyService::class.java)
  }
}
Use deferred objects locally within methods:
// ❌ Wrong
class MyWorkflow : Workflow(), MyWorkflowInterface {
  lateinit var deferred: Deferred<String>
}

// ✅ Correct
class MyWorkflow : Workflow(), MyWorkflowInterface {
  override fun process(): String {
    val deferred = dispatch(service::call)
    return deferred.await()
  }
}
Route specific tasks to specialized workers:
// GPU-intensive task
private val mlService = newService(
  MLService::class.java,
  tags = setOf("gpu-enabled")
)

// High-priority task
private val urgentService = newService(
  ProcessingService::class.java,
  tags = setOf("priority-high")
)

Next Steps

Sub-workflows

Learn how to call workflows from workflows

Channels

Receive external signals during execution

Timers

Add delays and timeouts

Workflow Methods

Complete method reference

Build docs developers (and LLMs) love