Skip to main content

temporalio_client/
workflow_handle.rs

1use crate::{
2    CancelWorkflowInput, DescribeWorkflowInput, DescribeWorkflowOutput,
3    FetchWorkflowHistoryPageInput, FetchWorkflowHistoryPageOutput, NamespacedClient, Next,
4    PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput, QueryWorkflowOutput,
5    RpcOptions, SignalWorkflowInput, StartWorkflowUpdateInput, StartWorkflowUpdateOutput,
6    TerminateWorkflowInput, WorkflowCancelOptions, WorkflowDescribeOptions,
7    WorkflowExecuteUpdateOptions, WorkflowExecutionStatus, WorkflowFetchHistoryOptions,
8    WorkflowGetResultOptions, WorkflowQueryOptions, WorkflowSignalOptions,
9    WorkflowStartUpdateOptions, WorkflowTerminateOptions,
10    errors::{
11        WorkflowGetResultError, WorkflowInteractionError, WorkflowQueryError, WorkflowUpdateError,
12    },
13    grpc::WorkflowService,
14    interceptors,
15};
16use futures_util::future::BoxFuture;
17use std::{fmt::Debug, marker::PhantomData};
18pub use temporalio_common::UntypedWorkflow;
19use temporalio_common::{
20    HasWorkflowDefinition, QueryDefinition, SignalDefinition, UpdateDefinition, WorkflowDefinition,
21    data_converters::{
22        DataConverter, DecodablePayloads, GenericPayloadConverter, PayloadConversionError,
23        PayloadConverter, RawValue, SerializationContext, SerializationContextData,
24    },
25    error::IncomingError,
26    payload_visitor::decode_payloads,
27    protos::{
28        coresdk::FromPayloadsExt,
29        proto_ts_to_system_time,
30        temporal::api::{
31            common::v1::{Payload, Payloads, WorkflowExecution as ProtoWorkflowExecution},
32            enums::v1::{HistoryEventFilterType, UpdateWorkflowExecutionLifecycleStage},
33            history::{
34                self,
35                v1::{History, HistoryEvent, history_event::Attributes},
36            },
37            query::v1::WorkflowQuery,
38            sdk::v1::UserMetadata,
39            update::{self, v1::WaitPolicy},
40            workflow::v1 as workflow,
41            workflowservice::v1::{
42                DescribeWorkflowExecutionRequest, DescribeWorkflowExecutionResponse,
43                GetWorkflowExecutionHistoryRequest, PollWorkflowExecutionUpdateRequest,
44                QueryWorkflowRequest, RequestCancelWorkflowExecutionRequest,
45                SignalWorkflowExecutionRequest, TerminateWorkflowExecutionRequest,
46                UpdateWorkflowExecutionRequest,
47            },
48        },
49    },
50    search_attributes::SearchAttributes,
51};
52use tonic::IntoRequest;
53use uuid::Uuid;
54
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
56struct DecodedUserMetadata {
57    summary: Option<String>,
58    details: Option<String>,
59}
60
61fn decode_user_metadata(
62    context: &SerializationContextData,
63    user_metadata: Option<UserMetadata>,
64) -> Result<DecodedUserMetadata, PayloadConversionError> {
65    let payload_converter = PayloadConverter::default();
66    let context = SerializationContext {
67        data: context,
68        converter: &payload_converter,
69    };
70    let (summary, details) = user_metadata
71        .map(|metadata| (metadata.summary, metadata.details))
72        .unwrap_or_default();
73    Ok(DecodedUserMetadata {
74        summary: match summary {
75            Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
76            None => None,
77        },
78        details: match details {
79            Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
80            None => None,
81        },
82    })
83}
84
85/// Details attached to a cancelled or terminated workflow result.
86#[derive(Clone, Debug)]
87#[non_exhaustive]
88pub struct WorkflowResultDetails {
89    payloads: DecodablePayloads,
90}
91
92impl WorkflowResultDetails {
93    async fn new(
94        payloads: Vec<Payload>,
95        data_converter: &DataConverter,
96    ) -> Result<Self, PayloadConversionError> {
97        let payloads = data_converter
98            .codec()
99            .decode(&SerializationContextData::Workflow, payloads)
100            .await?;
101        Ok(Self {
102            payloads: DecodablePayloads::new(
103                payloads,
104                data_converter.payload_converter().clone(),
105                SerializationContextData::Workflow,
106            ),
107        })
108    }
109
110    /// Deserialize the details into a typed value using the client's payload converter.
111    pub fn deserialize<T: temporalio_common::data_converters::TemporalDeserializable + 'static>(
112        &self,
113    ) -> Result<T, PayloadConversionError> {
114        self.payloads.deserialize()
115    }
116
117    /// Returns the codec-decoded payloads.
118    pub fn raw(&self) -> &[Payload] {
119        self.payloads.raw()
120    }
121
122    /// Consume these details and return their codec-decoded payloads.
123    pub fn into_raw(self) -> RawValue {
124        self.payloads.into_raw()
125    }
126}
127
128/// Enumerates terminal states for a particular workflow execution
129#[derive(Debug)]
130#[allow(clippy::large_enum_variant)]
131pub enum WorkflowExecutionResult<T> {
132    /// The workflow finished successfully
133    Succeeded(T),
134    /// The workflow finished in failure
135    Failed(IncomingError),
136    /// The workflow was cancelled
137    Cancelled {
138        /// Details provided at cancellation time
139        details: WorkflowResultDetails,
140    },
141    /// The workflow was terminated
142    Terminated {
143        /// Details provided at termination time
144        details: WorkflowResultDetails,
145    },
146    /// The workflow timed out
147    TimedOut,
148    /// The workflow continued as new
149    ContinuedAsNew,
150}
151
152/// Description of a workflow execution returned by `WorkflowHandle::describe`.
153///
154/// Access to the underlying Protobuf message is provided by [`raw`](Self::raw).
155#[derive(Debug, Clone)]
156pub struct WorkflowExecutionDescription {
157    /// The raw proto response from the server.
158    pub raw_description: DescribeWorkflowExecutionResponse,
159    history_length: usize,
160    static_summary: Option<String>,
161    static_details: Option<String>,
162    data_converter: DataConverter,
163}
164
165impl WorkflowExecutionDescription {
166    async fn new(
167        mut raw_description: DescribeWorkflowExecutionResponse,
168        data_converter: &DataConverter,
169    ) -> Result<Self, PayloadConversionError> {
170        let raw_user_metadata = raw_description
171            .execution_config
172            .as_ref()
173            .and_then(|cfg| cfg.user_metadata.clone());
174        decode_payloads(
175            &mut raw_description,
176            data_converter.codec(),
177            &SerializationContextData::Workflow,
178        )
179        .await?;
180        let decoded_metadata =
181            decode_user_metadata(&SerializationContextData::Workflow, raw_user_metadata)?;
182        let history_length_raw = raw_description
183            .workflow_execution_info
184            .as_ref()
185            .map(|info| info.history_length)
186            .unwrap_or(0);
187        let history_length = history_length_raw.try_into().map_err(|_| {
188            PayloadConversionError::EncodingError(
189                format!("workflow history_length must be non-negative, got {history_length_raw}")
190                    .into(),
191            )
192        })?;
193        Ok(Self {
194            raw_description,
195            history_length,
196            static_summary: decoded_metadata.summary,
197            static_details: decoded_metadata.details,
198            data_converter: data_converter.clone(),
199        })
200    }
201
202    /// The workflow ID.
203    pub fn id(&self) -> &str {
204        self.execution().workflow_id.as_str()
205    }
206
207    /// The run ID.
208    pub fn run_id(&self) -> &str {
209        self.execution().run_id.as_str()
210    }
211
212    /// The workflow type name.
213    pub fn workflow_type(&self) -> &str {
214        self.workflow_type_info().name.as_str()
215    }
216
217    /// The current status of the workflow execution.
218    pub fn status(&self) -> WorkflowExecutionStatus {
219        WorkflowExecutionStatus::from_raw(self.workflow_info().status)
220    }
221
222    /// When the workflow was created.
223    pub fn start_time(&self) -> Option<std::time::SystemTime> {
224        self.workflow_info()
225            .start_time
226            .as_ref()
227            .and_then(proto_ts_to_system_time)
228    }
229
230    /// When the workflow run started or should start.
231    pub fn execution_time(&self) -> Option<std::time::SystemTime> {
232        self.workflow_info()
233            .execution_time
234            .as_ref()
235            .and_then(proto_ts_to_system_time)
236    }
237
238    /// When the workflow was closed, if closed.
239    pub fn close_time(&self) -> Option<std::time::SystemTime> {
240        self.workflow_info()
241            .close_time
242            .as_ref()
243            .and_then(proto_ts_to_system_time)
244    }
245
246    /// The task queue the workflow runs on.
247    pub fn task_queue(&self) -> &str {
248        self.workflow_info().task_queue.as_str()
249    }
250
251    /// Number of events in history.
252    pub fn history_length(&self) -> usize {
253        self.history_length
254    }
255
256    /// Workflow memo decoded with the client's payload converter.
257    pub fn memo(&self) -> crate::Memo {
258        crate::Memo::from_raw(
259            self.workflow_info().memo.clone(),
260            self.data_converter.payload_converter().clone(),
261            SerializationContextData::Workflow,
262        )
263    }
264
265    /// Parent workflow ID, if this is a child workflow.
266    pub fn parent_id(&self) -> Option<&str> {
267        self.workflow_info()
268            .parent_execution
269            .as_ref()
270            .map(|e| e.workflow_id.as_str())
271    }
272
273    /// Parent run ID, if this is a child workflow.
274    pub fn parent_run_id(&self) -> Option<&str> {
275        self.workflow_info()
276            .parent_execution
277            .as_ref()
278            .map(|e| e.run_id.as_str())
279    }
280
281    /// Search attributes on the workflow.
282    pub fn search_attributes(&self) -> SearchAttributes {
283        self.workflow_info()
284            .search_attributes
285            .as_ref()
286            .map(SearchAttributes::from_proto)
287            .unwrap_or_default()
288    }
289
290    /// Static summary configured on the workflow, if present.
291    pub fn static_summary(&self) -> Option<&str> {
292        self.static_summary.as_deref()
293    }
294
295    /// Static details configured on the workflow, if present.
296    pub fn static_details(&self) -> Option<&str> {
297        self.static_details.as_deref()
298    }
299
300    /// Access the raw proto for additional fields not exposed via accessors.
301    pub fn raw(&self) -> &DescribeWorkflowExecutionResponse {
302        &self.raw_description
303    }
304
305    /// Consume the wrapper and return the raw proto.
306    pub fn into_raw(self) -> DescribeWorkflowExecutionResponse {
307        self.raw_description
308    }
309
310    fn workflow_info(&self) -> &workflow::WorkflowExecutionInfo {
311        self.raw_description
312            .workflow_execution_info
313            .as_ref()
314            .expect("describe response missing workflow_execution_info")
315    }
316
317    fn execution(&self) -> &ProtoWorkflowExecution {
318        self.workflow_info()
319            .execution
320            .as_ref()
321            .expect("describe response missing workflow_execution_info.execution")
322    }
323
324    fn workflow_type_info(
325        &self,
326    ) -> &temporalio_common::protos::temporal::api::common::v1::WorkflowType {
327        self.workflow_info()
328            .r#type
329            .as_ref()
330            .expect("describe response missing workflow_execution_info.type")
331    }
332}
333
334// TODO [rust-sdk-branch]: Could implment stream a-la ListWorkflowsStream
335/// Workflow execution history returned by `WorkflowHandle::fetch_history`.
336#[derive(Debug, Clone)]
337pub struct WorkflowHistory {
338    events: Vec<HistoryEvent>,
339    workflow_id: Option<String>,
340}
341impl From<WorkflowHistory> for history::v1::History {
342    fn from(h: WorkflowHistory) -> Self {
343        Self { events: h.events }
344    }
345}
346
347/// Error converting a workflow history to or from JSON.
348#[derive(Debug, thiserror::Error)]
349#[error("failed to convert workflow history JSON: {0}")]
350pub struct WorkflowHistoryJsonError(#[from] serde_json::Error);
351
352impl WorkflowHistory {
353    fn new(events: Vec<HistoryEvent>, workflow_id: Option<String>) -> Self {
354        Self {
355            events,
356            workflow_id,
357        }
358    }
359
360    /// Decode a workflow history from JSON bytes.
361    pub fn from_json(bytes: &[u8]) -> Result<Self, WorkflowHistoryJsonError> {
362        let history: History = serde_json::from_slice(bytes)?;
363        let workflow_id = history
364            .events
365            .first()
366            .and_then(|event| match event.attributes.as_ref() {
367                Some(Attributes::WorkflowExecutionStartedEventAttributes(attributes)) => {
368                    Some(attributes)
369                }
370                _ => None,
371            })
372            .map(|attributes| attributes.workflow_id.clone())
373            .filter(|wfid| !wfid.is_empty());
374        Ok(Self::new(history.events, workflow_id))
375    }
376
377    /// Encode this workflow history as JSON bytes.
378    pub fn to_json(&self) -> Result<Vec<u8>, WorkflowHistoryJsonError> {
379        Ok(serde_json::to_vec(&History {
380            events: self.events.clone(),
381        })?)
382    }
383
384    /// Return the workflow ID when it is known.
385    pub fn workflow_id(&self) -> Option<&str> {
386        self.workflow_id.as_deref()
387    }
388
389    /// The history events.
390    pub fn events(&self) -> &[HistoryEvent] {
391        &self.events
392    }
393
394    /// Consume the history and return the events.
395    pub fn into_events(self) -> Vec<HistoryEvent> {
396        self.events
397    }
398}
399
400/// A workflow handle which can refer to a specific workflow run, or a chain of workflow runs with
401/// the same workflow id.
402#[derive(Clone)]
403pub struct WorkflowHandle<ClientT, W> {
404    client: ClientT,
405    info: WorkflowExecutionInfo,
406
407    _wf_type: PhantomData<W>,
408}
409
410impl<CT, W> WorkflowHandle<CT, W> {
411    /// Return the run id of the Workflow Execution pointed at by this handle, if there is one.
412    pub fn run_id(&self) -> Option<&str> {
413        self.info.run_id.as_deref()
414    }
415}
416
417/// Holds needed information to refer to a specific workflow run, or workflow execution chain
418#[derive(Debug, Clone)]
419pub struct WorkflowExecutionInfo {
420    /// Namespace the workflow lives in.
421    pub namespace: String,
422    /// The workflow's id.
423    pub workflow_id: String,
424    /// If set, target this specific run of the workflow.
425    pub run_id: Option<String>,
426    /// Run ID used for cancellation and termination to ensure they happen on a workflow starting
427    /// with this run ID. This can be set when getting a workflow handle. When starting a workflow,
428    /// this is set as the resulting run ID if no start signal was provided.
429    pub first_execution_run_id: Option<String>,
430}
431
432impl WorkflowExecutionInfo {
433    /// Bind the workflow info to a specific client, turning it into a workflow handle
434    pub fn bind_untyped<CT>(self, client: CT) -> UntypedWorkflowHandle<CT>
435    where
436        CT: WorkflowService + Clone,
437    {
438        UntypedWorkflowHandle::new(client, self)
439    }
440}
441
442/// A workflow handle to a workflow with unknown types. Uses single argument raw payloads for input
443/// and output.
444pub type UntypedWorkflowHandle<CT> = WorkflowHandle<CT, UntypedWorkflow>;
445
446/// Marker type for sending untyped signals. Stores the signal name for runtime lookup.
447///
448/// Use with `handle.signal(UntypedSignal::new("signal_name"), raw_payload)`.
449pub struct UntypedSignal<W> {
450    name: String,
451    _wf: PhantomData<W>,
452}
453
454impl<W> UntypedSignal<W> {
455    /// Create a new `UntypedSignal` with the given signal name.
456    pub fn new(name: impl Into<String>) -> Self {
457        Self {
458            name: name.into(),
459            _wf: PhantomData,
460        }
461    }
462}
463
464impl<W: WorkflowDefinition> SignalDefinition for UntypedSignal<W> {
465    type Workflow = W;
466    type Input = RawValue;
467
468    fn name(&self) -> &str {
469        &self.name
470    }
471}
472
473/// Marker type for sending untyped queries. Stores the query name for runtime lookup.
474///
475/// Use with `handle.query(UntypedQuery::new("query_name"), raw_payload)`.
476pub struct UntypedQuery<W> {
477    name: String,
478    _wf: PhantomData<W>,
479}
480
481impl<W> UntypedQuery<W> {
482    /// Create a new `UntypedQuery` with the given query name.
483    pub fn new(name: impl Into<String>) -> Self {
484        Self {
485            name: name.into(),
486            _wf: PhantomData,
487        }
488    }
489}
490
491impl<W: WorkflowDefinition> QueryDefinition for UntypedQuery<W> {
492    type Workflow = W;
493    type Input = RawValue;
494    type Output = RawValue;
495
496    fn name(&self) -> &str {
497        &self.name
498    }
499}
500
501/// Marker type for sending untyped updates. Stores the update name for runtime lookup.
502///
503/// Use with `handle.update(UntypedUpdate::new("update_name"), raw_payload)`.
504pub struct UntypedUpdate<W> {
505    name: String,
506    _wf: PhantomData<W>,
507}
508
509impl<W> UntypedUpdate<W> {
510    /// Create a new `UntypedUpdate` with the given update name.
511    pub fn new(name: impl Into<String>) -> Self {
512        Self {
513            name: name.into(),
514            _wf: PhantomData,
515        }
516    }
517}
518
519impl<W: WorkflowDefinition> UpdateDefinition for UntypedUpdate<W> {
520    type Workflow = W;
521    type Input = RawValue;
522    type Output = RawValue;
523
524    fn name(&self) -> &str {
525        &self.name
526    }
527}
528
529impl<CT, W> WorkflowHandle<CT, W>
530where
531    CT: WorkflowService + Clone,
532    W: HasWorkflowDefinition,
533{
534    /// Create a workflow handle from a client and identifying information.
535    pub fn new(client: CT, info: WorkflowExecutionInfo) -> Self {
536        Self {
537            client,
538            info,
539            _wf_type: PhantomData::<W>,
540        }
541    }
542
543    /// Get the workflow execution info
544    pub fn info(&self) -> &WorkflowExecutionInfo {
545        &self.info
546    }
547
548    /// Get the client attached to this handle
549    pub fn client(&self) -> &CT {
550        &self.client
551    }
552
553    /// Await the result of the workflow execution
554    pub async fn get_result(
555        &self,
556        opts: WorkflowGetResultOptions,
557    ) -> Result<W::Output, WorkflowGetResultError>
558    where
559        CT: WorkflowService + NamespacedClient + Clone,
560    {
561        let raw = self.get_result_raw(opts).await?;
562        match raw {
563            WorkflowExecutionResult::Succeeded(v) => Ok(v),
564            WorkflowExecutionResult::Failed(f) => Err(WorkflowGetResultError::Failed(Box::new(f))),
565            WorkflowExecutionResult::Cancelled { details } => {
566                Err(WorkflowGetResultError::Cancelled { details })
567            }
568            WorkflowExecutionResult::Terminated { details } => {
569                Err(WorkflowGetResultError::Terminated { details })
570            }
571            WorkflowExecutionResult::TimedOut => Err(WorkflowGetResultError::TimedOut),
572            WorkflowExecutionResult::ContinuedAsNew => Err(WorkflowGetResultError::ContinuedAsNew),
573        }
574    }
575
576    /// Await the result of the workflow execution, returning the full
577    /// [`WorkflowExecutionResult`] enum for callers that need to inspect non-success outcomes
578    /// directly.
579    async fn get_result_raw(
580        &self,
581        opts: WorkflowGetResultOptions,
582    ) -> Result<WorkflowExecutionResult<W::Output>, WorkflowInteractionError>
583    where
584        CT: WorkflowService + NamespacedClient + Clone,
585    {
586        let mut run_id = self.info.run_id.clone().unwrap_or_default();
587        let fetch_opts = WorkflowFetchHistoryOptions::builder()
588            .skip_archival(true)
589            .wait_new_event(true)
590            .event_filter_type(HistoryEventFilterType::CloseEvent)
591            .rpc_options(opts.rpc_options.clone())
592            .build();
593
594        loop {
595            let history = self.fetch_history_for_run(&run_id, &fetch_opts).await?;
596            let mut events = history.into_events();
597
598            if events.is_empty() {
599                continue;
600            }
601
602            let event_attrs = events.pop().and_then(|ev| ev.attributes);
603
604            macro_rules! follow {
605                ($attrs:ident) => {
606                    if opts.follow_runs && $attrs.new_execution_run_id != "" {
607                        run_id = $attrs.new_execution_run_id;
608                        continue;
609                    }
610                };
611            }
612
613            let dc = self.client.data_converter();
614
615            break match event_attrs {
616                Some(Attributes::WorkflowExecutionCompletedEventAttributes(attrs)) => {
617                    follow!(attrs);
618                    let payload = attrs
619                        .result
620                        .and_then(|p| p.payloads.into_iter().next())
621                        .unwrap_or_default();
622                    let result: W::Output = dc
623                        .from_payload(&SerializationContextData::Workflow, payload)
624                        .await?;
625                    Ok(WorkflowExecutionResult::Succeeded(result))
626                }
627                Some(Attributes::WorkflowExecutionFailedEventAttributes(attrs)) => {
628                    follow!(attrs);
629                    let mut failure = attrs.failure.unwrap_or_default();
630                    decode_payloads(
631                        &mut failure,
632                        dc.codec(),
633                        &SerializationContextData::Workflow,
634                    )
635                    .await?;
636                    let error = dc.failure_converter().to_error(
637                        failure,
638                        dc.payload_converter(),
639                        &SerializationContextData::Workflow,
640                    )?;
641                    Ok(WorkflowExecutionResult::Failed(error))
642                }
643                Some(Attributes::WorkflowExecutionCanceledEventAttributes(attrs)) => {
644                    Ok(WorkflowExecutionResult::Cancelled {
645                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
646                            .await?,
647                    })
648                }
649                Some(Attributes::WorkflowExecutionTimedOutEventAttributes(attrs)) => {
650                    follow!(attrs);
651                    Ok(WorkflowExecutionResult::TimedOut)
652                }
653                Some(Attributes::WorkflowExecutionTerminatedEventAttributes(attrs)) => {
654                    Ok(WorkflowExecutionResult::Terminated {
655                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
656                            .await?,
657                    })
658                }
659                Some(Attributes::WorkflowExecutionContinuedAsNewEventAttributes(attrs)) => {
660                    if opts.follow_runs {
661                        if !attrs.new_execution_run_id.is_empty() {
662                            run_id = attrs.new_execution_run_id;
663                            continue;
664                        } else {
665                            return Err(WorkflowInteractionError::Other(
666                                "New execution run id was empty in continue as new event!".into(),
667                            ));
668                        }
669                    } else {
670                        Ok(WorkflowExecutionResult::ContinuedAsNew)
671                    }
672                }
673                o => Err(WorkflowInteractionError::Other(
674                    format!(
675                        "Server returned an event that didn't match the CloseEvent filter. \
676                         This is either a server bug or a new event the SDK does not understand. \
677                         Event details: {o:?}"
678                    )
679                    .into(),
680                )),
681            };
682        }
683    }
684
685    /// Send a signal to the workflow
686    pub async fn signal<S>(
687        &self,
688        signal: S,
689        input: S::Input,
690        opts: WorkflowSignalOptions,
691    ) -> Result<(), WorkflowInteractionError>
692    where
693        CT: WorkflowService + NamespacedClient + Clone,
694        S: SignalDefinition<Workflow = W::Run>,
695        S::Input: Send,
696    {
697        interceptors::call_signal_workflow(
698            self.client.client_interceptors(),
699            SignalWorkflowInput::new(
700                self.info.workflow_id.clone(),
701                self.info.run_id.clone().unwrap_or_default(),
702                signal.name().to_string(),
703                input,
704                opts,
705            ),
706            Next::new({
707                let mut client = self.client.clone();
708                move |input: SignalWorkflowInput| -> BoxFuture<
709                    '_,
710                    Result<(), WorkflowInteractionError>,
711                > {
712                    Box::pin(async move {
713                        let (workflow_id, run_id, signal_name, args, options) =
714                            input.into_parts();
715                        let data_converter = client.data_converter().clone();
716                        let unencoded_payloads = {
717                            let payload_converter = data_converter.payload_converter();
718                            let context = SerializationContext {
719                                data: &SerializationContextData::Workflow,
720                                converter: payload_converter,
721                            };
722                            args.serialize_payloads(&context)
723                        };
724                        drop(args);
725                        let payloads = data_converter
726                            .codec()
727                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
728                            .await?;
729                        let mut request = SignalWorkflowExecutionRequest {
730                            namespace: client.namespace(),
731                            workflow_execution: Some(ProtoWorkflowExecution {
732                                workflow_id,
733                                run_id,
734                            }),
735                            signal_name,
736                            input: Some(Payloads { payloads }),
737                            identity: client.identity(),
738                            request_id: options
739                                .request_id
740                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
741                            header: options.header,
742                            ..Default::default()
743                        }
744                        .into_request();
745                        options.rpc_options.apply_to(&mut request);
746                        WorkflowService::signal_workflow_execution(&mut client, request)
747                            .await
748                            .map_err(WorkflowInteractionError::from_status)?;
749                        Ok(())
750                    })
751                }
752            }),
753        )
754        .await
755    }
756
757    /// Query the workflow
758    pub async fn query<Q>(
759        &self,
760        query: Q,
761        input: Q::Input,
762        opts: WorkflowQueryOptions,
763    ) -> Result<Q::Output, WorkflowQueryError>
764    where
765        CT: WorkflowService + NamespacedClient + Clone,
766        Q: QueryDefinition<Workflow = W::Run>,
767        Q::Input: Send,
768    {
769        let output = interceptors::call_query_workflow(
770            self.client.client_interceptors(),
771            QueryWorkflowInput::new(
772                self.info.workflow_id.clone(),
773                self.info.run_id.clone().unwrap_or_default(),
774                query.name().to_string(),
775                input,
776                opts,
777            ),
778            Next::new({
779                let mut client = self.client.clone();
780                move |input: QueryWorkflowInput| -> BoxFuture<
781                    '_,
782                    Result<QueryWorkflowOutput, WorkflowQueryError>,
783                > {
784                    Box::pin(async move {
785                        let (workflow_id, run_id, query_name, args, options) = input.into_parts();
786                        let data_converter = client.data_converter().clone();
787                        let unencoded_payloads = {
788                            let payload_converter = data_converter.payload_converter();
789                            let context = SerializationContext {
790                                data: &SerializationContextData::Workflow,
791                                converter: payload_converter,
792                            };
793                            args.serialize_payloads(&context)
794                        };
795                        drop(args);
796                        let payloads = data_converter
797                            .codec()
798                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
799                            .await?;
800                        let mut request = QueryWorkflowRequest {
801                            namespace: client.namespace(),
802                            execution: Some(ProtoWorkflowExecution {
803                                workflow_id,
804                                run_id,
805                            }),
806                            query: Some(WorkflowQuery {
807                                query_type: query_name,
808                                query_args: Some(Payloads { payloads }),
809                                header: options.header,
810                            }),
811                            query_reject_condition: options
812                                .reject_condition
813                                .map(|condition| condition as i32)
814                                .unwrap_or(1),
815                        }
816                        .into_request();
817                        options.rpc_options.apply_to(&mut request);
818                        let response = client
819                            .query_workflow(request)
820                            .await
821                            .map_err(WorkflowQueryError::from_status)?
822                            .into_inner();
823                        Ok(QueryWorkflowOutput::new(response))
824                    })
825                }
826            }),
827        )
828        .await?;
829        let response = output.response;
830
831        if let Some(rejected) = response.query_rejected {
832            return Err(WorkflowQueryError::Rejected {
833                status: (rejected.status != 0)
834                    .then(|| WorkflowExecutionStatus::from_raw(rejected.status)),
835            });
836        }
837
838        let result_payloads = response
839            .query_result
840            .map(|p| p.payloads)
841            .unwrap_or_default();
842
843        self.client
844            .data_converter()
845            .from_payloads(&SerializationContextData::Workflow, result_payloads)
846            .await
847            .map_err(WorkflowQueryError::from)
848    }
849
850    /// Send an update to the workflow and wait for it to complete, returning the result.
851    pub async fn execute_update<U>(
852        &self,
853        update: U,
854        input: U::Input,
855        options: WorkflowExecuteUpdateOptions,
856    ) -> Result<U::Output, WorkflowUpdateError>
857    where
858        CT: WorkflowService + NamespacedClient + Clone,
859        U: UpdateDefinition<Workflow = W::Run>,
860        U::Input: Send,
861        U::Output: 'static,
862    {
863        let rpc_options = options.rpc_options.clone();
864        let handle = self
865            .start_update(
866                update,
867                input,
868                WorkflowStartUpdateOptions::builder()
869                    .maybe_update_id(options.update_id)
870                    .maybe_header(options.header)
871                    .rpc_options(rpc_options.clone())
872                    .build(),
873            )
874            .await?;
875        handle.get_result(rpc_options).await
876    }
877
878    /// Start an update and return a handle without waiting for completion.
879    /// Use `execute_update()` if you want to wait for the result immediately.
880    pub async fn start_update<U>(
881        &self,
882        update: U,
883        input: U::Input,
884        options: WorkflowStartUpdateOptions,
885    ) -> Result<WorkflowUpdateHandle<CT, U::Output>, WorkflowUpdateError>
886    where
887        CT: WorkflowService + NamespacedClient + Clone,
888        U: UpdateDefinition<Workflow = W::Run>,
889        U::Input: Send,
890    {
891        let output = interceptors::call_start_workflow_update(
892            self.client.client_interceptors(),
893            StartWorkflowUpdateInput::new(
894                self.info().workflow_id.clone(),
895                self.info().run_id.clone().unwrap_or_default(),
896                update.name().to_string(),
897                input,
898                options,
899            ),
900            Next::new({
901                let mut client = self.client.clone();
902                move |input: StartWorkflowUpdateInput| -> BoxFuture<
903                    '_,
904                    Result<StartWorkflowUpdateOutput, WorkflowUpdateError>,
905                > {
906                    Box::pin(async move {
907                        let (workflow_id, run_id, update_name, args, options) = input.into_parts();
908                        let data_converter = client.data_converter().clone();
909                        let unencoded_payloads = {
910                            let payload_converter = data_converter.payload_converter();
911                            let context = SerializationContext {
912                                data: &SerializationContextData::Workflow,
913                                converter: payload_converter,
914                            };
915                            args.serialize_payloads(&context)
916                        };
917                        drop(args);
918                        let payloads = data_converter
919                            .codec()
920                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
921                            .await?;
922                        let update_id = options
923                            .update_id
924                            .unwrap_or_else(|| Uuid::new_v4().to_string());
925                        let mut request = UpdateWorkflowExecutionRequest {
926                            namespace: client.namespace(),
927                            workflow_execution: Some(ProtoWorkflowExecution {
928                                workflow_id: workflow_id.clone(),
929                                run_id,
930                            }),
931                            wait_policy: Some(WaitPolicy {
932                                lifecycle_stage:
933                                    UpdateWorkflowExecutionLifecycleStage::Accepted.into(),
934                            }),
935                            request: Some(update::v1::Request {
936                                meta: Some(update::v1::Meta {
937                                    update_id: update_id.clone(),
938                                    identity: client.identity(),
939                                }),
940                                input: Some(update::v1::Input {
941                                    header: options.header,
942                                    name: update_name,
943                                    args: Some(Payloads { payloads }),
944                                }),
945                                ..Default::default()
946                            }),
947                            ..Default::default()
948                        }
949                        .into_request();
950                        options.rpc_options.apply_to(&mut request);
951                        let response = WorkflowService::update_workflow_execution(
952                            &mut client,
953                            request,
954                        )
955                        .await
956                        .map_err(WorkflowUpdateError::from_status)?
957                        .into_inner();
958                        let run_id = response
959                            .update_ref
960                            .as_ref()
961                            .and_then(|reference| reference.workflow_execution.as_ref())
962                            .map(|execution| execution.run_id.clone())
963                            .filter(|run_id| !run_id.is_empty());
964                        Ok(StartWorkflowUpdateOutput::new(
965                            update_id,
966                            workflow_id,
967                            run_id,
968                            response.outcome,
969                        ))
970                    })
971                }
972            }),
973        )
974        .await?;
975
976        Ok(WorkflowUpdateHandle {
977            client: self.client.clone(),
978            update_id: output.update_id,
979            workflow_id: output.workflow_id,
980            run_id: output.run_id.or_else(|| self.info().run_id.clone()),
981            known_outcome: output.known_outcome,
982            _output: PhantomData,
983        })
984    }
985
986    /// Request cancellation of this workflow.
987    pub async fn cancel(&self, opts: WorkflowCancelOptions) -> Result<(), WorkflowInteractionError>
988    where
989        CT: NamespacedClient,
990    {
991        interceptors::call_cancel_workflow(
992            self.client.client_interceptors(),
993            CancelWorkflowInput {
994                workflow_id: self.info.workflow_id.clone(),
995                run_id: self.info.run_id.clone().unwrap_or_default(),
996                first_execution_run_id: self
997                    .info
998                    .first_execution_run_id
999                    .clone()
1000                    .unwrap_or_default(),
1001                options: opts,
1002            },
1003            Next::new({
1004                let mut client = self.client.clone();
1005                move |input: CancelWorkflowInput| -> BoxFuture<
1006                    '_,
1007                    Result<(), WorkflowInteractionError>,
1008                > {
1009                    Box::pin(async move {
1010                        let mut request = RequestCancelWorkflowExecutionRequest {
1011                            namespace: client.namespace(),
1012                            workflow_execution: Some(ProtoWorkflowExecution {
1013                                workflow_id: input.workflow_id,
1014                                run_id: input.run_id,
1015                            }),
1016                            identity: client.identity(),
1017                            request_id: input
1018                                .options
1019                                .request_id
1020                                .clone()
1021                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
1022                            first_execution_run_id: input.first_execution_run_id,
1023                            reason: input.options.reason.clone(),
1024                            links: vec![],
1025                        }
1026                        .into_request();
1027                        input.options.rpc_options.apply_to(&mut request);
1028                        WorkflowService::request_cancel_workflow_execution(&mut client, request)
1029                            .await
1030                            .map_err(WorkflowInteractionError::from_status)?;
1031                        Ok(())
1032                    })
1033                }
1034            }),
1035        )
1036        .await
1037    }
1038
1039    /// Terminate this workflow.
1040    pub async fn terminate(
1041        &self,
1042        opts: WorkflowTerminateOptions,
1043    ) -> Result<(), WorkflowInteractionError>
1044    where
1045        CT: NamespacedClient,
1046    {
1047        interceptors::call_terminate_workflow(
1048            self.client.client_interceptors(),
1049            TerminateWorkflowInput {
1050                workflow_id: self.info.workflow_id.clone(),
1051                run_id: self.info.run_id.clone().unwrap_or_default(),
1052                first_execution_run_id: self
1053                    .info
1054                    .first_execution_run_id
1055                    .clone()
1056                    .unwrap_or_default(),
1057                options: opts,
1058            },
1059            Next::new({
1060                let mut client = self.client.clone();
1061                move |input: TerminateWorkflowInput| -> BoxFuture<
1062                    '_,
1063                    Result<(), WorkflowInteractionError>,
1064                > {
1065                    Box::pin(async move {
1066                        let mut request = TerminateWorkflowExecutionRequest {
1067                            namespace: client.namespace(),
1068                            workflow_execution: Some(ProtoWorkflowExecution {
1069                                workflow_id: input.workflow_id,
1070                                run_id: input.run_id,
1071                            }),
1072                            reason: input.options.reason.clone(),
1073                            details: input.options.details.clone(),
1074                            identity: client.identity(),
1075                            first_execution_run_id: input.first_execution_run_id,
1076                            links: vec![],
1077                        }
1078                        .into_request();
1079                        input.options.rpc_options.apply_to(&mut request);
1080                        WorkflowService::terminate_workflow_execution(&mut client, request)
1081                            .await
1082                            .map_err(WorkflowInteractionError::from_status)?;
1083                        Ok(())
1084                    })
1085                }
1086            }),
1087        )
1088        .await
1089    }
1090
1091    /// Get workflow execution description/metadata.
1092    pub async fn describe(
1093        &self,
1094        opts: WorkflowDescribeOptions,
1095    ) -> Result<WorkflowExecutionDescription, WorkflowInteractionError>
1096    where
1097        CT: NamespacedClient,
1098    {
1099        let output = interceptors::call_describe_workflow(
1100            self.client.client_interceptors(),
1101            DescribeWorkflowInput {
1102                workflow_id: self.info.workflow_id.clone(),
1103                run_id: self.info.run_id.clone().unwrap_or_default(),
1104                options: opts,
1105            },
1106            Next::new({
1107                let mut client = self.client.clone();
1108                move |input: DescribeWorkflowInput| -> BoxFuture<
1109                        '_,
1110                        Result<DescribeWorkflowOutput, WorkflowInteractionError>,
1111                    > {
1112                        Box::pin(async move {
1113                            let mut request = DescribeWorkflowExecutionRequest {
1114                                namespace: client.namespace(),
1115                                execution: Some(ProtoWorkflowExecution {
1116                                    workflow_id: input.workflow_id,
1117                                    run_id: input.run_id,
1118                                }),
1119                            }
1120                            .into_request();
1121                            input.options.rpc_options.apply_to(&mut request);
1122                            let response =
1123                                WorkflowService::describe_workflow_execution(&mut client, request)
1124                                    .await
1125                                    .map_err(WorkflowInteractionError::from_status)?
1126                                    .into_inner();
1127                            Ok(DescribeWorkflowOutput::new(response))
1128                        })
1129                    }
1130            }),
1131        )
1132        .await?;
1133        WorkflowExecutionDescription::new(output.response, self.client.data_converter())
1134            .await
1135            .map_err(WorkflowInteractionError::from)
1136    }
1137    /// Fetch workflow execution history.
1138    pub async fn fetch_history(
1139        &self,
1140        opts: WorkflowFetchHistoryOptions,
1141    ) -> Result<WorkflowHistory, WorkflowInteractionError>
1142    where
1143        CT: NamespacedClient,
1144    {
1145        let run_id = self.info.run_id.clone().unwrap_or_default();
1146        self.fetch_history_for_run(&run_id, &opts).await
1147    }
1148
1149    /// Fetch history for a specific run_id, handling pagination.
1150    async fn fetch_history_for_run(
1151        &self,
1152        run_id: &str,
1153        opts: &WorkflowFetchHistoryOptions,
1154    ) -> Result<WorkflowHistory, WorkflowInteractionError>
1155    where
1156        CT: NamespacedClient,
1157    {
1158        let mut all_events = Vec::new();
1159        let mut next_page_token = vec![];
1160
1161        loop {
1162            let output = interceptors::call_fetch_workflow_history_page(
1163                self.client.client_interceptors(),
1164                FetchWorkflowHistoryPageInput {
1165                    workflow_id: self.info.workflow_id.clone(),
1166                    run_id: run_id.to_string(),
1167                    next_page_token,
1168                    options: opts.clone(),
1169                },
1170                Next::new({
1171                    let mut client = self.client.clone();
1172                    move |input: FetchWorkflowHistoryPageInput| -> BoxFuture<
1173                        '_,
1174                        Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>,
1175                    > {
1176                        Box::pin(async move {
1177                            let mut request = GetWorkflowExecutionHistoryRequest {
1178                                namespace: client.namespace(),
1179                                execution: Some(ProtoWorkflowExecution {
1180                                    workflow_id: input.workflow_id,
1181                                    run_id: input.run_id,
1182                                }),
1183                                next_page_token: input.next_page_token,
1184                                skip_archival: input.options.skip_archival,
1185                                wait_new_event: input.options.wait_new_event,
1186                                history_event_filter_type: input.options.event_filter_type as i32,
1187                                ..Default::default()
1188                            }
1189                            .into_request();
1190                            input.options.rpc_options.apply_to(&mut request);
1191                            let response = WorkflowService::get_workflow_execution_history(
1192                                &mut client,
1193                                request,
1194                            )
1195                            .await
1196                            .map_err(WorkflowInteractionError::from_status)?
1197                            .into_inner();
1198                            Ok(FetchWorkflowHistoryPageOutput::new(
1199                                response
1200                                    .history
1201                                    .map(|history| history.events)
1202                                    .unwrap_or_default(),
1203                                response.next_page_token,
1204                            ))
1205                        })
1206                    }
1207                }),
1208            )
1209            .await?;
1210
1211            all_events.extend(output.events);
1212            if output.next_page_token.is_empty() {
1213                break;
1214            }
1215            next_page_token = output.next_page_token;
1216        }
1217
1218        Ok(WorkflowHistory::new(
1219            all_events,
1220            Some(self.info.workflow_id.clone()),
1221        ))
1222    }
1223}
1224
1225/// Handle to a workflow update that has been started but may not be complete.
1226///
1227/// Use [`get_result`](Self::get_result) to wait for the update to complete and retrieve its result.
1228pub struct WorkflowUpdateHandle<CT, T> {
1229    client: CT,
1230    update_id: String,
1231    workflow_id: String,
1232    run_id: Option<String>,
1233    /// If the update was started with `Completed` wait stage, the outcome is already available.
1234    known_outcome: Option<update::v1::Outcome>,
1235    _output: PhantomData<T>,
1236}
1237
1238impl<CT, T> WorkflowUpdateHandle<CT, T> {
1239    /// Get the update ID.
1240    pub fn id(&self) -> &str {
1241        &self.update_id
1242    }
1243
1244    /// Get the workflow ID.
1245    pub fn workflow_id(&self) -> &str {
1246        &self.workflow_id
1247    }
1248
1249    /// Get the workflow run ID, if available.
1250    pub fn workflow_run_id(&self) -> Option<&str> {
1251        self.run_id.as_deref()
1252    }
1253}
1254
1255impl<CT, T: 'static> WorkflowUpdateHandle<CT, T>
1256where
1257    CT: WorkflowService + NamespacedClient + Clone,
1258{
1259    /// Wait for the update to complete and return the result using the provided RPC controls.
1260    pub async fn get_result(&self, rpc_options: RpcOptions) -> Result<T, WorkflowUpdateError>
1261    where
1262        T: temporalio_common::data_converters::TemporalDeserializable,
1263    {
1264        let output = interceptors::call_poll_workflow_update(
1265            self.client.client_interceptors(),
1266            PollWorkflowUpdateInput {
1267                update_id: self.update_id.clone(),
1268                workflow_id: self.workflow_id.clone(),
1269                run_id: self.run_id.clone().unwrap_or_default(),
1270                rpc_options,
1271            },
1272            Next::new({
1273                let mut client = self.client.clone();
1274                let known_outcome = self.known_outcome.clone();
1275                move |input: PollWorkflowUpdateInput| -> BoxFuture<
1276                    '_,
1277                    Result<PollWorkflowUpdateOutput, WorkflowUpdateError>,
1278                > {
1279                    Box::pin(async move {
1280                        if let Some(outcome) = known_outcome {
1281                            return Ok(PollWorkflowUpdateOutput::new(outcome));
1282                        }
1283                        // The server's internal long-poll timeout (~60s) may expire before the update
1284                        // completes, returning a response with outcome: None. Keep polling until we
1285                        // get an actual outcome.
1286                        loop {
1287                            let mut request = PollWorkflowExecutionUpdateRequest {
1288                                namespace: client.namespace(),
1289                                update_ref: Some(update::v1::UpdateRef {
1290                                    workflow_execution: Some(ProtoWorkflowExecution {
1291                                        workflow_id: input.workflow_id.clone(),
1292                                        run_id: input.run_id.clone(),
1293                                    }),
1294                                    update_id: input.update_id.clone(),
1295                                }),
1296                                identity: client.identity(),
1297                                wait_policy: Some(WaitPolicy {
1298                                    lifecycle_stage:
1299                                        UpdateWorkflowExecutionLifecycleStage::Completed.into(),
1300                                }),
1301                            }
1302                            .into_request();
1303                            input.rpc_options.apply_to(&mut request);
1304                            let response = WorkflowService::poll_workflow_execution_update(
1305                                &mut client,
1306                                request,
1307                            )
1308                            .await
1309                            .map_err(WorkflowUpdateError::from_status)?
1310                            .into_inner();
1311                            if let Some(outcome) = response.outcome {
1312                                return Ok(PollWorkflowUpdateOutput::new(outcome));
1313                            }
1314                        }
1315                    })
1316                }
1317            }),
1318        )
1319        .await?;
1320        let outcome = output.outcome;
1321
1322        match outcome.value {
1323            Some(update::v1::outcome::Value::Success(success)) => self
1324                .client
1325                .data_converter()
1326                .from_payloads(&SerializationContextData::Workflow, success.payloads)
1327                .await
1328                .map_err(WorkflowUpdateError::from),
1329            Some(update::v1::outcome::Value::Failure(failure)) => {
1330                Err(WorkflowUpdateError::Failed(Box::new(failure)))
1331            }
1332            None => Err(WorkflowUpdateError::Other(
1333                "Update returned no outcome value".into(),
1334            )),
1335        }
1336    }
1337}
1338
1339#[cfg(test)]
1340mod tests {
1341    use super::*;
1342    use crate::test_helpers::XorCodec;
1343    use std::collections::HashMap;
1344    use temporalio_common::{
1345        data_converters::DefaultFailureConverter,
1346        protos::temporal::api::{
1347            common::v1::{Memo, SearchAttributes},
1348            enums::v1::WorkflowExecutionStatus as ProtoWorkflowExecutionStatus,
1349            history::v1::WorkflowExecutionStartedEventAttributes,
1350            sdk::v1::UserMetadata,
1351            workflow::v1::WorkflowExecutionConfig,
1352        },
1353    };
1354
1355    #[test]
1356    fn workflow_history_workflow_id_roundtrips() {
1357        let event = HistoryEvent {
1358            event_id: 1,
1359            attributes: Some(Attributes::WorkflowExecutionStartedEventAttributes(
1360                WorkflowExecutionStartedEventAttributes {
1361                    workflow_id: "workflow-id".to_owned(),
1362                    original_execution_run_id: "run-id".to_owned(),
1363                    ..Default::default()
1364                },
1365            )),
1366            ..Default::default()
1367        };
1368        let history = WorkflowHistory::new(vec![event], None);
1369
1370        let bytes = history.to_json().unwrap();
1371
1372        let decoded = WorkflowHistory::from_json(&bytes).unwrap();
1373        assert_eq!(decoded.workflow_id(), Some("workflow-id"));
1374    }
1375
1376    #[tokio::test]
1377    async fn workflow_result_details_support_typed_decoding() {
1378        let converter = DataConverter::new(
1379            PayloadConverter::default(),
1380            DefaultFailureConverter,
1381            XorCodec,
1382        );
1383        let payloads = converter
1384            .to_payloads(
1385                &SerializationContextData::Workflow,
1386                &"workflow-result-details".to_owned(),
1387            )
1388            .await
1389            .unwrap();
1390        let details = WorkflowResultDetails::new(payloads.clone(), &converter)
1391            .await
1392            .unwrap();
1393
1394        assert_ne!(details.raw(), payloads);
1395        let decoded_payloads = details.raw().to_vec();
1396        assert_eq!(
1397            details.deserialize::<String>().unwrap(),
1398            "workflow-result-details"
1399        );
1400        assert_eq!(details.into_raw().payloads, decoded_payloads);
1401    }
1402
1403    #[tokio::test]
1404    async fn workflow_result_detail_conversion_errors_are_reported() {
1405        let details =
1406            WorkflowResultDetails::new(vec![Payload::default()], &DataConverter::default())
1407                .await
1408                .unwrap();
1409
1410        assert_eq!(details.raw(), &[Payload::default()]);
1411        assert!(details.deserialize::<String>().is_err());
1412    }
1413
1414    #[tokio::test]
1415    async fn workflow_description_memo_uses_saved_converter() {
1416        let converter = DataConverter::new(
1417            PayloadConverter::default(),
1418            DefaultFailureConverter,
1419            XorCodec,
1420        );
1421        let encoded = converter
1422            .to_payload(
1423                &SerializationContextData::Workflow,
1424                &"memo-value".to_owned(),
1425            )
1426            .await
1427            .unwrap();
1428        let description = WorkflowExecutionDescription::new(
1429            DescribeWorkflowExecutionResponse {
1430                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1431                    memo: Some(Memo {
1432                        fields: HashMap::from([("memo-key".to_owned(), encoded)]),
1433                    }),
1434                    ..Default::default()
1435                }),
1436                ..Default::default()
1437            },
1438            &converter,
1439        )
1440        .await
1441        .unwrap();
1442        let memo = description.memo();
1443
1444        assert_eq!(
1445            memo.get::<String>("memo-key").unwrap(),
1446            Some("memo-value".to_owned())
1447        );
1448    }
1449
1450    #[tokio::test]
1451    async fn workflow_description_accessors_expose_decoded_fields() {
1452        let converter = DataConverter::default();
1453        let memo_payload = converter
1454            .to_payload(&SerializationContextData::Workflow, &"memo-value")
1455            .await
1456            .unwrap();
1457        let search_attr_payload = converter
1458            .to_payload(&SerializationContextData::Workflow, &"search-value")
1459            .await
1460            .unwrap();
1461        let summary_payload = converter
1462            .to_payload(&SerializationContextData::Workflow, &"workflow summary")
1463            .await
1464            .unwrap();
1465        let details_payload = converter
1466            .to_payload(&SerializationContextData::Workflow, &"workflow details")
1467            .await
1468            .unwrap();
1469        let description = WorkflowExecutionDescription::new(
1470            DescribeWorkflowExecutionResponse {
1471                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1472                    execution: Some(ProtoWorkflowExecution {
1473                        workflow_id: "wf-id".to_string(),
1474                        run_id: "run-id".to_string(),
1475                    }),
1476                    r#type: Some(
1477                        temporalio_common::protos::temporal::api::common::v1::WorkflowType {
1478                            name: "wf-type".to_string(),
1479                        },
1480                    ),
1481                    status: ProtoWorkflowExecutionStatus::Completed as i32,
1482                    task_queue: "task-queue".to_string(),
1483                    history_length: 42,
1484                    memo: Some(Memo {
1485                        fields: HashMap::from([("memo-key".to_string(), memo_payload.clone())]),
1486                    }),
1487                    parent_execution: Some(ProtoWorkflowExecution {
1488                        workflow_id: "parent-id".to_string(),
1489                        run_id: "parent-run-id".to_string(),
1490                    }),
1491                    search_attributes: Some(SearchAttributes {
1492                        indexed_fields: HashMap::from([(
1493                            "CustomKeywordField".to_string(),
1494                            search_attr_payload.clone(),
1495                        )]),
1496                    }),
1497                    ..Default::default()
1498                }),
1499                execution_config: Some(WorkflowExecutionConfig {
1500                    user_metadata: Some(UserMetadata {
1501                        summary: Some(summary_payload),
1502                        details: Some(details_payload),
1503                    }),
1504                    ..Default::default()
1505                }),
1506                ..Default::default()
1507            },
1508            &converter,
1509        )
1510        .await
1511        .unwrap();
1512
1513        assert_eq!(description.id(), "wf-id");
1514        assert_eq!(description.run_id(), "run-id");
1515        assert_eq!(description.workflow_type(), "wf-type");
1516        assert_eq!(description.status(), WorkflowExecutionStatus::Completed);
1517        let mut unknown_status_description = description.clone();
1518        unknown_status_description
1519            .raw_description
1520            .workflow_execution_info
1521            .as_mut()
1522            .unwrap()
1523            .status = 123_456;
1524        assert_eq!(
1525            unknown_status_description.status(),
1526            WorkflowExecutionStatus::Unknown
1527        );
1528        assert_eq!(description.task_queue(), "task-queue");
1529        assert_eq!(description.history_length(), 42);
1530        assert_eq!(description.parent_id(), Some("parent-id"));
1531        assert_eq!(description.parent_run_id(), Some("parent-run-id"));
1532        let memo = description.memo();
1533        assert_eq!(memo.raw_value("memo-key"), Some(&memo_payload));
1534        assert_eq!(
1535            memo.get::<String>("memo-key").unwrap(),
1536            Some("memo-value".to_owned())
1537        );
1538        let search_attributes = description.search_attributes();
1539        assert_eq!(
1540            search_attributes.raw_payload("CustomKeywordField"),
1541            Some(&search_attr_payload)
1542        );
1543        assert_eq!(description.static_summary(), Some("workflow summary"));
1544        assert_eq!(description.static_details(), Some("workflow details"));
1545    }
1546
1547    #[tokio::test]
1548    async fn workflow_description_rejects_negative_history_length() {
1549        let err = WorkflowExecutionDescription::new(
1550            DescribeWorkflowExecutionResponse {
1551                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1552                    history_length: -1,
1553                    ..Default::default()
1554                }),
1555                ..Default::default()
1556            },
1557            &DataConverter::default(),
1558        )
1559        .await
1560        .unwrap_err();
1561
1562        assert_eq!(
1563            err.to_string(),
1564            "Encoding error: workflow history_length must be non-negative, got -1"
1565        );
1566    }
1567}