Skip to content

Conversation

@mfateev
Copy link
Member

@mfateev mfateev commented Jan 12, 2026

WARNING

The Kotlin SDK is an experiment in using LLMs for both design and implementation.

Summary

This proposal defines the public API for the Temporal Kotlin SDK, providing an idiomatic Kotlin experience using coroutines and suspend functions.

Formatted reading: https://github.com/mfateev/proposals/tree/kotlin-api-review/kotlin

Start at the README and click "Next" at the bottom of each document to navigate through all 27 files in order.

Key Features

  • Coroutine-based workflows with suspend fun
  • Type-safe activity and workflow execution via method references (no stubs)
  • Native Kotlin Duration support (30.seconds)
  • KOptions data classes for configuration
  • Full interoperability with Java SDK

Documentation Structure

Section Description
Kotlin Idioms Duration, null safety, coroutine patterns
Configuration KOptions, data conversion, interceptors
Workflows Definition, signals, queries, updates, child workflows
Activities Regular and local activities
Client KWorkflowClient, handles, advanced operations
Worker KWorkerFactory, KWorker setup
Testing KTestWorkflowEnvironment, time skipping
Migration Java SDK migration guide
API Parity Java SDK comparison

API document (sdk-api.md):
- Kotlin idioms: Duration, null safety, coroutines
- Stub-less activity/workflow invocation using method references
- String-based activity execution for cross-language interop
- KWorkflowHandle<T> and KTypedWorkflowHandle<T, R> for typed operations
- SignalWithStart and UpdateWithStart operations
- KotlinPlugin for enabling coroutine support
- K-prefix naming convention for public types
- Complete migration guide from Java SDK

Implementation document (sdk-implementation.md):
- Unified worker architecture with pluggable WorkflowImplementationFactory
- KotlinCoroutineDispatcher for deterministic execution
- Reified generics for type-safe activity execution
- Return type handling for DataConverter (typeOf<R>())
- Detailed open questions with context and recommendations

Proposal overview (sdk-proposal.md):
- Updated to reflect coroutines as key idiom
- Links to API and implementation docs
- Add Cancellation section to API doc covering cooperative cancellation,
  parallel execution, detached scopes with NonCancellable, and withTimeout
- Resolve Open Question temporalio#2: Kotlin built-in cancellation replaces CancellationScope
- Update complete example with cleanup activities and cancellation handling
- Rename KWorkflow.condition to KWorkflow.awaitCondition for clarity
- Fix parameter order to be consistent: (method/name, options, args...)
- Add implementation plan and detailed phase documents
- Add KWorkflowClient with DSL constructor and suspend functions for
  startWorkflow, executeWorkflow, signalWithStart, updateWithStart
- Add KWorkerFactory that automatically enables KotlinPlugin
- Add KChildWorkflowHandle<T, R> for child workflow interaction (signal,
  cancel, result) via startChildWorkflow and getChildWorkflowHandle
- Update implementation-plan.md with Phase 1 completion status
- Reorganize Phase 2 to include new client and worker APIs
- Remove api-implementation-discrepancies.md (was untracked)
Documents the analysis of Java vs Kotlin SDK API differences:
- APIs not needed in Kotlin (delay, cancellation scopes, sync primitives, wrap)
- Cancellation support integration with Kotlin coroutines
- Remaining gaps organized by priority (search attributes, memo, cron APIs)
- KActivityInfo missing fields
- Mark Phase 2 as complete in implementation-plan.md
- Add KWorkflow.retry() to implemented APIs
- Update sdk-api.md to mark retry as implemented
- Mark KUpdateHandle as implemented in implementation-plan.md
- Update sdk-api.md Java SDK API Parity section with accurate status
  - Most "High Priority" and "Medium Priority" gaps are now implemented
  - Reorganize into "Implemented APIs" and "Remaining Gaps" sections
- Clarify KWorkerFactory vs unified architecture in sdk-implementation.md
- Remove duplicate phases section from sdk-proposal.md
- Update implementation-plan.md to reflect KWorker is now part of
  temporal-kotlin worker package (not testing-only)
