* Add all Ruby SDK reference files (11 files, ~2100 lines) Created complete Ruby reference documentation covering: - ruby.md: Overview, quick start, key concepts, file organization - patterns.md: Signals, queries, updates, child workflows, saga, cancellation, etc. - determinism.md: Illegal call tracing, safe alternatives table - determinism-protection.md: TracePoint, durable fiber scheduler, customization - versioning.md: Patching API, type versioning, worker versioning - testing.md: WorkflowEnvironment, mocking, replay, activity testing - error-handling.md: ApplicationError, retries, timeouts, workflow failure - data-handling.md: Data converter, ActiveModel, hints, search attributes - observability.md: Logging, metrics, best practices - gotchas.md: Common mistakes, illegal call tracing issues - advanced-features.md: Schedules, async completion, worker tuning, Rails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix alignment issues in Ruby reference files Self-review fixes: - patterns.md: Remove non-existent `workflow_run` annotation; entry point is `def execute` (no annotation needed, unlike Python's @workflow.run) - patterns.md: Remove conflicting manual query methods that duplicated workflow_query_attr_reader - error-handling.md: Remove `await` keyword (doesn't exist in Ruby) - gotchas.md: Replace TS-style CancellationScope with Ruby's Temporalio::Cancellation token-based detached cancellation - data-handling.md: Replace homemade ActiveModel mixin with official SDK pattern using ActiveSupport::Concern + ActiveModel::Serializers::JSON - data-handling.md: Fix list_workflows call signature (positional, not kw) - ruby.md, gotchas.md: Fix require paths to use 'temporalio/activity' instead of 'temporalio/activity/definition' Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix correctness issues in Ruby reference files - patterns.md: Fix external workflow signal to use class method ref (TargetWorkflow.data_ready instead of TargetWorkflow, :data_ready) - patterns.md: Add ? suffix to all_handlers_finished (Ruby boolean convention) - ruby.md: Add 'default' namespace to Client.connect calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add Ruby to all language references in SKILL.md and core files - SKILL.md: Add "Temporal Ruby" trigger phrase to description - SKILL.md: Update Overview to list Ruby as supported language - SKILL.md: Add Ruby entry to Getting Started guide - core/determinism.md: Add Ruby SDK Protection Mechanism entry (Illegal Call Tracing via TracePoint + Durable Fiber Scheduler) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * Apply suggestions from code review Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com> Co-authored-by: Donald Pinckney <donald_pinckney@icloud.com> * Apply suggestion from @chris-olszewski Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * copy over sample code * Remove useless section, mention Mutex * cleanup mutex mentions * Clean up transitive NDE section * Menial changes to align to python structure * Add Workflow Init section to Ruby advanced-features Document the workflow_init class method (Ruby's equivalent of Python's @workflow.init) for initializing workflow state before signal/update handlers run. Parallels the Python reference's Workflow Init section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document graceful_shutdown_period in Ruby Worker Tuning Add the graceful_shutdown_period worker option (Ruby's equivalent of Python's graceful_shutdown_timeout) to the Worker Tuning section, with an explanation of the worker shutdown sequence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Propagate cancellation in Ruby activity-error handling Update the Handling Activity Errors example to re-raise when Temporalio::Error.canceled? is true (Ruby's equivalent of Python's is_cancelled_exception), so a canceled activity cancels the workflow rather than failing it. Also clarify that only ApplicationError fails a workflow; other exceptions only fail/retry the workflow task. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Align Ruby Workflow Failure section to Python Replace the workflow_failure_exception_type / worker-option examples (misaligned with Python and already covered in advanced-features.md) with Python's example of raising an ApplicationError to deliberately fail a workflow. Add the terse note about not using non_retryable inside a workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add logger configuration to Ruby observability Document configuring the logger via Client.connect (logger: kwarg), which is used by both Temporalio::Workflow.logger and the activity logger. Parallels Python's Customizing Logger Configuration section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make Ruby Saga compensations cancellation-proof Run saga compensations with a detached Temporalio::Cancellation so they still execute when the workflow is canceled mid-saga. Previously they used the workflow cancellation, which is already canceled at that point, so the compensation activities would be canceled before starting. This is the Ruby equivalent of Python's asyncio.shield. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document patched() memoization caveat in Ruby versioning Note that Temporalio::Workflow.patched memoizes per patch ID, so it can't be used reliably in loops; append a sequence number to the patch ID per iteration. This behavior is shared with Python and .NET. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add default versioning behavior to Ruby worker versioning Document configuring default_versioning_behavior on Temporalio::Worker::DeploymentOptions, paralleling Python's Worker Configuration with Default Behavior section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix worker versioning config API names in Ruby docs The Configuring Workers for Versioning example used class/kwarg names that don't exist in the SDK. Correct them to deployment_options:, Temporalio::Worker::DeploymentOptions, and Temporalio::WorkerDeploymentVersion, matching the actual API and the Worker Configuration with Default Behavior example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix worker concurrency config in Ruby Worker Tuning max_concurrent_workflow_tasks and max_concurrent_activities are not valid Worker.new kwargs. Use the tuner: option with Temporalio::Worker::Tuner.create_fixed(workflow_slots:, activity_slots:) to control concurrent execution slots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Align Ruby Workflow Init title with Python Rename the section to 'Workflow Init Decorator' to match the Python reference's heading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Structure Ruby Metrics to match Python Split the flat Metrics section into 'Enabling SDK Metrics' and 'Key SDK Metrics' subsections, matching the Python reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Bart de Water <118401830+bdewater-thatch@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
10 KiB
Ruby SDK Patterns
Signals
class OrderWorkflow < Temporalio::Workflow::Definition
def initialize
@approved = false
@items = []
end
workflow_signal
def approve
@approved = true
end
workflow_signal
def add_item(item)
@items << item
end
def execute
Temporalio::Workflow.wait_condition { @approved }
"Processed #{@items.length} items"
end
end
Dynamic Signal Handlers
For handling signals with names not known at compile time. Use cases for this pattern are rare — most workflows should use statically defined signal handlers.
class DynamicSignalWorkflow < Temporalio::Workflow::Definition
def initialize
@signals = {}
end
workflow_signal dynamic: true, raw_args: true
def dynamic_signal(signal_name, *args)
@signals[signal_name] ||= []
@signals[signal_name] << Temporalio::Workflow.payload_converter.from_payload(args.first)
end
end
Queries
Important: Queries must NOT modify workflow state or have side effects.
class StatusWorkflow < Temporalio::Workflow::Definition
def initialize
@status = 'pending'
@progress = 0
end
# Shorthand for simple attribute readers
workflow_query_attr_reader :status, :progress
def execute
@status = 'running'
100.times do |i|
@progress = i
Temporalio::Workflow.execute_activity(
ProcessItem, i,
start_to_close_timeout: 60
)
end
@status = 'completed'
'done'
end
end
Dynamic Query Handlers
For handling queries with names not known at compile time. Use cases for this pattern are rare — most workflows should use statically defined query handlers.
workflow_query dynamic: true, raw_args: true
def dynamic_query(query_name, *args)
if query_name == 'get_field'
field_name = Temporalio::Workflow.payload_converter.from_payload(args.first)
instance_variable_get(:"@#{field_name}")
end
end
Updates
class OrderWorkflow < Temporalio::Workflow::Definition
def initialize
@items = []
end
workflow_update
def add_item(item)
@items << item
@items.length # Returns new count to caller
end
workflow_update_validator(:add_item)
def validate_add_item(item)
raise 'Item cannot be empty' if item.nil? || item.empty?
raise 'Order is full' if @items.length >= 100
end
end
Important: Validators must NOT mutate workflow state or do anything blocking (no activities, sleeps, or other commands). They are read-only, similar to query handlers. Raise an exception to reject the update; return nil to accept.
Child Workflows
class MyWorkflow < Temporalio::Workflow::Definition
def execute(orders)
results = []
orders.each do |order|
result = Temporalio::Workflow.execute_child_workflow(
ProcessOrderWorkflow, order,
id: "order-#{order.id}",
parent_close_policy: Temporalio::Workflow::ParentClosePolicy::ABANDON
)
results << result
end
results
end
end
Child Workflow Options
Temporalio::Workflow.execute_child_workflow(
ChildWorkflow, arg,
id: 'child-1',
parent_close_policy: Temporalio::Workflow::ParentClosePolicy::ABANDON,
cancellation_type: Temporalio::Workflow::ChildWorkflowCancellationType::WAIT_CANCELLATION_COMPLETED,
execution_timeout: 3600,
run_timeout: 1800
)
Handles to External Workflows
class MyWorkflow < Temporalio::Workflow::Definition
def execute(target_workflow_id)
handle = Temporalio::Workflow.external_workflow_handle(target_workflow_id)
# Signal the external workflow
handle.signal(TargetWorkflow.data_ready, data_payload)
# Or cancel it
handle.cancel
end
end
Parallel Execution
class MyWorkflow < Temporalio::Workflow::Definition
def execute(items)
futures = items.map do |item|
Temporalio::Workflow::Future.new do
Temporalio::Workflow.execute_activity(
ProcessItem, item,
start_to_close_timeout: 300
)
end
end
Temporalio::Workflow::Future.all_of(*futures).wait
results = futures.map(&:result)
results
end
end
Continue-as-New
class MyWorkflow < Temporalio::Workflow::Definition
def execute(state)
loop do
state = process_batch(state)
return 'done' if state.complete?
# Continue with fresh history before hitting limits
if Temporalio::Workflow.continue_as_new_suggested
raise Temporalio::Workflow::ContinueAsNewError.new(state)
end
end
end
end
Saga Pattern (Compensations)
Important: Compensation activities should be idempotent - they may be retried (as with ALL activities).
class MyWorkflow < Temporalio::Workflow::Definition
def execute(order)
compensations = []
begin
# Save compensation before running the activity, because:
# 1. reserve_inventory starts running
# 2. it successfully reserves inventory
# 3. but then fails for some other reason (timeout, reporting metrics, etc.)
# 4. the activity failed, but the effect (reserved inventory) already happened
# So the compensation must handle both reserved and unreserved states.
compensations << lambda { |cancellation|
Temporalio::Workflow.execute_activity(
ReleaseInventoryIfReserved, order,
start_to_close_timeout: 300,
cancellation: cancellation
)
}
Temporalio::Workflow.execute_activity(
ReserveInventory, order,
start_to_close_timeout: 300
)
compensations << lambda { |cancellation|
Temporalio::Workflow.execute_activity(
RefundPaymentIfCharged, order,
start_to_close_timeout: 300,
cancellation: cancellation
)
}
Temporalio::Workflow.execute_activity(
ChargePayment, order,
start_to_close_timeout: 300
)
Temporalio::Workflow.execute_activity(
ShipOrder, order,
start_to_close_timeout: 300
)
'Order completed'
rescue => e
Temporalio::Workflow.logger.error("Order failed: #{e}, running compensations")
# Use a detached cancellation so compensations still run even if the workflow
# was canceled (the workflow's own cancellation is already canceled by then).
detached_cancel, = Temporalio::Cancellation.new
compensations.reverse.each do |compensate|
begin
compensate.call(detached_cancel)
rescue => comp_err
Temporalio::Workflow.logger.error("Compensation failed: #{comp_err}")
end
end
raise
end
end
end
Cancellation (Token-based)
Ruby uses Temporalio::Cancellation tokens.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
# The workflow's cancellation token
workflow_cancel = Temporalio::Workflow.cancellation
begin
Temporalio::Workflow.execute_activity(
LongRunningActivity,
start_to_close_timeout: 3600,
cancellation: workflow_cancel
)
'completed'
ensure
# Create a detached cancellation for cleanup
# (not tied to workflow cancellation)
cancel, _cancel_proc = Temporalio::Cancellation.new
Temporalio::Workflow.execute_activity(
CleanupActivity,
start_to_close_timeout: 300,
cancellation: cancel
)
end
end
end
Wait Condition with Timeout
class MyWorkflow < Temporalio::Workflow::Definition
def execute
@approved = false
# Wait for approval with 24-hour timeout
# Returns false on timeout (no exception raised)
if Temporalio::Workflow.wait_condition(timeout: 86400) { @approved }
'approved'
else
'auto-rejected due to timeout'
end
end
end
Waiting for All Handlers to Finish
Signal and update handlers should generally be non-async (avoid running activities from them). Otherwise, the workflow may complete before handlers finish their execution. However, making handlers non-async sometimes requires workarounds that add complexity.
When async handlers are necessary, use wait_condition { all_handlers_finished } at the end of your workflow (or before continue-as-new) to prevent completion until all pending handlers complete.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
# ... main workflow logic ...
# Before exiting, wait for all handlers to finish
Temporalio::Workflow.wait_condition { Temporalio::Workflow.all_handlers_finished? }
'done'
end
end
Activity Heartbeat Details
WHY:
- Support activity cancellation - Cancellations are delivered via heartbeat; activities that don't heartbeat won't know they've been cancelled
- Resume progress after worker failure - Heartbeat details persist across retries
WHEN:
- Cancellable activities - Any activity that should respond to cancellation
- Long-running activities - Track progress for resumability
- Checkpointing - Save progress periodically
class ProcessLargeFile < Temporalio::Activity::Definition
def execute(file_path)
context = Temporalio::Activity::Context.current
# Get heartbeat details from previous attempt (if any)
heartbeat_details = context.info.heartbeat_details
start_line = heartbeat_details&.first || 0
begin
File.foreach(file_path).with_index do |line, i|
next if i < start_line
process_line(line)
# Heartbeat with progress
# If cancelled, heartbeat raises Temporalio::Error::CanceledError
context.heartbeat(i + 1)
end
'completed'
rescue Temporalio::Error::CanceledError
cleanup
raise
end
end
end
Timers
class MyWorkflow < Temporalio::Workflow::Definition
def execute
Temporalio::Workflow.sleep(3600)
'Timer fired'
end
end
Local Activities
Purpose: Reduce latency for short, lightweight operations by skipping the task queue. ONLY use these when necessary for performance. Do NOT use these by default, as they are not durable and distributed.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
result = Temporalio::Workflow.execute_local_activity(
QuickLookup, 'key',
start_to_close_timeout: 5
)
result
end
end
Using ActiveModel
See references/ruby/data-handling.md.