Workshop 10: Signals
Building Interactive Workflow Systems
Create workflows that can receive and respond to external signals while running
What we want to build
Create workflows that can receive and respond to external signals while running.
This enables interactive workflows that can change behavior based on external events.
Expecting Result
By the end of this workshop, you'll have:
- ✅ Long-running workflows that listen for signals
- ✅ Signal handlers that modify workflow behavior
- ✅ Querying workflow state from external systems
- ✅ Interactive approval workflows
Code Steps
Step 1: Define Workflow with Signals
@WorkflowInterface
interface ApprovalWorkflow {
@WorkflowMethod
fun processApprovalRequest(request: ApprovalRequest): ApprovalResult
@SignalMethod
fun approve(approverComment: String)
@SignalMethod
fun reject(rejectionReason: String)
@QueryMethod
fun getStatus(): ApprovalStatus
@QueryMethod
fun getPendingApprovers(): List<String>
}
Key concepts: @SignalMethod
for external events, @QueryMethod
for state reading
Step 2: Implement Signal Handling
class ApprovalWorkflowImpl : ApprovalWorkflow {
private var status = ApprovalStatus.PENDING
private var approverComment: String? = null
private var rejectionReason: String? = null
private val pendingApprovers = mutableListOf<String>()
override fun processApprovalRequest(request: ApprovalRequest): ApprovalResult {
val logger = Workflow.getLogger(this::class.java)
logger.info("Starting approval process for: ${request.id}")
// Initialize approvers
pendingApprovers.addAll(request.requiredApprovers)
// Send notification to approvers
notificationActivity.notifyApprovers(request.requiredApprovers, request)
// Continued on next slide...
Signal Waiting Pattern
// Wait for approval or timeout
val approvalDeadline = Workflow.newTimer(Duration.ofDays(7))
// Use Workflow.await to wait for signals or timeout
Workflow.await(
Duration.ofDays(7)
) {
status != ApprovalStatus.PENDING
}
return when (status) {
ApprovalStatus.APPROVED -> {
logger.info("Request approved with comment: $approverComment")
// Execute approved workflow
val executionResult = executionActivity.executeApprovedRequest(request)
ApprovalResult.approved(
requestId = request.id,
approverComment = approverComment!!,
executionResult = executionResult
)
}
// Continued on next slide...
Complete Status Handling
ApprovalStatus.REJECTED -> {
logger.info("Request rejected: $rejectionReason")
// Clean up any resources
cleanupActivity.cleanupRejectedRequest(request)
ApprovalResult.rejected(
requestId = request.id,
rejectionReason = rejectionReason!!
)
}
ApprovalStatus.PENDING -> {
logger.warn("Approval request timed out")
// Auto-reject due to timeout
cleanupActivity.cleanupTimedOutRequest(request)
ApprovalResult.timedOut(
requestId = request.id,
timeoutDays = 7
)
}
}
}
// Continued on next slide...
Signal Handlers Implementation
override fun approve(approverComment: String) {
val logger = Workflow.getLogger(this::class.java)
logger.info("Approval signal received with comment: $approverComment")
this.approverComment = approverComment
this.status = ApprovalStatus.APPROVED
}
override fun reject(rejectionReason: String) {
val logger = Workflow.getLogger(this::class.java)
logger.info("Rejection signal received: $rejectionReason")
this.rejectionReason = rejectionReason
this.status = ApprovalStatus.REJECTED
}
override fun getStatus(): ApprovalStatus = status
override fun getPendingApprovers(): List<String> = pendingApprovers.toList()
}
Signal handlers update workflow state and trigger condition checks
Step 3: Send Signals from External System
// Start the approval workflow
val workflow = workflowClient.newWorkflowStub(
ApprovalWorkflow::class.java,
WorkflowOptions.newBuilder()
.setTaskQueue("approval-queue")
.setWorkflowId("approval-${request.id}")
.build()
)
// Start workflow asynchronously
val workflowExecution = WorkflowClient.start(workflow::processApprovalRequest, request)
// Later, send approval signal
val workflowStub = workflowClient.newWorkflowStub(
ApprovalWorkflow::class.java,
workflowExecution.workflowId
)
workflowStub.approve("Looks good to me! Approved by John Doe")
// Query current status
val currentStatus = workflowStub.getStatus()
println("Current approval status: $currentStatus")
Step 4: Multiple Signal Handlers
class OrderTrackingWorkflowImpl : OrderTrackingWorkflow {
private var orderStatus = OrderStatus.PROCESSING
private val statusHistory = mutableListOf<StatusUpdate>()
override fun trackOrder(orderId: String): OrderTrackingResult {
// Wait for various signals during order lifecycle
Workflow.await { orderStatus == OrderStatus.COMPLETED || orderStatus == OrderStatus.CANCELLED }
return OrderTrackingResult(
orderId = orderId,
finalStatus = orderStatus,
statusHistory = statusHistory.toList()
)
}
@SignalMethod
fun updateShippingStatus(trackingUpdate: TrackingUpdate) {
statusHistory.add(StatusUpdate.shipping(trackingUpdate))
if (trackingUpdate.isDelivered) {
orderStatus = OrderStatus.COMPLETED
}
}
// Continued on next slide...
More Signal Handlers
@SignalMethod
fun reportIssue(issue: OrderIssue) {
statusHistory.add(StatusUpdate.issue(issue))
if (issue.isCritical) {
orderStatus = OrderStatus.CANCELLED
}
}
@SignalMethod
fun customerUpdate(customerAction: CustomerAction) {
statusHistory.add(StatusUpdate.customer(customerAction))
if (customerAction.type == CustomerActionType.CANCEL) {
orderStatus = OrderStatus.CANCELLED
}
}
}
Multiple signal handlers enable rich interaction patterns
How to Run
Start workflow and send signals:
// Start the workflow
val approvalRequest = ApprovalRequest(
id = "req-123",
description = "Deploy new feature to production",
requiredApprovers = listOf("manager@company.com", "tech-lead@company.com")
)
val workflowId = "approval-${approvalRequest.id}"
val workflow = workflowClient.newWorkflowStub(
ApprovalWorkflow::class.java,
WorkflowOptions.newBuilder()
.setTaskQueue("approval-queue")
.setWorkflowId(workflowId)
.build()
)
// Start async
WorkflowClient.start(workflow::processApprovalRequest, approvalRequest)
// Query status
val status = workflow.getStatus()
println("Status: $status")
// Send approval
workflow.approve("Deployment approved after security review")
Signal vs Query Patterns
Signals (Write Operations):
- ✅ Modify workflow state asynchronously
- ✅ Trigger workflow logic changes
- ✅ Persisted in workflow history for replay
- ✅ Enable external interaction with running workflows
Queries (Read Operations):
- ✅ Read current state synchronously
- ✅ No side effects on workflow execution
- ✅ Fast response without persistence
- ✅ Real-time monitoring and observability
💡 Key Takeaways
What You've Learned:
- ✅ Signals enable interactive workflows that respond to external events
- ✅ Workflow.await() blocks until conditions are met
- ✅ Signal handlers update state and trigger logic changes
- ✅ Queries provide real-time state visibility
- ✅ Asynchronous workflow execution with external interaction
🚀 Next Steps
You now understand building interactive workflow systems!
Lesson 11 will cover:
- Advanced query patterns and optimization
- Real-time workflow observability
- Query design best practices
- Performance considerations
Ready to master workflow queries? Let's continue! 🎉