- KWorkerFactory.newWorker() now returns KWorker
- Mark Phase 2 commits (4-6) as complete in test framework plan
- Mark updateWithStart as complete with details on implemented methods
- Mark mocking support (3.2) as complete
Documents the implemented updateWithStart API:
- KWithStartWorkflowOperation class
- KUpdateWithStartOptions data class
- withStartWorkflowOperation factory methods
- startUpdateWithStart and executeUpdateWithStart methods
- Add Dynamic Handler Registration section with usage examples
- Add implemented APIs table for all handler registration methods
- Remove dynamic handler registration from Remaining Gaps (now implemented)
- Remove updateWithStart from Remaining Gaps (was implemented earlier)
Break down the large sdk-api.md (3,479 lines) into a navigable hierarchy:

- README.md: Main entry point with navigation
- kotlin-idioms.md: Duration, null safety, property syntax
- migration.md: Java SDK migration guide
- api-parity.md: Java SDK comparison and gaps

- workflows/: Workflow definition, signals/queries/updates, child workflows,
  timers, cancellation, continue-as-new
- activities/: Definition, implementation, local activities, KActivity API
- client/: KWorkflowClient, workflow handles, signalWithStart, updateWithStart
- worker/: KWorkerFactory, KWorker setup
- configuration/: KOptions, data conversion, interceptors

- implementation/: Moved internal design docs (sdk-proposal, sdk-implementation,
  implementation-plan, test-framework-*, suspend-activities-design)

All internal links verified and updated.
The type-safe version on KActivityContext is the preferred API:
  fun <T> getHeartbeatDetails(detailsClass: Class<T>): T?
  inline fun <reified T> KActivityContext.getHeartbeatDetails(): T?
Heartbeat is a short non-blocking operation, so a single heartbeat()
API is sufficient for both sync and suspend activities.
- Single heartbeat() API for both sync and suspend activities
- Support for both sync and suspend activity methods
Replace unrelated KActivity usage example with getHeartbeatDetails()
which demonstrates nullable return type and elvis operator.
Java clients can call Kotlin suspend workflows using untyped stubs,
making the parallel interface pattern unnecessary. Updated docs to
show the simpler approach with Java interop via untyped stubs.
- Renamed "Next Steps" sections to "Related" across all docs
- Added "Next: <topic>" links for linear document traversal
- Reading order follows README structure: Idioms → Config → Workflows → Activities → Client → Worker → Migration → API Parity
…tion, timeouts

Added sections for:
- Suspend functions (workflow and activity methods)
- Coroutines and concurrency (async, awaitAll, coroutineScope)
- Cancellation patterns (CancellationException, NonCancellable)
- Timeouts (withTimeout, withTimeoutOrNull)
- Mapping tables showing Kotlin patterns to Temporal/Java equivalents
- Add opening summary table of Java→Kotlin mappings
- Streamline code examples (less verbose)
- Improve table descriptions (Purpose instead of Temporal Equivalent)
- Add Data Classes section for DTOs with @serializable
- Remove redundant Property Syntax section (already in Suspend Functions)
- Clean up Null Safety section (remove unrelated type mappings)
arg1: A1, arg2: A2
): R

