Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cross-Language Consistency

This document describes Tasker Core’s philosophy for maintaining consistent APIs across Rust, Ruby, Python, and TypeScript workers while respecting each language’s idioms.

The Core Philosophy

“There should be one–and preferably only one–obvious way to do it.” – The Zen of Python

When a developer learns one Tasker worker language, they should understand all of them at the conceptual level. The specific syntax changes; the patterns remain constant.


Consistency Without Uniformity

What We Align

Developer-facing touchpoints that affect daily work:

TouchpointWhy Align
Handler signaturesDevelopers switch languages within projects
Result factoriesError handling should feel familiar
Registry APIsService configuration is cross-cutting
Context access patternsData access is the core operation
Specialized handlersAPI, Decision, Batchable are reusable patterns

What We Don’t Force

Language idioms that feel natural in their ecosystem:

RubyPythonTypeScriptRust
Blocks, yieldDecorators, context managersGenerics, interfacesTraits, associated types
Symbols (:name)Type hintsasync/awaitPattern matching
Duck typingABC, ProtocolUnion typesEnums, Result<T,E>

The Aligned APIs

Handler Signatures

All handlers receive context, return results:

# Ruby
class MyHandler < TaskerCore::StepHandler::Base
  def call(context)
    success(result: { data: "value" })
  end
end
# Python
class MyHandler(BaseStepHandler):
    def call(self, context: StepContext) -> StepHandlerResult:
        return self.success({"data": "value"})
// TypeScript
class MyHandler extends BaseStepHandler {
  async call(context: StepContext): Promise<StepHandlerResult> {
    return this.success({ data: "value" });
  }
}
#![allow(unused)]
fn main() {
// Rust
impl StepHandler for MyHandler {
    async fn call(&self, step_data: &TaskSequenceStep) -> StepExecutionResult {
        StepExecutionResult::success(json!({"data": "value"}), None)
    }
}
}

Result Factories

Success and failure patterns are identical:

OperationPattern
Successsuccess(result_data, metadata?)
Failurefailure(message, error_type, error_code?, retryable?, metadata?)

The factory methods hide implementation details (wrapper classes, enum variants) behind a consistent interface.

Registry Operations

All registries support the same core operations:

OperationDescription
register(name, handler)Register a handler by name
is_registered(name)Check if handler exists
resolve(name)Get handler instance
list_handlers()List all registered handlers

Context Access Patterns

The StepContext provides unified access to execution data:

Core Fields (All Languages)

FieldTypeDescription
task_uuidStringUnique task identifier
step_uuidStringUnique step identifier
input_dataDict/HashInput data for the step
step_configDict/HashHandler configuration
dependency_resultsWrapperResults from parent steps
retry_countIntegerCurrent retry attempt
max_retriesIntegerMaximum retry attempts

Convenience Methods

MethodDescription
get_task_field(name)Get field from task context
get_dependency_result(step_name)Get result from a parent step

Specialized Handler Patterns

API Handler

HTTP operations available in all languages:

MethodPattern
GETget(path, params?, headers?)
POSTpost(path, data?, headers?)
PUTput(path, data?, headers?)
DELETEdelete(path, params?, headers?)

Decision Handler

Conditional workflow branching:

# Ruby
decision_success(steps: ["branch_a", "branch_b"], result_data: { routing: "criteria" })
decision_no_branches(result_data: { reason: "no action needed" })
# Python
self.decision_success(["branch_a", "branch_b"], {"routing": "criteria"})
self.decision_no_branches({"reason": "no action needed"})

Batchable Handler

Cursor-based batch processing:

OperationDescription
get_batch_context(context)Extract batch metadata
batch_worker_complete(count, data)Signal batch completion
handle_no_op_worker(batch_ctx)Handle empty batch

FFI Boundary Types

When data crosses the FFI boundary (Rust <-> Ruby/Python/TypeScript), types must serialize identically:

Required Explicit Types

TypePurpose
DecisionPointOutcomeDecision handler results
BatchProcessingOutcomeBatch handler results
CursorConfigBatch cursor configuration
StepHandlerResultAll handler results

Serialization Guarantee

The same JSON representation must work across all languages:

{
  "success": true,
  "result": { "data": "value" },
  "metadata": { "timing_ms": 50 }
}

Why This Matters

Developer Productivity

When switching from a Ruby handler to a Python handler:

  • No relearning core concepts
  • Same mental model applies
  • Documentation transfers

Code Review Consistency

Reviewers can evaluate handlers in any language:

  • Pattern violations are obvious
  • Best practices are universal
  • Anti-patterns are recognizable

Documentation Efficiency

One set of conceptual docs serves all languages:

  • Language-specific pages show syntax only
  • Core patterns documented once
  • Examples parallel across languages

The Pre-Alpha Advantage

In pre-alpha, we can make breaking changes to achieve consistency:

Change TypeExample
Method renameshandle()call()
Signature changes(task, step)(context)
Return type unificationSeparate Success/Error → unified result

These changes would be costly post-release but are cheap now.


Migration Path

When APIs diverge, we follow this pattern:

  1. Non-Breaking First: Add aliases, helpers, new modules
  2. Deprecation Period: Mark old APIs deprecated (warnings in logs)
  3. Breaking Release: Remove old APIs, document migration

Example timeline:

Phase 1: Python migration (non-breaking + breaking)
Phase 2: Ruby migration (non-breaking + breaking)
Phase 3: Rust alignment (already aligned)
Phase 4: TypeScript alignment (new implementation)
Phase 5: Breaking changes release (all languages together)

Anti-Patterns

Don’t: Force Identical Syntax

# BAD: Ruby-style symbols in Python
def call(context) -> Hash[:success => true]  # Not Python!

Don’t: Ignore Language Idioms

# BAD: Python-style type hints in Ruby
def call(context: StepContext) -> StepHandlerResult  # Not Ruby!

Don’t: Duplicate Orchestration Logic

# BAD: Worker creating decision steps
def call(context)
  # Don't do orchestration's job!
  create_decision_steps(...)  # Orchestration handles this
end