From ff1368741063e3a338cc9ec272d26edaf7d2cc9f Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 10:28:22 -0400 Subject: [PATCH 01/33] init --- Cargo.toml | 1 + crates/client/src/lib.rs | 15 ++- crates/client/src/options_structs.rs | 32 +++++ crates/common/build.rs | 1 + .../integ_tests/workflow_tests/queries.rs | 110 ++++++++++++++++++ crates/sdk/Cargo.toml | 1 + crates/sdk/src/workflow_context.rs | 82 +++++++++++++ crates/sdk/src/workflow_context/options.rs | 8 +- crates/sdk/src/workflow_future.rs | 49 ++++++-- 9 files changed, 285 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f3843119..f1641428a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/macros", "crates/sdk", "crates/sdk-core-c-bridge", + "current-details-demo", ] resolver = "2" diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index adfbdc1e8..8890b7123 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -69,12 +69,13 @@ use temporalio_common::{ HasWorkflowDefinition, data_converters::{DataConverter, SerializationContextData}, protos::{ - coresdk::IntoPayloadsExt, + coresdk::{AsJsonPayloadExt, IntoPayloadsExt}, grpc::health::v1::health_client::HealthClient, proto_ts_to_system_time, temporal::api::{ cloud::cloudservice::v1::cloud_service_client::CloudServiceClient, common::v1::{Memo, Payload, SearchAttributes, WorkflowType}, + sdk::v1::UserMetadata, enums::v1::{TaskQueueKind, WorkflowExecutionStatus}, errordetails::v1::WorkflowExecutionAlreadyStartedFailure, operatorservice::v1::operator_service_client::OperatorServiceClient, @@ -1027,6 +1028,16 @@ where let workflow_id = options.workflow_id.clone(); let task_queue_name = options.task_queue.clone(); + let user_metadata = + if options.static_summary.is_some() || options.static_details.is_some() { + Some(UserMetadata { + summary: options.static_summary.and_then(|s| s.as_json_payload().ok()), + details: options.static_details.and_then(|s| s.as_json_payload().ok()), + }) + } else { + None + }; + let run_id = if let Some(start_signal) = options.start_signal { // Use signal-with-start when a start_signal is provided let res = WorkflowService::signal_with_start_workflow_execution( @@ -1057,6 +1068,7 @@ where search_attributes: options.search_attributes.map(|d| d.into()), cron_schedule: options.cron_schedule.unwrap_or_default(), header: options.header.or(start_signal.header), + user_metadata, ..Default::default() } .into_request(), @@ -1097,6 +1109,7 @@ where completion_callbacks: options.completion_callbacks, priority: Some(options.priority.into()), header: options.header, + user_metadata, ..Default::default() } .into_request(), diff --git a/crates/client/src/options_structs.rs b/crates/client/src/options_structs.rs index c58c77987..16994050e 100644 --- a/crates/client/src/options_structs.rs +++ b/crates/client/src/options_structs.rs @@ -238,6 +238,12 @@ pub struct WorkflowStartOptions { /// Headers to include with the start request. pub header: Option
, + + /// Single-line static summary for the workflow, shown in the Temporal UI. + pub static_summary: Option, + + /// Multi-line static details for the workflow, shown in the Temporal UI. + pub static_details: Option, } /// A signal to send atomically when starting a workflow. @@ -455,3 +461,29 @@ pub struct WorkflowListOptions { #[derive(Debug, Clone, Default, bon::Builder)] #[non_exhaustive] pub struct WorkflowCountOptions {} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn workflow_start_options_static_summary_default_none() { + let opts = WorkflowStartOptions::new("tq", "wf-id").build(); + assert!(opts.static_summary.is_none()); + assert!(opts.static_details.is_none()); + } + + #[test] + fn workflow_start_options_static_summary_set() { + let opts = WorkflowStartOptions::new("tq", "wf-id") + .static_summary("Order fulfillment workflow".to_string()) + .static_details("Validates the order, processes payment".to_string()) + .build(); + assert_eq!(opts.static_summary.as_deref(), Some("Order fulfillment workflow")); + assert_eq!( + opts.static_details.as_deref(), + Some("Validates the order, processes payment") + ); + } +} diff --git a/crates/common/build.rs b/crates/common/build.rs index 45aed80ae..39248237e 100644 --- a/crates/common/build.rs +++ b/crates/common/build.rs @@ -158,6 +158,7 @@ fn main() -> Result<(), Box> { }, &[ "./protos/local/temporal/sdk/core/core_interface.proto", + "./protos/api_upstream/temporal/api/sdk/v1/workflow_metadata.proto", "./protos/api_upstream/temporal/api/workflowservice/v1/service.proto", "./protos/api_upstream/temporal/api/operatorservice/v1/service.proto", "./protos/api_upstream/temporal/api/errordetails/v1/message.proto", diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 0ad99e3e4..2d7c8a880 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -412,3 +412,113 @@ async fn query_returns_workflow_context_view_info() { worker.register_workflow::(); worker.run().await.unwrap(); } + +// ── current_details integration test ──────────────────────────────────────── + +/// Workflow that sets current_details and then waits for a signal before completing. +/// The wait ensures the workflow stays alive when the metadata query arrives. +#[workflow] +#[derive(Default)] +struct CurrentDetailsWf { + done: bool, +} + +#[workflow_methods] +impl CurrentDetailsWf { + #[run(name = DEFAULT_WORKFLOW_TYPE)] + async fn run(ctx: &mut WorkflowContext) -> WorkflowResult<()> { + ctx.set_current_details("details from workflow"); + ctx.wait_condition(|s| s.done).await; + Ok(()) + } + + #[signal] + fn finish(&mut self, _ctx: &mut SyncWorkflowContext) { + self.done = true; + } +} + +/// Verify that `__temporal_workflow_metadata` returns a proto-JSON-encoded `WorkflowMetadata` +/// whose `current_details` field reflects the value set by `set_current_details`. +#[tokio::test] +async fn workflow_metadata_query_returns_current_details() { + let wfid = "workflow_metadata_query_test"; + + let mut t = TestHistoryBuilder::default(); + t.add_by_type(EventType::WorkflowExecutionStarted); + t.add_full_wf_task(); + + let tasks = [ + // First task: workflow starts, sets current_details, and blocks on wait_condition. + hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)), + // Second task: legacy query for __temporal_workflow_metadata while workflow is blocked. + { + let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)); + pr.query = Some(WorkflowQuery { + query_type: "__temporal_workflow_metadata".to_string(), + query_args: None, + header: None, + }); + pr.history = Some(Default::default()); + pr + }, + ]; + + let mut mock_cfg = MockPollCfg::from_resp_batches(wfid, t, tasks, mock_worker_client()); + mock_cfg.num_expected_legacy_query_resps = 1; + + mock_cfg.completion_asserts_from_expectations(|mut asserts| { + asserts + .then(|wft| { + // First activation: workflow runs and blocks. No completion command. + let has_complete = wft + .commands + .iter() + .any(|c| c.command_type() == CommandType::CompleteWorkflowExecution); + assert!(!has_complete, "Workflow should not complete on first activation"); + }) + .then(|wft| { + // Second activation: the metadata query response. + assert_eq!( + wft.query_responses.len(), + 1, + "Expected exactly one query response" + ); + let query_resp = &wft.query_responses[0]; + + match &query_resp.variant { + Some(query_result::Variant::Succeeded(success)) => { + let payload = success + .response + .as_ref() + .expect("Expected a response payload"); + + assert_eq!( + payload.metadata.get("encoding").map(|v| v.as_slice()), + Some(b"json/protobuf".as_slice()), + "Expected json/protobuf encoding" + ); + assert_eq!( + payload.metadata.get("messageType").map(|v| v.as_slice()), + Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), + "Expected WorkflowMetadata messageType" + ); + + // The data is proto-JSON: {"currentDetails":"..."} + let json: serde_json::Value = + serde_json::from_slice(&payload.data).expect("valid JSON"); + assert_eq!( + json["currentDetails"].as_str(), + Some("details from workflow"), + "current_details should match what the workflow set" + ); + } + other => panic!("Expected Succeeded query response, got {:?}", other), + } + }); + }); + + let mut worker = build_fake_sdk(mock_cfg); + worker.register_workflow::(); + worker.run().await.unwrap(); +} diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 6a275c020..af76ec864 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -21,6 +21,7 @@ gethostname = "1.0.2" parking_lot = { version = "0.12", features = ["send_guard"] } prost-types = { workspace = true } serde = "1.0" +serde_json = "1.0" thiserror = "2" tokio = { version = "1.47", features = [ "rt", diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 8f751971d..6ef7e00bb 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -402,6 +402,11 @@ impl BaseWorkflowContext { self.inner.state_mutated.set(true); } + /// Return the current value of current_details. + pub(crate) fn current_details(&self) -> String { + self.inner.shared.borrow().current_details.clone() + } + /// Cancel any cancellable operation by ID fn cancel(&self, cancellable_id: CancellableID) { self.send(RustWfCmd::Cancel(cancellable_id)); @@ -847,6 +852,21 @@ impl SyncWorkflowContext { )) } + /// Set the current free-form details string for this workflow execution. + /// + /// The value is surfaced to the Temporal server UI in real time via the + /// `__temporal_workflow_metadata` built-in query. + pub fn set_current_details(&self, details: impl Into) { + self.base.inner.shared.borrow_mut().current_details = details.into(); + } + + /// Get the current details string previously set via [`set_current_details`]. + /// + /// Returns an empty string if [`set_current_details`] has not been called. + pub fn get_current_details(&self) -> String { + self.base.inner.shared.borrow().current_details.clone() + } + /// Force a workflow task failure (EX: in order to retry on non-sticky queue) pub fn force_task_fail(&self, with: anyhow::Error) { self.base.send(with.into()); @@ -1071,6 +1091,20 @@ impl WorkflowContext { self.sync.upsert_memo(attr_iter) } + /// Set the current free-form details string for this workflow execution. + /// + /// See [`SyncWorkflowContext::set_current_details`]. + pub fn set_current_details(&self, details: impl Into) { + self.sync.set_current_details(details) + } + + /// Get the current details string previously set via [`set_current_details`]. + /// + /// See [`SyncWorkflowContext::get_current_details`]. + pub fn get_current_details(&self) -> String { + self.sync.get_current_details() + } + /// Force a workflow task failure (EX: in order to retry on non-sticky queue) pub fn force_task_fail(&self, with: anyhow::Error) { self.sync.force_task_fail(with) @@ -1186,6 +1220,8 @@ pub(crate) struct WorkflowContextSharedData { pub(crate) current_deployment_version: Option, pub(crate) search_attributes: SearchAttributes, pub(crate) random_seed: u64, + /// Current free-form details string, surfaced via `__temporal_workflow_metadata` query. + pub(crate) current_details: String, } /// A Future that can be cancelled. @@ -2085,3 +2121,49 @@ impl StartedNexusOperation { .cancel(CancellableID::NexusOp(self.unblock_dat.schedule_seq)); } } + +#[cfg(test)] +mod tests { + use super::*; + use temporalio_common::{ + data_converters::PayloadConverter, + protos::coresdk::workflow_activation::InitializeWorkflow, + }; + use tokio::sync::watch; + + fn make_base_ctx() -> BaseWorkflowContext { + let (cancel_tx, cancel_rx) = watch::channel(None); + // cancel_tx must be kept alive for the duration of the test + std::mem::forget(cancel_tx); + let (ctx, _rx) = BaseWorkflowContext::new( + "ns".into(), + "tq".into(), + "run-id".into(), + InitializeWorkflow::default(), + cancel_rx, + PayloadConverter::default(), + ); + ctx + } + + #[test] + fn current_details_defaults_to_empty_string() { + let ctx = make_base_ctx(); + assert_eq!(ctx.current_details(), ""); + } + + #[test] + fn set_and_get_current_details_roundtrip() { + let ctx = make_base_ctx(); + ctx.inner.shared.borrow_mut().current_details = "my details".into(); + assert_eq!(ctx.current_details(), "my details"); + } + + #[test] + fn set_current_details_overwrites_previous() { + let ctx = make_base_ctx(); + ctx.inner.shared.borrow_mut().current_details = "first".into(); + ctx.inner.shared.borrow_mut().current_details = "second".into(); + assert_eq!(ctx.current_details(), "second"); + } +} diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index 74b797b11..162ebea92 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -108,8 +108,8 @@ impl ActivityOptions { } .into(), ), - user_metadata: self.summary.map(|s| UserMetadata { - summary: Some(s.into()), + user_metadata: self.summary.and_then(|s| s.as_json_payload().ok()).map(|summary| UserMetadata { + summary: Some(summary), details: None, }), } @@ -251,8 +251,8 @@ impl ChildWorkflowOptions { ) -> WorkflowCommand { let user_metadata = if self.static_summary.is_some() || self.static_details.is_some() { Some(UserMetadata { - summary: self.static_summary.map(Into::into), - details: self.static_details.map(Into::into), + summary: self.static_summary.and_then(|s| s.as_json_payload().ok()), + details: self.static_details.and_then(|s| s.as_json_payload().ok()), }) } else { None diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index 468783b20..86ee1a847 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -14,6 +14,7 @@ use std::{ sync::mpsc::Receiver, task::{Context, Poll}, }; + use temporalio_common::{ data_converters::PayloadConverter, protos::{ @@ -248,15 +249,45 @@ impl WorkflowFuture { converter: &self.payload_converter, }; - let dispatch_result = match panic::catch_unwind(AssertUnwindSafe(|| { - self.execution.dispatch_query(&query_type, data) - })) { - Ok(r) => r, - Err(e) => Some(Err(anyhow!( - "Panic in query handler: {}", - panic_formatter(e) - ) - .into())), + let dispatch_result = if query_type == "__temporal_workflow_metadata" { + // Build the proto-JSON representation of WorkflowMetadata. + // The Go SDK uses "json/protobuf" (proto JSON) encoding, so we match that. + // Proto JSON uses lowerCamelCase field names; the only field we set is + // `current_details` → `currentDetails`. + let details = self.base_ctx.current_details(); + let json_bytes = if details.is_empty() { + b"{}".to_vec() + } else { + // serde_json::to_string produces a properly escaped JSON string. + let escaped = serde_json::to_string(&details) + .unwrap_or_else(|_| "\"\"".to_string()); + format!("{{\"currentDetails\":{escaped}}}").into_bytes() + }; + let payload = Payload { + metadata: [ + ("encoding".to_string(), b"json/protobuf".to_vec()), + ( + "messageType".to_string(), + b"temporal.api.sdk.v1.WorkflowMetadata".to_vec(), + ), + ] + .into_iter() + .collect(), + data: json_bytes, + ..Default::default() + }; + Some(Ok(payload)) + } else { + match panic::catch_unwind(AssertUnwindSafe(|| { + self.execution.dispatch_query(&query_type, data) + })) { + Ok(r) => r, + Err(e) => Some(Err(anyhow!( + "Panic in query handler: {}", + panic_formatter(e) + ) + .into())), + } }; let response = match dispatch_result { From 566ab43bda3fd363c47158a0592a16a9afafdf2a Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 13:34:42 -0400 Subject: [PATCH 02/33] Remove demo cargo entry. --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f1641428a..9f3843119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/macros", "crates/sdk", "crates/sdk-core-c-bridge", - "current-details-demo", ] resolver = "2" From 691789a7b3fa81ff43331e3116a42b7a022a42ff Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 13:51:17 -0400 Subject: [PATCH 03/33] Remove trivial accessor tests. --- crates/client/src/options_structs.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/crates/client/src/options_structs.rs b/crates/client/src/options_structs.rs index 16994050e..6153522db 100644 --- a/crates/client/src/options_structs.rs +++ b/crates/client/src/options_structs.rs @@ -461,29 +461,3 @@ pub struct WorkflowListOptions { #[derive(Debug, Clone, Default, bon::Builder)] #[non_exhaustive] pub struct WorkflowCountOptions {} - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn workflow_start_options_static_summary_default_none() { - let opts = WorkflowStartOptions::new("tq", "wf-id").build(); - assert!(opts.static_summary.is_none()); - assert!(opts.static_details.is_none()); - } - - #[test] - fn workflow_start_options_static_summary_set() { - let opts = WorkflowStartOptions::new("tq", "wf-id") - .static_summary("Order fulfillment workflow".to_string()) - .static_details("Validates the order, processes payment".to_string()) - .build(); - assert_eq!(opts.static_summary.as_deref(), Some("Order fulfillment workflow")); - assert_eq!( - opts.static_details.as_deref(), - Some("Validates the order, processes payment") - ); - } -} From fd77df644eeddc8b1ba54d094415dea93043e2ed Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 15:33:01 -0400 Subject: [PATCH 04/33] Remove trivial accessor tests; fmt. --- crates/client/src/lib.rs | 24 ++++++---- .../integ_tests/workflow_tests/queries.rs | 7 +-- crates/sdk/src/workflow_context.rs | 46 ------------------- crates/sdk/src/workflow_context/options.rs | 11 +++-- crates/sdk/src/workflow_future.rs | 4 -- 5 files changed, 25 insertions(+), 67 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 8890b7123..f34c27ecd 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -75,10 +75,10 @@ use temporalio_common::{ temporal::api::{ cloud::cloudservice::v1::cloud_service_client::CloudServiceClient, common::v1::{Memo, Payload, SearchAttributes, WorkflowType}, - sdk::v1::UserMetadata, enums::v1::{TaskQueueKind, WorkflowExecutionStatus}, errordetails::v1::WorkflowExecutionAlreadyStartedFailure, operatorservice::v1::operator_service_client::OperatorServiceClient, + sdk::v1::UserMetadata, taskqueue::v1::TaskQueue, testservice::v1::test_service_client::TestServiceClient, workflow::v1 as workflow, @@ -1028,15 +1028,19 @@ where let workflow_id = options.workflow_id.clone(); let task_queue_name = options.task_queue.clone(); - let user_metadata = - if options.static_summary.is_some() || options.static_details.is_some() { - Some(UserMetadata { - summary: options.static_summary.and_then(|s| s.as_json_payload().ok()), - details: options.static_details.and_then(|s| s.as_json_payload().ok()), - }) - } else { - None - }; + let user_metadata = if options.static_summary.is_some() || options.static_details.is_some() + { + Some(UserMetadata { + summary: options + .static_summary + .and_then(|s| s.as_json_payload().ok()), + details: options + .static_details + .and_then(|s| s.as_json_payload().ok()), + }) + } else { + None + }; let run_id = if let Some(start_signal) = options.start_signal { // Use signal-with-start when a start_signal is provided diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 2d7c8a880..7b7311fee 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -413,8 +413,6 @@ async fn query_returns_workflow_context_view_info() { worker.run().await.unwrap(); } -// ── current_details integration test ──────────────────────────────────────── - /// Workflow that sets current_details and then waits for a signal before completing. /// The wait ensures the workflow stays alive when the metadata query arrives. #[workflow] @@ -475,7 +473,10 @@ async fn workflow_metadata_query_returns_current_details() { .commands .iter() .any(|c| c.command_type() == CommandType::CompleteWorkflowExecution); - assert!(!has_complete, "Workflow should not complete on first activation"); + assert!( + !has_complete, + "Workflow should not complete on first activation" + ); }) .then(|wft| { // Second activation: the metadata query response. diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 6ef7e00bb..9b2d39016 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -2121,49 +2121,3 @@ impl StartedNexusOperation { .cancel(CancellableID::NexusOp(self.unblock_dat.schedule_seq)); } } - -#[cfg(test)] -mod tests { - use super::*; - use temporalio_common::{ - data_converters::PayloadConverter, - protos::coresdk::workflow_activation::InitializeWorkflow, - }; - use tokio::sync::watch; - - fn make_base_ctx() -> BaseWorkflowContext { - let (cancel_tx, cancel_rx) = watch::channel(None); - // cancel_tx must be kept alive for the duration of the test - std::mem::forget(cancel_tx); - let (ctx, _rx) = BaseWorkflowContext::new( - "ns".into(), - "tq".into(), - "run-id".into(), - InitializeWorkflow::default(), - cancel_rx, - PayloadConverter::default(), - ); - ctx - } - - #[test] - fn current_details_defaults_to_empty_string() { - let ctx = make_base_ctx(); - assert_eq!(ctx.current_details(), ""); - } - - #[test] - fn set_and_get_current_details_roundtrip() { - let ctx = make_base_ctx(); - ctx.inner.shared.borrow_mut().current_details = "my details".into(); - assert_eq!(ctx.current_details(), "my details"); - } - - #[test] - fn set_current_details_overwrites_previous() { - let ctx = make_base_ctx(); - ctx.inner.shared.borrow_mut().current_details = "first".into(); - ctx.inner.shared.borrow_mut().current_details = "second".into(); - assert_eq!(ctx.current_details(), "second"); - } -} diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index 162ebea92..c1405381f 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -108,10 +108,13 @@ impl ActivityOptions { } .into(), ), - user_metadata: self.summary.and_then(|s| s.as_json_payload().ok()).map(|summary| UserMetadata { - summary: Some(summary), - details: None, - }), + user_metadata: self + .summary + .and_then(|s| s.as_json_payload().ok()) + .map(|summary| UserMetadata { + summary: Some(summary), + details: None, + }), } } } diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index 86ee1a847..44045f58a 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -14,7 +14,6 @@ use std::{ sync::mpsc::Receiver, task::{Context, Poll}, }; - use temporalio_common::{ data_converters::PayloadConverter, protos::{ @@ -251,9 +250,6 @@ impl WorkflowFuture { let dispatch_result = if query_type == "__temporal_workflow_metadata" { // Build the proto-JSON representation of WorkflowMetadata. - // The Go SDK uses "json/protobuf" (proto JSON) encoding, so we match that. - // Proto JSON uses lowerCamelCase field names; the only field we set is - // `current_details` → `currentDetails`. let details = self.base_ctx.current_details(); let json_bytes = if details.is_empty() { b"{}".to_vec() From d7e8f542adab8ce8019cd2cdf4de48e5dcee15f1 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 15:41:00 -0400 Subject: [PATCH 05/33] Remove unused methods. --- crates/sdk/src/workflow_context.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 9b2d39016..f52235335 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -860,13 +860,6 @@ impl SyncWorkflowContext { self.base.inner.shared.borrow_mut().current_details = details.into(); } - /// Get the current details string previously set via [`set_current_details`]. - /// - /// Returns an empty string if [`set_current_details`] has not been called. - pub fn get_current_details(&self) -> String { - self.base.inner.shared.borrow().current_details.clone() - } - /// Force a workflow task failure (EX: in order to retry on non-sticky queue) pub fn force_task_fail(&self, with: anyhow::Error) { self.base.send(with.into()); @@ -1098,13 +1091,6 @@ impl WorkflowContext { self.sync.set_current_details(details) } - /// Get the current details string previously set via [`set_current_details`]. - /// - /// See [`SyncWorkflowContext::get_current_details`]. - pub fn get_current_details(&self) -> String { - self.sync.get_current_details() - } - /// Force a workflow task failure (EX: in order to retry on non-sticky queue) pub fn force_task_fail(&self, with: anyhow::Error) { self.sync.force_task_fail(with) From ff6f8a7ed12db431231eba8e2fcff9d8569f0cb1 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 15:48:29 -0400 Subject: [PATCH 06/33] Integration test for startic summary and details. --- .../workflow_tests/client_interactions.rs | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs index caaceb86e..72c200639 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs @@ -1,7 +1,7 @@ use crate::common::CoreWfStarter; use temporalio_client::{ - UntypedQuery, UntypedSignal, UntypedUpdate, WorkflowExecuteUpdateOptions, WorkflowQueryOptions, - WorkflowSignalOptions, WorkflowStartOptions, + UntypedQuery, UntypedSignal, UntypedUpdate, WorkflowDescribeOptions, + WorkflowExecuteUpdateOptions, WorkflowQueryOptions, WorkflowSignalOptions, WorkflowStartOptions, }; use temporalio_common::{ data_converters::{PayloadConverter, RawValue}, @@ -620,3 +620,73 @@ async fn test_typed_signal_query_update() { let result = handle.get_result(Default::default()).await.unwrap(); assert_eq!(result.counter, 100); } + +#[workflow] +#[derive(Default)] +struct ImmediatelyCompletingWf; + +#[workflow_methods] +impl ImmediatelyCompletingWf { + #[run] + async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { + Ok(()) + } +} + +/// Verify that `static_summary` and `static_details` set in `WorkflowStartOptions` are +/// stored on the server and visible via `DescribeWorkflowExecution`. +#[tokio::test] +async fn static_summary_and_details_visible_after_start() { + let wf_name = "static_summary_and_details_visible_after_start"; + let mut starter = CoreWfStarter::new(wf_name); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); + let mut worker = starter.worker().await; + worker.register_workflow::(); + + let task_queue = starter.get_task_queue().to_owned(); + let handle = worker + .submit_workflow( + ImmediatelyCompletingWf::run, + (), + WorkflowStartOptions::new(task_queue, wf_name) + .static_summary("my static summary".to_string()) + .static_details("my static details".to_string()) + .build(), + ) + .await + .unwrap(); + + worker.run_until_done().await.unwrap(); + + let description = handle + .describe(WorkflowDescribeOptions::default()) + .await + .unwrap(); + + let user_metadata = description + .raw_description + .execution_config + .expect("execution_config present") + .user_metadata + .expect("user_metadata present"); + + let summary_payload = user_metadata.summary.expect("summary present"); + assert_eq!( + summary_payload.metadata.get("encoding").map(|v| v.as_slice()), + Some(b"json/plain".as_slice()), + ); + assert_eq!( + serde_json::from_slice::(&summary_payload.data).unwrap(), + "my static summary", + ); + + let details_payload = user_metadata.details.expect("details present"); + assert_eq!( + details_payload.metadata.get("encoding").map(|v| v.as_slice()), + Some(b"json/plain".as_slice()), + ); + assert_eq!( + serde_json::from_slice::(&details_payload.data).unwrap(), + "my static details", + ); +} From dfdafdaea8e8936255a03fca67e9636bdfe951a2 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 8 Apr 2026 16:22:42 -0400 Subject: [PATCH 07/33] Use expect_legacy_query_matcher for confirming current details. --- .../sdk-core/src/test_help/integ_helpers.rs | 4 +- .../integ_tests/workflow_tests/queries.rs | 99 +++++++++---------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/crates/sdk-core/src/test_help/integ_helpers.rs b/crates/sdk-core/src/test_help/integ_helpers.rs index 829615a45..ea9bff7c4 100644 --- a/crates/sdk-core/src/test_help/integ_helpers.rs +++ b/crates/sdk-core/src/test_help/integ_helpers.rs @@ -4,7 +4,7 @@ pub use crate::{ internal_flags::CoreInternalFlags, - worker::{LEGACY_QUERY_ID, client::mocks::mock_worker_client}, + worker::{LEGACY_QUERY_ID, client::{LegacyQueryResult, mocks::mock_worker_client}}, }; use crate::{ @@ -16,7 +16,7 @@ use crate::{ sticky_q_name_for_worker, worker::{ TaskPollers, WorkerTelemetry, - client::{LegacyQueryResult, MockWorkerClient, WorkerClient, WorkflowTaskCompletion}, + client::{MockWorkerClient, WorkerClient, WorkflowTaskCompletion}, worker_config_builder, }, }; diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 7b7311fee..e0298a0f6 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -17,7 +17,7 @@ use temporalio_common::protos::{ use temporalio_macros::{workflow, workflow_methods}; use temporalio_sdk::{SyncWorkflowContext, WorkflowContext, WorkflowContextView, WorkflowResult}; use temporalio_sdk_core::test_help::{ - MockPollCfg, ResponseType, hist_to_poll_resp, mock_worker_client, + LegacyQueryResult, MockPollCfg, ResponseType, hist_to_poll_resp, mock_worker_client, }; /// A workflow that returns Pending on first poll and Ready on second poll. @@ -466,57 +466,54 @@ async fn workflow_metadata_query_returns_current_details() { mock_cfg.num_expected_legacy_query_resps = 1; mock_cfg.completion_asserts_from_expectations(|mut asserts| { - asserts - .then(|wft| { - // First activation: workflow runs and blocks. No completion command. - let has_complete = wft - .commands - .iter() - .any(|c| c.command_type() == CommandType::CompleteWorkflowExecution); - assert!( - !has_complete, - "Workflow should not complete on first activation" - ); - }) - .then(|wft| { - // Second activation: the metadata query response. - assert_eq!( - wft.query_responses.len(), - 1, - "Expected exactly one query response" - ); - let query_resp = &wft.query_responses[0]; - - match &query_resp.variant { - Some(query_result::Variant::Succeeded(success)) => { - let payload = success - .response - .as_ref() - .expect("Expected a response payload"); - - assert_eq!( - payload.metadata.get("encoding").map(|v| v.as_slice()), - Some(b"json/protobuf".as_slice()), - "Expected json/protobuf encoding" - ); - assert_eq!( - payload.metadata.get("messageType").map(|v| v.as_slice()), - Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), - "Expected WorkflowMetadata messageType" - ); + asserts.then(|wft| { + // First activation: workflow runs and blocks. Should produce no CompleteWorkflow command. + let has_complete = wft + .commands + .iter() + .any(|c| c.command_type() == CommandType::CompleteWorkflowExecution); + assert!( + !has_complete, + "Workflow should not complete on first activation" + ); + }); + }); - // The data is proto-JSON: {"currentDetails":"..."} - let json: serde_json::Value = - serde_json::from_slice(&payload.data).expect("valid JSON"); - assert_eq!( - json["currentDetails"].as_str(), - Some("details from workflow"), - "current_details should match what the workflow set" - ); - } - other => panic!("Expected Succeeded query response, got {:?}", other), - } - }); + // The legacy query response goes through respond_legacy_query, not complete_workflow_activation. + // Use expect_legacy_query_matcher to assert on the actual payload sent to the server. + mock_cfg.expect_legacy_query_matcher = Box::new(|_, result| { + let LegacyQueryResult::Succeeded(qr) = result else { + panic!("Expected Succeeded legacy query result"); + }; + let Some(query_result::Variant::Succeeded(success)) = &qr.variant else { + panic!("Expected Succeeded query result variant"); + }; + let payload = success + .response + .as_ref() + .expect("Expected a response payload in __temporal_workflow_metadata response"); + + assert_eq!( + payload.metadata.get("encoding").map(|v| v.as_slice()), + Some(b"json/protobuf".as_slice()), + "Expected json/protobuf encoding" + ); + assert_eq!( + payload.metadata.get("messageType").map(|v| v.as_slice()), + Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), + "Expected WorkflowMetadata messageType" + ); + + // The data is proto-JSON: {"currentDetails":"..."} + let json: serde_json::Value = + serde_json::from_slice(&payload.data).expect("Response data should be valid JSON"); + assert_eq!( + json["currentDetails"].as_str(), + Some("details from workflow"), + "current_details should match what the workflow set" + ); + + true }); let mut worker = build_fake_sdk(mock_cfg); From d10c4f264627c0038df8e00f7dc71db53ccf0813 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 10:09:47 -0400 Subject: [PATCH 08/33] Build JSON via struct. --- crates/sdk/src/workflow_future.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index 44045f58a..abca47fb1 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -249,16 +249,21 @@ impl WorkflowFuture { }; let dispatch_result = if query_type == "__temporal_workflow_metadata" { - // Build the proto-JSON representation of WorkflowMetadata. - let details = self.base_ctx.current_details(); - let json_bytes = if details.is_empty() { - b"{}".to_vec() - } else { - // serde_json::to_string produces a properly escaped JSON string. - let escaped = serde_json::to_string(&details) - .unwrap_or_else(|_| "\"\"".to_string()); - format!("{{\"currentDetails\":{escaped}}}").into_bytes() - }; + // Mirror the proto JSON shape of temporal.api.sdk.v1.WorkflowMetadata. + // Field names are camelCase per proto3 JSON; skip_serializing_if matches + // proto3 default-field omission behavior. + #[derive(serde::Serialize)] + struct WorkflowMetadataJson { + #[serde( + rename = "currentDetails", + skip_serializing_if = "String::is_empty" + )] + current_details: String, + } + let json_bytes = serde_json::to_vec(&WorkflowMetadataJson { + current_details: self.base_ctx.current_details(), + }) + .expect("WorkflowMetadata serialization is infallible"); let payload = Payload { metadata: [ ("encoding".to_string(), b"json/protobuf".to_vec()), From 22c32a091d4719952d8d090335763df3401c873d Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 10:27:02 -0400 Subject: [PATCH 09/33] Default values for infallible JSON encoding. --- crates/client/src/lib.rs | 4 ++-- crates/sdk/src/workflow_context/options.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index f34c27ecd..83c5735fb 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1033,10 +1033,10 @@ where Some(UserMetadata { summary: options .static_summary - .and_then(|s| s.as_json_payload().ok()), + .map(|s| s.as_json_payload().unwrap_or_default()), details: options .static_details - .and_then(|s| s.as_json_payload().ok()), + .map(|s| s.as_json_payload().unwrap_or_default()), }) } else { None diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index c1405381f..ae07a93ea 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -110,7 +110,7 @@ impl ActivityOptions { ), user_metadata: self .summary - .and_then(|s| s.as_json_payload().ok()) + .map(|s| s.as_json_payload().unwrap_or_default()) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -203,7 +203,7 @@ impl LocalActivityOptions { ), user_metadata: self .summary - .and_then(|summary| summary.as_json_payload().ok()) + .map(|summary| summary.as_json_payload().unwrap_or_default()) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -254,8 +254,8 @@ impl ChildWorkflowOptions { ) -> WorkflowCommand { let user_metadata = if self.static_summary.is_some() || self.static_details.is_some() { Some(UserMetadata { - summary: self.static_summary.and_then(|s| s.as_json_payload().ok()), - details: self.static_details.and_then(|s| s.as_json_payload().ok()), + summary: self.static_summary.map(|s| s.as_json_payload().unwrap_or_default()), + details: self.static_details.map(|s| s.as_json_payload().unwrap_or_default()), }) } else { None From 28d46cb5f5aae90d7e3b915e657a36d36b7c0dde Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 10:30:38 -0400 Subject: [PATCH 10/33] Assert command list is empty. --- .../tests/integ_tests/workflow_tests/queries.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index e0298a0f6..9cdfc1b34 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -467,14 +467,12 @@ async fn workflow_metadata_query_returns_current_details() { mock_cfg.completion_asserts_from_expectations(|mut asserts| { asserts.then(|wft| { - // First activation: workflow runs and blocks. Should produce no CompleteWorkflow command. - let has_complete = wft - .commands - .iter() - .any(|c| c.command_type() == CommandType::CompleteWorkflowExecution); + // First activation: workflow runs, sets current_details, and blocks. + // No commands should be emitted. assert!( - !has_complete, - "Workflow should not complete on first activation" + wft.commands.is_empty(), + "Expected no commands on first activation, got: {:?}", + wft.commands ); }); }); From 7127fa9cf7f4367a088346268e9faa5c5fd0e1d2 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 10:48:51 -0400 Subject: [PATCH 11/33] Remove unnecessary signal handler. Integration test for unset current details. --- .../integ_tests/workflow_tests/queries.rs | 81 ++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 9cdfc1b34..395f022d5 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -413,27 +413,34 @@ async fn query_returns_workflow_context_view_info() { worker.run().await.unwrap(); } -/// Workflow that sets current_details and then waits for a signal before completing. -/// The wait ensures the workflow stays alive when the metadata query arrives. +/// Workflow that blocks indefinitely without ever setting current_details. #[workflow] #[derive(Default)] -struct CurrentDetailsWf { - done: bool, +struct NoCurrentDetailsWf; + +#[workflow_methods] +impl NoCurrentDetailsWf { + #[run(name = DEFAULT_WORKFLOW_TYPE)] + async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { + std::future::pending::<()>().await; + Ok(()) + } } +/// Workflow that sets current_details and then blocks indefinitely. +/// The pending await keeps the workflow alive when the metadata query arrives. +#[workflow] +#[derive(Default)] +struct CurrentDetailsWf; + #[workflow_methods] impl CurrentDetailsWf { #[run(name = DEFAULT_WORKFLOW_TYPE)] async fn run(ctx: &mut WorkflowContext) -> WorkflowResult<()> { ctx.set_current_details("details from workflow"); - ctx.wait_condition(|s| s.done).await; + std::future::pending::<()>().await; Ok(()) } - - #[signal] - fn finish(&mut self, _ctx: &mut SyncWorkflowContext) { - self.done = true; - } } /// Verify that `__temporal_workflow_metadata` returns a proto-JSON-encoded `WorkflowMetadata` @@ -518,3 +525,57 @@ async fn workflow_metadata_query_returns_current_details() { worker.register_workflow::(); worker.run().await.unwrap(); } + +/// Verify that `__temporal_workflow_metadata` returns `{}` when `set_current_details` was never +/// called, matching proto3 JSON behavior where default (empty) fields are omitted. +#[tokio::test] +async fn workflow_metadata_query_empty_details() { + let wfid = "workflow_metadata_query_empty_test"; + + let mut t = TestHistoryBuilder::default(); + t.add_by_type(EventType::WorkflowExecutionStarted); + t.add_full_wf_task(); + + let tasks = [ + hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)), + { + let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)); + pr.query = Some(WorkflowQuery { + query_type: "__temporal_workflow_metadata".to_string(), + query_args: None, + header: None, + }); + pr.history = Some(Default::default()); + pr + }, + ]; + + let mut mock_cfg = MockPollCfg::from_resp_batches(wfid, t, tasks, mock_worker_client()); + mock_cfg.num_expected_legacy_query_resps = 1; + + mock_cfg.expect_legacy_query_matcher = Box::new(|_, result| { + let LegacyQueryResult::Succeeded(qr) = result else { + panic!("Expected Succeeded legacy query result"); + }; + let Some(query_result::Variant::Succeeded(success)) = &qr.variant else { + panic!("Expected Succeeded query result variant"); + }; + let payload = success + .response + .as_ref() + .expect("Expected a response payload"); + + // With no current_details set the field is omitted per proto3 JSON rules. + assert_eq!( + &payload.data, b"{}", + "Expected {{}} when current_details is empty, got: {}", + String::from_utf8_lossy(&payload.data) + ); + + true + }); + + let mut worker = build_fake_sdk(mock_cfg); + worker.register_workflow::(); + worker.run().await.unwrap(); +} From 8b5dd660d1cbf44458e6233cfe51cadec6ab8bc7 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 10:56:11 -0400 Subject: [PATCH 12/33] Pin serde_json version in workspace. --- Cargo.toml | 1 + crates/common/Cargo.toml | 2 +- crates/sdk-core-c-bridge/Cargo.toml | 2 +- crates/sdk-core/Cargo.toml | 2 +- crates/sdk/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9f3843119..d07fbeb00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ prost = "0.14" prost-types = { version = "0.7", package = "prost-wkt-types" } pbjson = "0.9" pbjson-build = "0.9" +serde_json = "1.0" [workspace.lints.rust] unreachable_pub = "warn" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index f88c5c8ab..7091a2070 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -75,7 +75,7 @@ prost-types = { workspace = true } rand = { version = "0.10", optional = true } ringbuf = { version = "0.4", optional = true } serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde_json = { workspace = true } thiserror = { workspace = true } tokio = { version = "1.47", features = [], optional = true } toml = { version = "1.0", optional = true } diff --git a/crates/sdk-core-c-bridge/Cargo.toml b/crates/sdk-core-c-bridge/Cargo.toml index 5b50d6231..5105b0a19 100644 --- a/crates/sdk-core-c-bridge/Cargo.toml +++ b/crates/sdk-core-c-bridge/Cargo.toml @@ -29,7 +29,7 @@ prost = { workspace = true } rand = "0.10" rand_pcg = "0.10" serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde_json = { workspace = true } tokio = "1.47" tokio-stream = "0.1" tokio-util = "0.7" diff --git a/crates/sdk-core/Cargo.toml b/crates/sdk-core/Cargo.toml index 1f49a5854..09116e58a 100644 --- a/crates/sdk-core/Cargo.toml +++ b/crates/sdk-core/Cargo.toml @@ -78,7 +78,7 @@ reqwest = { version = "0.13", features = [ "rustls", ], default-features = false, optional = true } serde = "1.0" -serde_json = "1.0" +serde_json = { workspace = true } siphasher = "1.0" slotmap = "1.0" sysinfo = { version = "0.38", default-features = false, features = ["system"] } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index af76ec864..2bc4c7c44 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -21,7 +21,7 @@ gethostname = "1.0.2" parking_lot = { version = "0.12", features = ["send_guard"] } prost-types = { workspace = true } serde = "1.0" -serde_json = "1.0" +serde_json = { workspace = true } thiserror = "2" tokio = { version = "1.47", features = [ "rt", From 774e1a23289849707d92f511d0e1120946518a0c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 11:07:22 -0400 Subject: [PATCH 13/33] Remove unnecessary .to_string() calls --- .../tests/integ_tests/workflow_tests/client_interactions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs index 72c200639..ae764ee00 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs @@ -649,8 +649,8 @@ async fn static_summary_and_details_visible_after_start() { ImmediatelyCompletingWf::run, (), WorkflowStartOptions::new(task_queue, wf_name) - .static_summary("my static summary".to_string()) - .static_details("my static details".to_string()) + .static_summary("my static summary") + .static_details("my static details") .build(), ) .await From 010c875a37c0d7e0f90762235303bf4746e0db54 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 11:52:54 -0400 Subject: [PATCH 14/33] Too much detail in comment. --- crates/sdk/src/workflow_future.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index abca47fb1..8245a2a4d 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -250,8 +250,6 @@ impl WorkflowFuture { let dispatch_result = if query_type == "__temporal_workflow_metadata" { // Mirror the proto JSON shape of temporal.api.sdk.v1.WorkflowMetadata. - // Field names are camelCase per proto3 JSON; skip_serializing_if matches - // proto3 default-field omission behavior. #[derive(serde::Serialize)] struct WorkflowMetadataJson { #[serde( From 4f211accf5c9da4d02863e36acf6df528da0278c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 13:35:37 -0400 Subject: [PATCH 15/33] .expect() rather than .unwrap(). Check encoding and messageType in empty details test. --- crates/client/src/lib.rs | 4 ++-- .../workflow_tests/client_interactions.rs | 6 ++++-- .../tests/integ_tests/workflow_tests/queries.rs | 13 ++++++++++++- crates/sdk/src/workflow_context/options.rs | 8 ++++---- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 83c5735fb..48d162efe 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1033,10 +1033,10 @@ where Some(UserMetadata { summary: options .static_summary - .map(|s| s.as_json_payload().unwrap_or_default()), + .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), details: options .static_details - .map(|s| s.as_json_payload().unwrap_or_default()), + .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), }) } else { None diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs index ae764ee00..674405764 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs @@ -676,7 +676,8 @@ async fn static_summary_and_details_visible_after_start() { Some(b"json/plain".as_slice()), ); assert_eq!( - serde_json::from_slice::(&summary_payload.data).unwrap(), + serde_json::from_slice::(&summary_payload.data) + .expect("summary payload data should deserialize as a JSON string"), "my static summary", ); @@ -686,7 +687,8 @@ async fn static_summary_and_details_visible_after_start() { Some(b"json/plain".as_slice()), ); assert_eq!( - serde_json::from_slice::(&details_payload.data).unwrap(), + serde_json::from_slice::(&details_payload.data) + .expect("details payload data should deserialize as a JSON string"), "my static details", ); } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 395f022d5..c7aab139b 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -454,7 +454,7 @@ async fn workflow_metadata_query_returns_current_details() { t.add_full_wf_task(); let tasks = [ - // First task: workflow starts, sets current_details, and blocks on wait_condition. + // First task: workflow starts, sets current_details, and blocks on pending. hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)), // Second task: legacy query for __temporal_workflow_metadata while workflow is blocked. { @@ -565,6 +565,17 @@ async fn workflow_metadata_query_empty_details() { .as_ref() .expect("Expected a response payload"); + assert_eq!( + payload.metadata.get("encoding").map(|v| v.as_slice()), + Some(b"json/protobuf".as_slice()), + "Expected json/protobuf encoding" + ); + assert_eq!( + payload.metadata.get("messageType").map(|v| v.as_slice()), + Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), + "Expected WorkflowMetadata messageType" + ); + // With no current_details set the field is omitted per proto3 JSON rules. assert_eq!( &payload.data, b"{}", diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index ae07a93ea..9d4fbd73e 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -110,7 +110,7 @@ impl ActivityOptions { ), user_metadata: self .summary - .map(|s| s.as_json_payload().unwrap_or_default()) + .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -203,7 +203,7 @@ impl LocalActivityOptions { ), user_metadata: self .summary - .map(|summary| summary.as_json_payload().unwrap_or_default()) + .map(|summary| summary.as_json_payload().expect("String-to-JSON payload serialization is infallible")) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -254,8 +254,8 @@ impl ChildWorkflowOptions { ) -> WorkflowCommand { let user_metadata = if self.static_summary.is_some() || self.static_details.is_some() { Some(UserMetadata { - summary: self.static_summary.map(|s| s.as_json_payload().unwrap_or_default()), - details: self.static_details.map(|s| s.as_json_payload().unwrap_or_default()), + summary: self.static_summary.map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), + details: self.static_details.map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), }) } else { None From f6dd3026f9806aafa1074ad326a9eea63e857827 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 13:41:05 -0400 Subject: [PATCH 16/33] Cleanup --- .../integ_tests/workflow_tests/queries.rs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index c7aab139b..ca6487eb9 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -413,20 +413,6 @@ async fn query_returns_workflow_context_view_info() { worker.run().await.unwrap(); } -/// Workflow that blocks indefinitely without ever setting current_details. -#[workflow] -#[derive(Default)] -struct NoCurrentDetailsWf; - -#[workflow_methods] -impl NoCurrentDetailsWf { - #[run(name = DEFAULT_WORKFLOW_TYPE)] - async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { - std::future::pending::<()>().await; - Ok(()) - } -} - /// Workflow that sets current_details and then blocks indefinitely. /// The pending await keeps the workflow alive when the metadata query arrives. #[workflow] @@ -526,6 +512,20 @@ async fn workflow_metadata_query_returns_current_details() { worker.run().await.unwrap(); } +/// Workflow that blocks indefinitely without ever setting current_details. +#[workflow] +#[derive(Default)] +struct NoCurrentDetailsWf; + +#[workflow_methods] +impl NoCurrentDetailsWf { + #[run(name = DEFAULT_WORKFLOW_TYPE)] + async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { + std::future::pending::<()>().await; + Ok(()) + } +} + /// Verify that `__temporal_workflow_metadata` returns `{}` when `set_current_details` was never /// called, matching proto3 JSON behavior where default (empty) fields are omitted. #[tokio::test] From 4f0af702a67014d83162d70ccfbcb63d2e0f161c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 13:42:26 -0400 Subject: [PATCH 17/33] Cleanup --- crates/sdk/src/workflow_context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index f52235335..33627ae25 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -852,7 +852,7 @@ impl SyncWorkflowContext { )) } - /// Set the current free-form details string for this workflow execution. + /// Set the current details string for this workflow execution. /// /// The value is surfaced to the Temporal server UI in real time via the /// `__temporal_workflow_metadata` built-in query. @@ -1084,7 +1084,7 @@ impl WorkflowContext { self.sync.upsert_memo(attr_iter) } - /// Set the current free-form details string for this workflow execution. + /// Set the current details string for this workflow execution. /// /// See [`SyncWorkflowContext::set_current_details`]. pub fn set_current_details(&self, details: impl Into) { @@ -1206,7 +1206,7 @@ pub(crate) struct WorkflowContextSharedData { pub(crate) current_deployment_version: Option, pub(crate) search_attributes: SearchAttributes, pub(crate) random_seed: u64, - /// Current free-form details string, surfaced via `__temporal_workflow_metadata` query. + /// Current details string, surfaced via `__temporal_workflow_metadata` query. pub(crate) current_details: String, } From fd3235c948d5aa66e2f03ce35c6bcf993cce20f9 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 15:13:22 -0400 Subject: [PATCH 18/33] Remove duplicate serde_json entry --- crates/sdk/Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index ef052cc26..7474a9432 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -62,11 +62,7 @@ rstest = "0.26" [features] default = [] antithesis_assertions = ["temporalio-sdk-core/antithesis_assertions"] -examples = ["serde/derive", "dep:serde_json"] - -[dependencies.serde_json] -version = "1" -optional = true +examples = ["serde/derive"] [[example]] name = "hello-world-worker" From 36d344065282cae005d7105bbaf5ef790cced896 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 15:20:31 -0400 Subject: [PATCH 19/33] Don't use internal query name in public docstrings. --- crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs | 4 ++-- crates/sdk/src/workflow_context.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index ca6487eb9..1694b53a1 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -429,7 +429,7 @@ impl CurrentDetailsWf { } } -/// Verify that `__temporal_workflow_metadata` returns a proto-JSON-encoded `WorkflowMetadata` +/// Verify that the query returns a proto-JSON-encoded `WorkflowMetadata` /// whose `current_details` field reflects the value set by `set_current_details`. #[tokio::test] async fn workflow_metadata_query_returns_current_details() { @@ -526,7 +526,7 @@ impl NoCurrentDetailsWf { } } -/// Verify that `__temporal_workflow_metadata` returns `{}` when `set_current_details` was never +/// Verify that the query returns `{}` when `set_current_details` was never /// called, matching proto3 JSON behavior where default (empty) fields are omitted. #[tokio::test] async fn workflow_metadata_query_empty_details() { diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index ce04ac602..0ace740cf 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -855,7 +855,7 @@ impl SyncWorkflowContext { /// Set the current details string for this workflow execution. /// /// The value is surfaced to the Temporal server UI in real time via the - /// `__temporal_workflow_metadata` built-in query. + /// the workflow metadata query. pub fn set_current_details(&self, details: impl Into) { self.base.inner.shared.borrow_mut().current_details = details.into(); } @@ -1208,7 +1208,7 @@ pub(crate) struct WorkflowContextSharedData { pub(crate) current_deployment_version: Option, pub(crate) search_attributes: SearchAttributes, pub(crate) random_seed: u64, - /// Current details string, surfaced via `__temporal_workflow_metadata` query. + /// Current details string, surfaced via the workflow metadata query. pub(crate) current_details: String, } From c2b37db3767423b9278ccf7abbcee7f080bf7b7c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 15:22:45 -0400 Subject: [PATCH 20/33] fmt --- crates/client/src/lib.rs | 14 +++++++------ .../sdk-core/src/test_help/integ_helpers.rs | 5 ++++- .../workflow_tests/client_interactions.rs | 13 +++++++++--- .../integ_tests/workflow_tests/queries.rs | 3 ++- crates/sdk/src/workflow_context/options.rs | 21 +++++++++++++++---- 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 663ca9f2a..75d6dbfde 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1034,12 +1034,14 @@ where let user_metadata = if options.static_summary.is_some() || options.static_details.is_some() { Some(UserMetadata { - summary: options - .static_summary - .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), - details: options - .static_details - .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), + summary: options.static_summary.map(|s| { + s.as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }), + details: options.static_details.map(|s| { + s.as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }), }) } else { None diff --git a/crates/sdk-core/src/test_help/integ_helpers.rs b/crates/sdk-core/src/test_help/integ_helpers.rs index ea9bff7c4..773244b96 100644 --- a/crates/sdk-core/src/test_help/integ_helpers.rs +++ b/crates/sdk-core/src/test_help/integ_helpers.rs @@ -4,7 +4,10 @@ pub use crate::{ internal_flags::CoreInternalFlags, - worker::{LEGACY_QUERY_ID, client::{LegacyQueryResult, mocks::mock_worker_client}}, + worker::{ + LEGACY_QUERY_ID, + client::{LegacyQueryResult, mocks::mock_worker_client}, + }, }; use crate::{ diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs index 674405764..dbb429156 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs @@ -1,7 +1,8 @@ use crate::common::CoreWfStarter; use temporalio_client::{ UntypedQuery, UntypedSignal, UntypedUpdate, WorkflowDescribeOptions, - WorkflowExecuteUpdateOptions, WorkflowQueryOptions, WorkflowSignalOptions, WorkflowStartOptions, + WorkflowExecuteUpdateOptions, WorkflowQueryOptions, WorkflowSignalOptions, + WorkflowStartOptions, }; use temporalio_common::{ data_converters::{PayloadConverter, RawValue}, @@ -672,7 +673,10 @@ async fn static_summary_and_details_visible_after_start() { let summary_payload = user_metadata.summary.expect("summary present"); assert_eq!( - summary_payload.metadata.get("encoding").map(|v| v.as_slice()), + summary_payload + .metadata + .get("encoding") + .map(|v| v.as_slice()), Some(b"json/plain".as_slice()), ); assert_eq!( @@ -683,7 +687,10 @@ async fn static_summary_and_details_visible_after_start() { let details_payload = user_metadata.details.expect("details present"); assert_eq!( - details_payload.metadata.get("encoding").map(|v| v.as_slice()), + details_payload + .metadata + .get("encoding") + .map(|v| v.as_slice()), Some(b"json/plain".as_slice()), ); assert_eq!( diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 1694b53a1..7750d39e5 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -578,7 +578,8 @@ async fn workflow_metadata_query_empty_details() { // With no current_details set the field is omitted per proto3 JSON rules. assert_eq!( - &payload.data, b"{}", + &payload.data, + b"{}", "Expected {{}} when current_details is empty, got: {}", String::from_utf8_lossy(&payload.data) ); diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index 9d4fbd73e..43d92d9ef 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -110,7 +110,10 @@ impl ActivityOptions { ), user_metadata: self .summary - .map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")) + .map(|s| { + s.as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -203,7 +206,11 @@ impl LocalActivityOptions { ), user_metadata: self .summary - .map(|summary| summary.as_json_payload().expect("String-to-JSON payload serialization is infallible")) + .map(|summary| { + summary + .as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }) .map(|summary| UserMetadata { summary: Some(summary), details: None, @@ -254,8 +261,14 @@ impl ChildWorkflowOptions { ) -> WorkflowCommand { let user_metadata = if self.static_summary.is_some() || self.static_details.is_some() { Some(UserMetadata { - summary: self.static_summary.map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), - details: self.static_details.map(|s| s.as_json_payload().expect("String-to-JSON payload serialization is infallible")), + summary: self.static_summary.map(|s| { + s.as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }), + details: self.static_details.map(|s| { + s.as_json_payload() + .expect("String-to-JSON payload serialization is infallible") + }), }) } else { None From f6a48524f41703e06989cb9eb6dc791a572ae93f Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 9 Apr 2026 15:53:35 -0400 Subject: [PATCH 21/33] Docstrings for LegacyQueryResult. --- crates/sdk-core/src/worker/client.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/sdk-core/src/worker/client.rs b/crates/sdk-core/src/worker/client.rs index 206da074e..0fce87351 100644 --- a/crates/sdk-core/src/worker/client.rs +++ b/crates/sdk-core/src/worker/client.rs @@ -44,8 +44,11 @@ use uuid::Uuid; type Result = std::result::Result; +/// The result of a legacy query sent via `respond_legacy_query`. pub enum LegacyQueryResult { + /// The query handler returned a result successfully. Succeeded(QueryResult), + /// The query handler failed. Failed(workflow_completion::Failure), } From da874441d227221f37d9fd9e08686f212d6d9662 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Fri, 10 Apr 2026 13:46:14 -0400 Subject: [PATCH 22/33] Use ctx.wait_condition in test workflows. TODO to remove inline `WorkflowMetadataJson` struct once we support both normal and proto JSON serialization. --- crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs | 4 ++-- crates/sdk/src/workflow_future.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 7750d39e5..8422e5d7f 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -424,7 +424,7 @@ impl CurrentDetailsWf { #[run(name = DEFAULT_WORKFLOW_TYPE)] async fn run(ctx: &mut WorkflowContext) -> WorkflowResult<()> { ctx.set_current_details("details from workflow"); - std::future::pending::<()>().await; + ctx.wait_condition(|_| false).await; Ok(()) } } @@ -521,7 +521,7 @@ struct NoCurrentDetailsWf; impl NoCurrentDetailsWf { #[run(name = DEFAULT_WORKFLOW_TYPE)] async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { - std::future::pending::<()>().await; + ctx.wait_condition(|_| false).await; Ok(()) } } diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index 2f29c2a05..d41b72a52 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -265,6 +265,7 @@ impl WorkflowFuture { let dispatch_result = if query_type == "__temporal_workflow_metadata" { // Mirror the proto JSON shape of temporal.api.sdk.v1.WorkflowMetadata. + // TODO [rust-sdk-branch]: support normal JSON and proto JSON serialization, and this will no longer be necessary. #[derive(serde::Serialize)] struct WorkflowMetadataJson { #[serde( From bd2de0182484bf96d4a2f53b997dd2a71a771e00 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Fri, 10 Apr 2026 14:10:10 -0400 Subject: [PATCH 23/33] Accessors through WorkflowExecutionDescription. --- crates/client/Cargo.toml | 1 + crates/client/src/workflow_handle.rs | 26 +++++++++++++ .../workflow_tests/client_interactions.rs | 39 +++---------------- .../integ_tests/workflow_tests/queries.rs | 2 +- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 7b538a67d..147f4b604 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -42,6 +42,7 @@ tracing = "0.1" url = "2.5" uuid = { version = "1.18", features = ["v4"] } rand = "0.10" +serde_json = { workspace = true } [dependencies.temporalio-common] path = "../common" diff --git a/crates/client/src/workflow_handle.rs b/crates/client/src/workflow_handle.rs index 661269c87..2d00181cd 100644 --- a/crates/client/src/workflow_handle.rs +++ b/crates/client/src/workflow_handle.rs @@ -73,6 +73,32 @@ impl WorkflowExecutionDescription { fn new(raw_description: DescribeWorkflowExecutionResponse) -> Self { Self { raw_description } } + + /// The static summary set when the workflow was started, if any. + pub fn static_summary(&self) -> Option { + let payload = self + .raw_description + .execution_config + .as_ref()? + .user_metadata + .as_ref()? + .summary + .as_ref()?; + serde_json::from_slice(&payload.data).ok() + } + + /// The static details set when the workflow was started, if any. + pub fn static_details(&self) -> Option { + let payload = self + .raw_description + .execution_config + .as_ref()? + .user_metadata + .as_ref()? + .details + .as_ref()?; + serde_json::from_slice(&payload.data).ok() + } } // TODO [rust-sdk-branch]: Could implment stream a-la ListWorkflowsStream diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs index dbb429156..3004db806 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/client_interactions.rs @@ -664,38 +664,9 @@ async fn static_summary_and_details_visible_after_start() { .await .unwrap(); - let user_metadata = description - .raw_description - .execution_config - .expect("execution_config present") - .user_metadata - .expect("user_metadata present"); - - let summary_payload = user_metadata.summary.expect("summary present"); - assert_eq!( - summary_payload - .metadata - .get("encoding") - .map(|v| v.as_slice()), - Some(b"json/plain".as_slice()), - ); - assert_eq!( - serde_json::from_slice::(&summary_payload.data) - .expect("summary payload data should deserialize as a JSON string"), - "my static summary", - ); - - let details_payload = user_metadata.details.expect("details present"); - assert_eq!( - details_payload - .metadata - .get("encoding") - .map(|v| v.as_slice()), - Some(b"json/plain".as_slice()), - ); - assert_eq!( - serde_json::from_slice::(&details_payload.data) - .expect("details payload data should deserialize as a JSON string"), - "my static details", - ); + let summary = description.static_summary().expect("summary present"); + assert_eq!(summary, "my static summary",); + + let details = description.static_details().expect("details present"); + assert_eq!(details, "my static details",); } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 8422e5d7f..17832d9f8 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -520,7 +520,7 @@ struct NoCurrentDetailsWf; #[workflow_methods] impl NoCurrentDetailsWf { #[run(name = DEFAULT_WORKFLOW_TYPE)] - async fn run(_ctx: &mut WorkflowContext) -> WorkflowResult<()> { + async fn run(ctx: &mut WorkflowContext) -> WorkflowResult<()> { ctx.wait_condition(|_| false).await; Ok(()) } From bf888659a685e15eb213638bdbb702c6ed004426 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Fri, 10 Apr 2026 15:40:45 -0400 Subject: [PATCH 24/33] Update tests to expect JSON summaries. --- .../sdk-core/tests/integ_tests/workflow_tests/activities.rs | 5 +++-- .../tests/integ_tests/workflow_tests/child_workflows.rs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index 41c2b92a3..45ac5222c 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -21,7 +21,8 @@ use temporalio_common::{ protos::{ DEFAULT_ACTIVITY_TYPE, DEFAULT_WORKFLOW_TYPE, TestHistoryBuilder, canned_histories, coresdk::{ - ActivityHeartbeat, ActivityTaskCompletion, IntoCompletion, IntoPayloadsExt, + ActivityHeartbeat, ActivityTaskCompletion, AsJsonPayloadExt, IntoCompletion, + IntoPayloadsExt, activity_result::{ self, ActivityExecutionResult, ActivityResolution, activity_resolution as act_res, }, @@ -1286,7 +1287,7 @@ async fn pass_activity_summary_to_metadata() { let wf_id = mock_cfg.hists[0].wf_id.clone(); let wf_type = DEFAULT_WORKFLOW_TYPE; let expected_user_metadata = Some(UserMetadata { - summary: Some(b"activity summary".into()), + summary: Some("activity summary".as_json_payload().unwrap()), details: None, }); mock_cfg.completion_asserts_from_expectations(|mut asserts| { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs index b167d8e3e..984d83110 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs @@ -640,8 +640,8 @@ async fn pass_child_workflow_summary_to_metadata() { let t = canned_histories::single_child_workflow(wf_id); let mut mock_cfg = MockPollCfg::from_hist_builder(t); let expected_user_metadata = Some(UserMetadata { - summary: Some(b"child summary".into()), - details: Some(b"child details".into()), + summary: Some("child summary".as_json_payload().unwrap()), + details: Some("child details".as_json_payload().unwrap()), }); mock_cfg.completion_asserts_from_expectations(|mut asserts| { asserts From a1f1e3bc2dfb1537e2be9c7ee2e1ac74ab22b23c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 13:16:40 -0400 Subject: [PATCH 25/33] Use default converter for query results. --- Cargo.toml | 1 + crates/sdk/src/workflow_future.rs | 30 ++++++++++++------------------ 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d07fbeb00..a085af074 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/macros", "crates/sdk", "crates/sdk-core-c-bridge", + "current-details-demo", ] resolver = "2" diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index d41b72a52..f372fe647 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -16,7 +16,7 @@ use std::{ task::{Context, Poll}, }; use temporalio_common::{ - data_converters::PayloadConverter, + data_converters::{GenericPayloadConverter,PayloadConverter,SerializationContext,SerializationContextData}, protos::{ coresdk::{ workflow_activation::{ @@ -274,24 +274,18 @@ impl WorkflowFuture { )] current_details: String, } - let json_bytes = serde_json::to_vec(&WorkflowMetadataJson { - current_details: self.base_ctx.current_details(), - }) - .expect("WorkflowMetadata serialization is infallible"); - let payload = Payload { - metadata: [ - ("encoding".to_string(), b"json/protobuf".to_vec()), - ( - "messageType".to_string(), - b"temporal.api.sdk.v1.WorkflowMetadata".to_vec(), - ), - ] - .into_iter() - .collect(), - data: json_bytes, - ..Default::default() + let converter = PayloadConverter::default(); + let ctx = SerializationContext { + data: &SerializationContextData::Workflow, + converter: &converter, }; - Some(Ok(payload)) + let payload = converter.to_payload( + &ctx, + &WorkflowMetadataJson { + current_details: self.base_ctx.current_details(), + }, + ); + Some(payload.map_err(|e| anyhow::Error::from(e).into())) } else { match panic::catch_unwind(AssertUnwindSafe(|| { self.execution.dispatch_query(&query_type, data) From 073310d105f4ba0b40d1e0906298a12b0f30204c Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 13:37:33 -0400 Subject: [PATCH 26/33] TOOD: Use DataConverter to avoid direct dependency on serde_json --- crates/client/src/workflow_handle.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/client/src/workflow_handle.rs b/crates/client/src/workflow_handle.rs index 2d00181cd..441ccc30b 100644 --- a/crates/client/src/workflow_handle.rs +++ b/crates/client/src/workflow_handle.rs @@ -75,6 +75,7 @@ impl WorkflowExecutionDescription { } /// The static summary set when the workflow was started, if any. + // TOOD: Use DataConverter to avoid direct dependency on serde_json pub fn static_summary(&self) -> Option { let payload = self .raw_description @@ -88,6 +89,7 @@ impl WorkflowExecutionDescription { } /// The static details set when the workflow was started, if any. + // TOOD: Use DataConverter to avoid direct dependency on serde_json pub fn static_details(&self) -> Option { let payload = self .raw_description From 6618bb04b7ab267508eaadcf49b8827ccccd66a9 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 14:58:30 -0400 Subject: [PATCH 27/33] Remove current-details-demo entry --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a085af074..d07fbeb00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/macros", "crates/sdk", "crates/sdk-core-c-bridge", - "current-details-demo", ] resolver = "2" From d594dd284eed48bf9e4d52801b4a43b167f066be Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 16:09:53 -0400 Subject: [PATCH 28/33] Update test for plain serialization --- .../sdk-core/tests/integ_tests/workflow_tests/queries.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index 17832d9f8..f5ed30ac5 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -486,13 +486,8 @@ async fn workflow_metadata_query_returns_current_details() { assert_eq!( payload.metadata.get("encoding").map(|v| v.as_slice()), - Some(b"json/protobuf".as_slice()), - "Expected json/protobuf encoding" - ); - assert_eq!( - payload.metadata.get("messageType").map(|v| v.as_slice()), - Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), - "Expected WorkflowMetadata messageType" + Some(b"json/plain".as_slice()), + "Expected json/plain encoding" ); // The data is proto-JSON: {"currentDetails":"..."} From 6602431256b1db7aadef932a69eee800b6832ac6 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 16:10:09 -0400 Subject: [PATCH 29/33] fmt --- crates/sdk/src/workflow_future.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index f372fe647..9ec9b326c 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -16,7 +16,9 @@ use std::{ task::{Context, Poll}, }; use temporalio_common::{ - data_converters::{GenericPayloadConverter,PayloadConverter,SerializationContext,SerializationContextData}, + data_converters::{ + GenericPayloadConverter, PayloadConverter, SerializationContext, SerializationContextData, + }, protos::{ coresdk::{ workflow_activation::{ From 50c0e2bf9a7b51393540c11f36fba1c22d7410e2 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 16:20:30 -0400 Subject: [PATCH 30/33] Update test for plain serialization --- .../sdk-core/tests/integ_tests/workflow_tests/queries.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs index f5ed30ac5..a2ea8c081 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/queries.rs @@ -562,13 +562,8 @@ async fn workflow_metadata_query_empty_details() { assert_eq!( payload.metadata.get("encoding").map(|v| v.as_slice()), - Some(b"json/protobuf".as_slice()), - "Expected json/protobuf encoding" - ); - assert_eq!( - payload.metadata.get("messageType").map(|v| v.as_slice()), - Some(b"temporal.api.sdk.v1.WorkflowMetadata".as_slice()), - "Expected WorkflowMetadata messageType" + Some(b"json/plain".as_slice()), + "Expected json/plain encoding" ); // With no current_details set the field is omitted per proto3 JSON rules. From 2b35523f6f90ee267f247c280930e7f3d811ba70 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 16:29:59 -0400 Subject: [PATCH 31/33] Remove serde_json dependency --- crates/sdk/Cargo.toml | 1 - crates/sdk/src/workflow_future.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 7474a9432..85e826d53 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -24,7 +24,6 @@ gethostname = "1.0.2" parking_lot = { version = "0.12", features = ["send_guard"] } prost-types = { workspace = true } serde = "1.0" -serde_json = { workspace = true } thiserror = "2" tokio = { version = "1.47", features = [ "rt", diff --git a/crates/sdk/src/workflow_future.rs b/crates/sdk/src/workflow_future.rs index 9ec9b326c..57cbe45cf 100644 --- a/crates/sdk/src/workflow_future.rs +++ b/crates/sdk/src/workflow_future.rs @@ -287,7 +287,7 @@ impl WorkflowFuture { current_details: self.base_ctx.current_details(), }, ); - Some(payload.map_err(|e| anyhow::Error::from(e).into())) + Some(Ok(payload?)) } else { match panic::catch_unwind(AssertUnwindSafe(|| { self.execution.dispatch_query(&query_type, data) From 8586cccd475f0a7ada9beb4ae89c17e2ff0e988f Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 16:58:05 -0400 Subject: [PATCH 32/33] serde_json required for example linting --- crates/sdk/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 85e826d53..d4abfa2d5 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -57,6 +57,7 @@ version = "0.2" [dev-dependencies] futures = "0.3" rstest = "0.26" +serde_json = { workspace = true } [features] default = [] From 921f5da3eb46e52c0dd9c610f7b7841e349e6c0e Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Mon, 13 Apr 2026 17:02:05 -0400 Subject: [PATCH 33/33] Or rather, under the examples feature --- crates/sdk/Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index d4abfa2d5..df5324757 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -57,12 +57,15 @@ version = "0.2" [dev-dependencies] futures = "0.3" rstest = "0.26" -serde_json = { workspace = true } [features] default = [] antithesis_assertions = ["temporalio-sdk-core/antithesis_assertions"] -examples = ["serde/derive"] +examples = ["serde/derive", "dep:serde_json"] + +[dependencies.serde_json] +version = "1" +optional = true [[example]] name = "hello-world-worker"