// ... up to 6 arguments
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In newer SDKs that don't have advanced generics for arbitrary arity, we have often just supported overloads with 0 or 1 parameter since we recommend no more than 1, but technically harmless to support multiple here except that we're encouraging multiple.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 1 argument
suspend fun <T, A1, R> executeActivity(
activity: KFunction2<T, A1, R>,
options: KActivityOptions,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably options as the trailing argument is clearer to read from a caller POV (i.e. meaning the args right after the function can make sense). Since Kotlin supports named parameters and parameter defaults, I considered the value of just putting all of the activity options as parameters, but it's kinda a bad practice since the list is so large.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

```kotlin
// Define activity interface
@ActivityInterface
interface GreetingActivities {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why require an interface at all? What we do in newer SDKs is even though we support interfaces (and/or abstract classes and/or never-implemented stubs), we don't require them. I think the only argument would be so it's usable from Java workflows, but at that point, the user can tease out an interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## Registering Activities

```kotlin
worker.registerActivitiesImplementations(OrderActivitiesImpl(paymentService))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am curious how this works without Kotlin in the Java SDK itself

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand the question.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw later there is a concept of a "plugin" that you register with a worker if you're using Java interop. Note, for a pure Kotlin experience, we don't have to be subject to Java worker approaches even if it wraps a Java worker. For instance, activities, workflows, Nexus services, etc can be worker options. But I understand not wanting to deviate too far from Java.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Updated to follow the Python/.NET pattern where workflows and activities are passed at worker construction time via options. This aligns with newer SDKs and provides immutable, all-in-one-place configuration.

Comment on lines 24 to 25
val factory = KWorkerFactory(client)
val worker = factory.newWorker("task-queue")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a factory/constructor separation here? Is there value in the separate steps, or can one create a worker with all options? Will our choice be limited by what Java does here wrt thread pool reuse across workers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java is the only SDK that doesn't rely on global variables to share cache and thread pool. It uses factory for this. If we decide that it is not a good pattern we can go with a simpler user experience

Comment on lines 6 to 12
val service = WorkflowServiceStubs.newLocalServiceStubs()

// Create KWorkflowClient with DSL configuration
val client = KWorkflowClient(service) {
setNamespace("default")
setDataConverter(myConverter)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In newer SDKs, we found requiring these separate steps unnecessary. Would recommend a static call on KWorkflowClient called connect that accepts an option set that is the union of service stub options and client options.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted as proposed. Updated to use KWorkflowClient.connect(options) pattern following .NET SDK conventions. This is cleaner and more explicit than the Java SDK's multi-step construction pattern.

worker.registerActivitiesImplementations(MyActivitiesImpl())

// Start the worker
factory.start()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should discourage the start pattern IMO in favor of a run one. We have learned that start+shutdown can swallow fatal worker-runtime errors (which are of course very rare, but technically can occur).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted with modification. Updated the documentation to recommend factory.run() (which blocks until shutdown) as the primary pattern, rather than discouraging start(). The documentation now shows:

// Recommended: blocks until worker shutdown
factory.run()

// Alternative for programmatic shutdown
factory.start()
// ... later ...
factory.shutdown()

This follows the pattern from other SDKs where run() is the simpler/preferred approach for most use cases.

- Use `KWorker` for pure Kotlin implementations (recommended)
- Use `Worker` (via `kworker.worker`) when mixing Java and Kotlin workflows/activities on the same worker

## KotlinPlugin (For Java Main)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have a concept of "plugin" coming for Java SDK, we need a different name here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be the same concept.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to implement Kotlin support on Java workers as a plugin, though may get a bit confusing to call it KotlinPlugin, assuming there will also be KPlugin for users to implement plugins in Kotlin (granted I can't think of a much better name, maybe KotlinToJavaWorkerPlugin or something)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about naming confusion. Renamed to KotlinJavaWorkerPlugin to make it clear this is for integrating Kotlin coroutine support with Java workers.

val a = async { KWorkflow.executeActivity(...) }
val b = async { KWorkflow.executeActivity(...) }

// If either activity fails, the other is cancelled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this statement exactly. Does it mean "if either activity fails, the workflow fails which implicitly cancels any activities" or is there some kind of cooperative cancellation happening here if a workflow fails/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted as proposed. Clarified the cancellation documentation to note that using coroutineScope and other structured concurrency patterns is standard Kotlin idiom - not a Temporal-specific concept. The documentation now emphasizes that Kotlin's structured concurrency naturally maps to workflow cancellation semantics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see this is the difference between coroutineScope and supervisorScope

Copy link

@GreyTeardrop GreyTeardrop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So excited for Kotlin-first Temporal SDK!

Comment on lines +16 to +28
data class KActivityOptions(
val startToCloseTimeout: Duration? = null,
val scheduleToCloseTimeout: Duration? = null,
val scheduleToStartTimeout: Duration? = null,
val heartbeatTimeout: Duration? = null,
val taskQueue: String? = null,
val retryOptions: KRetryOptions? = null,
val cancellationType: ActivityCancellationType? = null, // Java default: TRY_CANCEL
val disableEagerExecution: Boolean = false,
// Experimental
@Experimental val summary: String? = null,
@Experimental val priority: Priority? = null
) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Kotlin team doesn't recommend using data classes as part of library APIs as adding a new field (even an optional one) is always a binary breaking change. I see that some libraries like Jackson started from constructors with named parameters and later deprecated them in favor of a DSL/builder combo.

class KActivityOptions
private constructor(builder: KActivityOptions.Builder) {
    val startToCloseTimeout: Duration?
    // ...

    init {
        startToCloseTimeout = builder.startToCloseTimeout
        //
    }

    class Builder {
        var startToCloseTimeout: Duration? = null
        // ...

        fun build(): KActivityOptions {
            require(startToCloseTimeout != null || scheduleToCloseTimeout != null) {
                "At least one of startToCloseTimeout or scheduleToCloseTimeout must be specified"
            }
            return KActivityOptions(this)
        }
    }
}

inline fun KActivityOptions(init: KActivityOptions.Builder.() -> Unit): KActivityOptions {
    return KActivityOptions.Builder().apply(init).build()
}

which gives a ABI-safe equivalent for data class named constructor parameters

val options = KActivityOptions {
    startToCloseTimeout = 10.minutes
    //
}

(if necessary, copy can be implemented the same way).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@mfateev mfateev Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about this, I don't understand the value of binary compatibility. Temporal SDK never guaranteed binary compatibility between versions. These structures are never sent over the wire.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary use case for keeping ABI compatibility is for libraries that might be built on top of the SDK. Without it, if there's a library built against Kotlin SDK vX, it can't be used with a different version of the SDK, even if that version only has changes that could've been source-compatible, e.g., adding a new optional field to one of the data classes.

If the Kotlin SDK is only meant to be used by direct consumers, that shouldn't matter - consumer code is almost always built with the version of libraries it's supposed to run with, but unstable ABI can be an issue for wider ecosystem around the SDK.

Copy link
Member

@cretz cretz Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, and would totally support non-data for options classes for this reason, but at this point, does that mean basically no data classes should be used by libraries ever that may get new fields, only end application, due to this constraint?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be fine to use data classes internally, but not as part of public API surface - at least that's what JetBrains suggests (https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api).

Add "Decision Needed" sections to activity and workflow definition
docs proposing Python SDK-style interfaceless definitions where
activities and workflows can be defined directly on implementation
classes without requiring separate interfaces.
- Create open-questions.md to centralize API design decisions
- Add link to open questions in main README reference section
- Link activity and workflow definition docs to full discussion
Document the process for adding API design questions that need
discussion before implementation, including format for the central
open-questions.md and inline sections in proposal docs.
Propose using typed KArgs classes instead of varargs for compile-time
type safety when invoking activities with multiple arguments.
Include examples for activities, child workflows, and client workflow
execution with 0, 1, and multiple arguments.
- Option A: Keep current varargs (no type safety)
- Option B: Direct overloads (0-7 arguments, no wrapper)
- Option C: KArgs wrapper classes (fewer overloads)

Include comparison table showing trade-offs between options.
- Add open questions document covering interfaceless workflows/activities,
  type-safe activity arguments, and data classes vs builder+DSL pattern
- Refine API documentation with Kotlin idioms (suspend functions, properties)
- Add testing documentation and implementation design docs
- Expand migration guide with comprehensive API mapping
- Various documentation fixes and improvements
@mfateev
Copy link
Member Author

mfateev commented Jan 13, 2026

- Remove priority (not in Java)
- Add scheduleToStartTimeout
- Add doNotIncludeArgumentsIntoMarker
Key changes:
- Add KWorkflow.delay() with optional KTimerOptions
- Rename typedSearchAttributes to searchAttributes throughout
- Rename getWorkflowHandle() to workflowHandle() with result type overload
- Add KWorkflowClient.connect(options) pattern
- Update handle type hierarchy: KWorkflowHandleUntyped -> KWorkflowHandle<T> -> KWorkflowHandleWithResult<T, R>
- Add KStartUpdateOptions/KUpdateOptions with waitForStage required
- Add KWorkflowExecutionInfo base class for listWorkflows()
- Add KWorkflowClientInterceptor for client-side interception
- Clarify cancellation docs re: standard Kotlin behavior
- Recommend factory.run() pattern over start()
### Benefits of Builder+DSL

- Adding new optional properties to the `Builder` class is binary compatible
- The inline factory function provides the same ergonomic DSL syntax as data class constructors
Copy link

@GreyTeardrop GreyTeardrop Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, DSL syntax can even provide slightly better ergonomics. E.g., in KActivityOptions case, retry options can be specified as

val options = KActivityOptions {
    startToCloseTimeout = 10.minutes
    retryOptions = KRetryOptions {
        initialInterval = 10.seconds
        backoffCoefficient = 1.5
    }
}

but with additional method like

inline fun KActivityOptions.Builder.retryOptions(init: KRretryOptions.Builder.() -> Unit) {
    this.retryOptions = KRetryOptions(init)
}

that can become

val options = KActivityOptions {
    startToCloseTimeout = 10.minutes
    retryOptions {
        initialInterval = 10.seconds
        backoffCoefficient = 1.5
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point! I added your proposal to the open-questions.md document.

Copy link
Member Author

@mfateev mfateev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responses to review comments

Comment on lines +16 to +28
data class KActivityOptions(
val startToCloseTimeout: Duration? = null,
val scheduleToCloseTimeout: Duration? = null,
val scheduleToStartTimeout: Duration? = null,
val heartbeatTimeout: Duration? = null,
val taskQueue: String? = null,
val retryOptions: KRetryOptions? = null,
val cancellationType: ActivityCancellationType? = null, // Java default: TRY_CANCEL
val disableEagerExecution: Boolean = false,
// Experimental
@Experimental val summary: String? = null,
@Experimental val priority: Priority? = null
) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. **Replay Safety** - If default values change between deployments, replayed workflows behave differently, violating determinism
2. **Serialization Ambiguity** - Unclear whether the caller serializes defaults or the worker applies them
3. **Cross-Language Compatibility** - Other languages calling the activity/workflow don't know about Kotlin defaults
4. **SDK Consistency** - Python SDK explicitly disallows default parameters; Go/Java don't have them
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree with the decision to disallow because it can be easily walked back if we change our minds, it may be worth noting that IIRC, Python, .NET, and Ruby all support default parameters. Here is a Python test confirming it: https://github.com/temporalio/sdk-python/blob/993de6d0e9b42bb01f24edfdb46e0795b00debcf/tests/worker/test_workflow.py#L3502-L3545.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point about other SDKs supporting defaults. Given we're moving to 0-1 parameters (with >1 falling back to untyped), supporting a default for a single parameter is straightforward and aligns with Python/.NET/Ruby. Updated the docs to allow defaults for the 0-1 argument case.


- Different from Java SDK convention
- Activity/workflow type names derived from method/class names (convention-based)
- Respects `@ActivityMethod(name = "...")` and `@WorkflowMethod(name = "...")` annotations if present
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, in order to support this in some newer SDKs and not hit diamond problems or other inheritance confusion for when they do want to have interfaces/abstracts, we require every overridden method to have the same matching annotation/attribute/decorator. This prevents ambiguity, is easy to understand, and clarifies author intent clearly to reader without relying on inheritance (at the small cost of having to write duplicate annotations on overrides).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this approach makes sense for newer SDKs starting fresh, requiring duplicate annotations would break backward compatibility with existing Java activities/workflows and feel unnatural to Java SDK users. Since Kotlin SDK builds on Java SDK, we'll maintain the current convention where annotations on the interface are sufficient.


---

### Option B: Direct Overloads (0-7 Arguments)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or "0-1 Argument(s)", meaning any > 1 argument, which Temporal discourages, can fall back to untyped. This is what we do in some newer SDKs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up question: Why do you prefer the type-unsafe fallback for >1 args instead of the KArgs approach which maintains type safety? Is API simplicity the main driver, or are there other considerations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API simplicity (and overload count) was my main driver in preferring just falling back to untyped, but in some SDKs (namely Python), we actually do support arbitrary multi-param typed as a different overload. Would be fine if we did that here too. And really, with how .NET is lambda expressions, it supports multi-param typed as well. So overall, yeah, ok w/ a typed multi-param, and having 0 or 1 be even simpler forms of that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for confirming. We'll go with the KArgs approach (Option C) - provides full type safety for all arities while keeping 0-1 argument cases simple. The kargs() wrapper for 2+ args is a small price for compile-time type checking.

val options = KActivityOptions(startToCloseTimeout = 10.minutes)
```

However, the Kotlin team doesn't recommend using data classes as part of library APIs because adding a new field (even an optional one) is always a **binary breaking change**. This happens because:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm, not sure we have ever considered binary breaking changes to be breaking changes from our POV (i.e. we expect you to compile against the same JAR you'll run with). Having said that, definitely not against better options patterns. But I don't think we want to discourage e.g. users from using data classes for their need-backwards-compatibility models.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. We won't discourage users from using data classes for their models that need backwards compatibility. The concern about binary breaking changes in the context of SDK options classes is less relevant since users compile against the SDK version they run with.

## Registering Activities

```kotlin
worker.registerActivitiesImplementations(OrderActivitiesImpl(paymentService))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw later there is a concept of a "plugin" that you register with a worker if you're using Java interop. Note, for a pure Kotlin experience, we don't have to be subject to Java worker approaches even if it wraps a Java worker. For instance, activities, workflows, Nexus services, etc can be worker options. But I understand not wanting to deviate too far from Java.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not understanding "push" cancellation, but yeah so long as I can have heartbeat infallible and cancellation represented as traditional Kotlin coroutine cancellation, I think that is ideal. Arguably both of those could be the default, but I understand if it is confusing to have suspend fun do something completely different than non-suspend fun (this is a struggle we run into w/ Python where cancellation is represented quite differently w/ async def vs just def, but not this differently, heartbeat remained infallible on both).

```kotlin
@ActivityInterface
interface ValidationActivities {
suspend fun validate(input: String): Boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, was just a bit strange to see it present inconsistently sometimes even w/out name customization in this proposal


| Java SDK API | Reason Not Needed |
|--------------|-------------------|
| `Workflow.sleep(Duration)` | Use `kotlinx.coroutines.delay()` - intercepted by dispatcher |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using existing async utilities makes sense, but still may need some workflow-specific alternatives for advanced users, such as being able to provide a timer summary for sleep.

- Use `KWorker` for pure Kotlin implementations (recommended)
- Use `Worker` (via `kworker.worker`) when mixing Java and Kotlin workflows/activities on the same worker

## KotlinPlugin (For Java Main)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to implement Kotlin support on Java workers as a plugin, though may get a bit confusing to call it KotlinPlugin, assuming there will also be KPlugin for users to implement plugins in Kotlin (granted I can't think of a much better name, maybe KotlinToJavaWorkerPlugin or something)

val a = async { KWorkflow.executeActivity(...) }
val b = async { KWorkflow.executeActivity(...) }

// If either activity fails, the other is cancelled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see this is the difference between coroutineScope and supervisorScope

- Q1: Allow default parameters for 0-1 argument case (align with Python/.NET/Ruby)
- Q5: Follow Python/.NET pattern for worker options (pass workflows/activities at construction)
- Q6: Heartbeat infallible, cancellation via coroutine or CompletableFuture
- Q7: Only show annotations when customizing names (consistency cleanup)
- Q8: Add KWorkflow.delay() with and without summary parameter
- Q9: Rename KotlinPlugin to KotlinJavaWorkerPlugin
@@ -0,0 +1,149 @@
# Workflow Client
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to mention this, but in newer SDKs, we found just having one big client that is a workflow client + schedule client (w/ features to make an async activity completion handle) is a bit cleaner from an options POV. This will also help when standalone activity client and Nexus operation client come about. No need to change though if we don't want, there is also value in matching what Java does (though you will duplicate a lot of these client options each time).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. We'll use a unified KClient matching the Python/.NET pattern - single client covering workflows, schedules, async activity completion, and ready for future Nexus support. Cleaner options and less duplication.

- Q3: Use KArgs approach for type-safe activity/workflow arguments
- Q11: Heartbeat throws CancellationException on cancellation (prevents slot eating)
- Q12: Unified KClient matching Python/.NET pattern (workflows, schedules, async completion)
- R1-Q5: Use KWorkflow.version() method instead of getter-style

Key changes:
- Rename KWorkflowClient to KClient throughout
- Add schedule and async activity completion APIs to KClient
- Update heartbeat to throw on cancellation, add TODO for cancellationFuture
- Mark KArgs (Option C) as decided in open-questions.md
- Use KActivityContext.current() pattern instead of KActivity (Q20)
- Remove KActivity.logger()/KWorkflow.logger(), use MDC with standard logging (Q21)
- Keep single-argument heartbeat, rename to lastHeartbeatDetails<T>() (Q22)
- Document event loop implementation on top of Java SDK (Q24)

Updated files:
- activities/implementation.md
- activities/local-activities.md
- workflows/definition.md
- configuration/interceptors.md
- migration.md
- implementation/implementation-plan.md
Copy link
Member

@cretz cretz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my POV, this proposal is definitely good enough from a proposal POV (and is much more complete than past SDK lang proposals). So can merge whenever IMO, but may want more feedback.

Note, in all SDKs, the implementation deviates from the proposal as we learn things, so we've never expected/required 100% proposal accuracy. Also, not sure if it is part of the proposal to determine where this will live (I prefer https://github.com/temporalio/sdk-java/tree/master/temporal-kotlin myself where it can be versioned/published alongside the Java SDK dependency and even use internal packages from it if we wanted).

Following Python SDK pattern, KWorker now has:
- run() - recommended, blocks until shutdown, propagates fatal errors
- start()/shutdown() - for advanced manual control scenarios

Updated examples to use run() as the primary pattern.
@mfateev mfateev merged commit f03edf5 into temporalio:master Jan 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants