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, WorkflowUpdateWaitStage,
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::{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}
340impl From<WorkflowHistory> for history::v1::History {
341    fn from(h: WorkflowHistory) -> Self {
342        Self { events: h.events }
343    }
344}
345
346impl WorkflowHistory {
347    fn new(events: Vec<HistoryEvent>) -> Self {
348        Self { events }
349    }
350
351    /// The history events.
352    pub fn events(&self) -> &[HistoryEvent] {
353        &self.events
354    }
355
356    /// Consume the history and return the events.
357    pub fn into_events(self) -> Vec<HistoryEvent> {
358        self.events
359    }
360}
361
362/// A workflow handle which can refer to a specific workflow run, or a chain of workflow runs with
363/// the same workflow id.
364#[derive(Clone)]
365pub struct WorkflowHandle<ClientT, W> {
366    client: ClientT,
367    info: WorkflowExecutionInfo,
368
369    _wf_type: PhantomData<W>,
370}
371
372impl<CT, W> WorkflowHandle<CT, W> {
373    /// Return the run id of the Workflow Execution pointed at by this handle, if there is one.
374    pub fn run_id(&self) -> Option<&str> {
375        self.info.run_id.as_deref()
376    }
377}
378
379/// Holds needed information to refer to a specific workflow run, or workflow execution chain
380#[derive(Debug, Clone)]
381pub struct WorkflowExecutionInfo {
382    /// Namespace the workflow lives in.
383    pub namespace: String,
384    /// The workflow's id.
385    pub workflow_id: String,
386    /// If set, target this specific run of the workflow.
387    pub run_id: Option<String>,
388    /// Run ID used for cancellation and termination to ensure they happen on a workflow starting
389    /// with this run ID. This can be set when getting a workflow handle. When starting a workflow,
390    /// this is set as the resulting run ID if no start signal was provided.
391    pub first_execution_run_id: Option<String>,
392}
393
394impl WorkflowExecutionInfo {
395    /// Bind the workflow info to a specific client, turning it into a workflow handle
396    pub fn bind_untyped<CT>(self, client: CT) -> UntypedWorkflowHandle<CT>
397    where
398        CT: WorkflowService + Clone,
399    {
400        UntypedWorkflowHandle::new(client, self)
401    }
402}
403
404/// A workflow handle to a workflow with unknown types. Uses single argument raw payloads for input
405/// and output.
406pub type UntypedWorkflowHandle<CT> = WorkflowHandle<CT, UntypedWorkflow>;
407
408/// Marker type for sending untyped signals. Stores the signal name for runtime lookup.
409///
410/// Use with `handle.signal(UntypedSignal::new("signal_name"), raw_payload)`.
411pub struct UntypedSignal<W> {
412    name: String,
413    _wf: PhantomData<W>,
414}
415
416impl<W> UntypedSignal<W> {
417    /// Create a new `UntypedSignal` with the given signal name.
418    pub fn new(name: impl Into<String>) -> Self {
419        Self {
420            name: name.into(),
421            _wf: PhantomData,
422        }
423    }
424}
425
426impl<W: WorkflowDefinition> SignalDefinition for UntypedSignal<W> {
427    type Workflow = W;
428    type Input = RawValue;
429
430    fn name(&self) -> &str {
431        &self.name
432    }
433}
434
435/// Marker type for sending untyped queries. Stores the query name for runtime lookup.
436///
437/// Use with `handle.query(UntypedQuery::new("query_name"), raw_payload)`.
438pub struct UntypedQuery<W> {
439    name: String,
440    _wf: PhantomData<W>,
441}
442
443impl<W> UntypedQuery<W> {
444    /// Create a new `UntypedQuery` with the given query name.
445    pub fn new(name: impl Into<String>) -> Self {
446        Self {
447            name: name.into(),
448            _wf: PhantomData,
449        }
450    }
451}
452
453impl<W: WorkflowDefinition> QueryDefinition for UntypedQuery<W> {
454    type Workflow = W;
455    type Input = RawValue;
456    type Output = RawValue;
457
458    fn name(&self) -> &str {
459        &self.name
460    }
461}
462
463/// Marker type for sending untyped updates. Stores the update name for runtime lookup.
464///
465/// Use with `handle.update(UntypedUpdate::new("update_name"), raw_payload)`.
466pub struct UntypedUpdate<W> {
467    name: String,
468    _wf: PhantomData<W>,
469}
470
471impl<W> UntypedUpdate<W> {
472    /// Create a new `UntypedUpdate` with the given update name.
473    pub fn new(name: impl Into<String>) -> Self {
474        Self {
475            name: name.into(),
476            _wf: PhantomData,
477        }
478    }
479}
480
481impl<W: WorkflowDefinition> UpdateDefinition for UntypedUpdate<W> {
482    type Workflow = W;
483    type Input = RawValue;
484    type Output = RawValue;
485
486    fn name(&self) -> &str {
487        &self.name
488    }
489}
490
491impl<CT, W> WorkflowHandle<CT, W>
492where
493    CT: WorkflowService + Clone,
494    W: HasWorkflowDefinition,
495{
496    /// Create a workflow handle from a client and identifying information.
497    pub fn new(client: CT, info: WorkflowExecutionInfo) -> Self {
498        Self {
499            client,
500            info,
501            _wf_type: PhantomData::<W>,
502        }
503    }
504
505    /// Get the workflow execution info
506    pub fn info(&self) -> &WorkflowExecutionInfo {
507        &self.info
508    }
509
510    /// Get the client attached to this handle
511    pub fn client(&self) -> &CT {
512        &self.client
513    }
514
515    /// Await the result of the workflow execution
516    pub async fn get_result(
517        &self,
518        opts: WorkflowGetResultOptions,
519    ) -> Result<W::Output, WorkflowGetResultError>
520    where
521        CT: WorkflowService + NamespacedClient + Clone,
522    {
523        let raw = self.get_result_raw(opts).await?;
524        match raw {
525            WorkflowExecutionResult::Succeeded(v) => Ok(v),
526            WorkflowExecutionResult::Failed(f) => Err(WorkflowGetResultError::Failed(Box::new(f))),
527            WorkflowExecutionResult::Cancelled { details } => {
528                Err(WorkflowGetResultError::Cancelled { details })
529            }
530            WorkflowExecutionResult::Terminated { details } => {
531                Err(WorkflowGetResultError::Terminated { details })
532            }
533            WorkflowExecutionResult::TimedOut => Err(WorkflowGetResultError::TimedOut),
534            WorkflowExecutionResult::ContinuedAsNew => Err(WorkflowGetResultError::ContinuedAsNew),
535        }
536    }
537
538    /// Await the result of the workflow execution, returning the full
539    /// [`WorkflowExecutionResult`] enum for callers that need to inspect non-success outcomes
540    /// directly.
541    async fn get_result_raw(
542        &self,
543        opts: WorkflowGetResultOptions,
544    ) -> Result<WorkflowExecutionResult<W::Output>, WorkflowInteractionError>
545    where
546        CT: WorkflowService + NamespacedClient + Clone,
547    {
548        let mut run_id = self.info.run_id.clone().unwrap_or_default();
549        let fetch_opts = WorkflowFetchHistoryOptions::builder()
550            .skip_archival(true)
551            .wait_new_event(true)
552            .event_filter_type(HistoryEventFilterType::CloseEvent)
553            .rpc_options(opts.rpc_options.clone())
554            .build();
555
556        loop {
557            let history = self.fetch_history_for_run(&run_id, &fetch_opts).await?;
558            let mut events = history.into_events();
559
560            if events.is_empty() {
561                continue;
562            }
563
564            let event_attrs = events.pop().and_then(|ev| ev.attributes);
565
566            macro_rules! follow {
567                ($attrs:ident) => {
568                    if opts.follow_runs && $attrs.new_execution_run_id != "" {
569                        run_id = $attrs.new_execution_run_id;
570                        continue;
571                    }
572                };
573            }
574
575            let dc = self.client.data_converter();
576
577            break match event_attrs {
578                Some(Attributes::WorkflowExecutionCompletedEventAttributes(attrs)) => {
579                    follow!(attrs);
580                    let payload = attrs
581                        .result
582                        .and_then(|p| p.payloads.into_iter().next())
583                        .unwrap_or_default();
584                    let result: W::Output = dc
585                        .from_payload(&SerializationContextData::Workflow, payload)
586                        .await?;
587                    Ok(WorkflowExecutionResult::Succeeded(result))
588                }
589                Some(Attributes::WorkflowExecutionFailedEventAttributes(attrs)) => {
590                    follow!(attrs);
591                    let mut failure = attrs.failure.unwrap_or_default();
592                    decode_payloads(
593                        &mut failure,
594                        dc.codec(),
595                        &SerializationContextData::Workflow,
596                    )
597                    .await?;
598                    let error = dc.failure_converter().to_error(
599                        failure,
600                        dc.payload_converter(),
601                        &SerializationContextData::Workflow,
602                    )?;
603                    Ok(WorkflowExecutionResult::Failed(error))
604                }
605                Some(Attributes::WorkflowExecutionCanceledEventAttributes(attrs)) => {
606                    Ok(WorkflowExecutionResult::Cancelled {
607                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
608                            .await?,
609                    })
610                }
611                Some(Attributes::WorkflowExecutionTimedOutEventAttributes(attrs)) => {
612                    follow!(attrs);
613                    Ok(WorkflowExecutionResult::TimedOut)
614                }
615                Some(Attributes::WorkflowExecutionTerminatedEventAttributes(attrs)) => {
616                    Ok(WorkflowExecutionResult::Terminated {
617                        details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
618                            .await?,
619                    })
620                }
621                Some(Attributes::WorkflowExecutionContinuedAsNewEventAttributes(attrs)) => {
622                    if opts.follow_runs {
623                        if !attrs.new_execution_run_id.is_empty() {
624                            run_id = attrs.new_execution_run_id;
625                            continue;
626                        } else {
627                            return Err(WorkflowInteractionError::Other(
628                                "New execution run id was empty in continue as new event!".into(),
629                            ));
630                        }
631                    } else {
632                        Ok(WorkflowExecutionResult::ContinuedAsNew)
633                    }
634                }
635                o => Err(WorkflowInteractionError::Other(
636                    format!(
637                        "Server returned an event that didn't match the CloseEvent filter. \
638                         This is either a server bug or a new event the SDK does not understand. \
639                         Event details: {o:?}"
640                    )
641                    .into(),
642                )),
643            };
644        }
645    }
646
647    /// Send a signal to the workflow
648    pub async fn signal<S>(
649        &self,
650        signal: S,
651        input: S::Input,
652        opts: WorkflowSignalOptions,
653    ) -> Result<(), WorkflowInteractionError>
654    where
655        CT: WorkflowService + NamespacedClient + Clone,
656        S: SignalDefinition<Workflow = W::Run>,
657        S::Input: Send,
658    {
659        interceptors::call_signal_workflow(
660            self.client.client_interceptors(),
661            SignalWorkflowInput::new(
662                self.info.workflow_id.clone(),
663                self.info.run_id.clone().unwrap_or_default(),
664                signal.name().to_string(),
665                input,
666                opts,
667            ),
668            Next::new({
669                let mut client = self.client.clone();
670                move |input: SignalWorkflowInput| -> BoxFuture<
671                    '_,
672                    Result<(), WorkflowInteractionError>,
673                > {
674                    Box::pin(async move {
675                        let (workflow_id, run_id, signal_name, args, options) =
676                            input.into_parts();
677                        let data_converter = client.data_converter().clone();
678                        let unencoded_payloads = {
679                            let payload_converter = data_converter.payload_converter();
680                            let context = SerializationContext {
681                                data: &SerializationContextData::Workflow,
682                                converter: payload_converter,
683                            };
684                            args.serialize_payloads(&context)
685                        };
686                        drop(args);
687                        let payloads = data_converter
688                            .codec()
689                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
690                            .await?;
691                        let mut request = SignalWorkflowExecutionRequest {
692                            namespace: client.namespace(),
693                            workflow_execution: Some(ProtoWorkflowExecution {
694                                workflow_id,
695                                run_id,
696                            }),
697                            signal_name,
698                            input: Some(Payloads { payloads }),
699                            identity: client.identity(),
700                            request_id: options
701                                .request_id
702                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
703                            header: options.header,
704                            ..Default::default()
705                        }
706                        .into_request();
707                        options.rpc_options.apply_to(&mut request);
708                        WorkflowService::signal_workflow_execution(&mut client, request)
709                            .await
710                            .map_err(WorkflowInteractionError::from_status)?;
711                        Ok(())
712                    })
713                }
714            }),
715        )
716        .await
717    }
718
719    /// Query the workflow
720    pub async fn query<Q>(
721        &self,
722        query: Q,
723        input: Q::Input,
724        opts: WorkflowQueryOptions,
725    ) -> Result<Q::Output, WorkflowQueryError>
726    where
727        CT: WorkflowService + NamespacedClient + Clone,
728        Q: QueryDefinition<Workflow = W::Run>,
729        Q::Input: Send,
730    {
731        let output = interceptors::call_query_workflow(
732            self.client.client_interceptors(),
733            QueryWorkflowInput::new(
734                self.info.workflow_id.clone(),
735                self.info.run_id.clone().unwrap_or_default(),
736                query.name().to_string(),
737                input,
738                opts,
739            ),
740            Next::new({
741                let mut client = self.client.clone();
742                move |input: QueryWorkflowInput| -> BoxFuture<
743                    '_,
744                    Result<QueryWorkflowOutput, WorkflowQueryError>,
745                > {
746                    Box::pin(async move {
747                        let (workflow_id, run_id, query_name, args, options) = input.into_parts();
748                        let data_converter = client.data_converter().clone();
749                        let unencoded_payloads = {
750                            let payload_converter = data_converter.payload_converter();
751                            let context = SerializationContext {
752                                data: &SerializationContextData::Workflow,
753                                converter: payload_converter,
754                            };
755                            args.serialize_payloads(&context)
756                        };
757                        drop(args);
758                        let payloads = data_converter
759                            .codec()
760                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
761                            .await?;
762                        let mut request = QueryWorkflowRequest {
763                            namespace: client.namespace(),
764                            execution: Some(ProtoWorkflowExecution {
765                                workflow_id,
766                                run_id,
767                            }),
768                            query: Some(WorkflowQuery {
769                                query_type: query_name,
770                                query_args: Some(Payloads { payloads }),
771                                header: options.header,
772                            }),
773                            query_reject_condition: options
774                                .reject_condition
775                                .map(|condition| condition as i32)
776                                .unwrap_or(1),
777                        }
778                        .into_request();
779                        options.rpc_options.apply_to(&mut request);
780                        let response = client
781                            .query_workflow(request)
782                            .await
783                            .map_err(WorkflowQueryError::from_status)?
784                            .into_inner();
785                        Ok(QueryWorkflowOutput::new(response))
786                    })
787                }
788            }),
789        )
790        .await?;
791        let response = output.response;
792
793        if let Some(rejected) = response.query_rejected {
794            return Err(WorkflowQueryError::Rejected {
795                status: (rejected.status != 0)
796                    .then(|| WorkflowExecutionStatus::from_raw(rejected.status)),
797            });
798        }
799
800        let result_payloads = response
801            .query_result
802            .map(|p| p.payloads)
803            .unwrap_or_default();
804
805        self.client
806            .data_converter()
807            .from_payloads(&SerializationContextData::Workflow, result_payloads)
808            .await
809            .map_err(WorkflowQueryError::from)
810    }
811
812    /// Send an update to the workflow and wait for it to complete, returning the result.
813    pub async fn execute_update<U>(
814        &self,
815        update: U,
816        input: U::Input,
817        options: WorkflowExecuteUpdateOptions,
818    ) -> Result<U::Output, WorkflowUpdateError>
819    where
820        CT: WorkflowService + NamespacedClient + Clone,
821        U: UpdateDefinition<Workflow = W::Run>,
822        U::Input: Send,
823        U::Output: 'static,
824    {
825        let rpc_options = options.rpc_options.clone();
826        let handle = self
827            .start_update(
828                update,
829                input,
830                WorkflowStartUpdateOptions::builder()
831                    .maybe_update_id(options.update_id)
832                    .maybe_header(options.header)
833                    .wait_for_stage(WorkflowUpdateWaitStage::Completed)
834                    .rpc_options(rpc_options.clone())
835                    .build(),
836            )
837            .await?;
838        handle.get_result(rpc_options).await
839    }
840
841    /// Start an update and return a handle without waiting for completion.
842    /// Use `execute_update()` if you want to wait for the result immediately.
843    pub async fn start_update<U>(
844        &self,
845        update: U,
846        input: U::Input,
847        options: WorkflowStartUpdateOptions,
848    ) -> Result<WorkflowUpdateHandle<CT, U::Output>, WorkflowUpdateError>
849    where
850        CT: WorkflowService + NamespacedClient + Clone,
851        U: UpdateDefinition<Workflow = W::Run>,
852        U::Input: Send,
853    {
854        let output = interceptors::call_start_workflow_update(
855            self.client.client_interceptors(),
856            StartWorkflowUpdateInput::new(
857                self.info().workflow_id.clone(),
858                self.info().run_id.clone().unwrap_or_default(),
859                update.name().to_string(),
860                input,
861                options,
862            ),
863            Next::new({
864                let mut client = self.client.clone();
865                move |input: StartWorkflowUpdateInput| -> BoxFuture<
866                    '_,
867                    Result<StartWorkflowUpdateOutput, WorkflowUpdateError>,
868                > {
869                    Box::pin(async move {
870                        let (workflow_id, run_id, update_name, args, options) = input.into_parts();
871                        let data_converter = client.data_converter().clone();
872                        let unencoded_payloads = {
873                            let payload_converter = data_converter.payload_converter();
874                            let context = SerializationContext {
875                                data: &SerializationContextData::Workflow,
876                                converter: payload_converter,
877                            };
878                            args.serialize_payloads(&context)
879                        };
880                        drop(args);
881                        let payloads = data_converter
882                            .codec()
883                            .encode(&SerializationContextData::Workflow, unencoded_payloads?)
884                            .await?;
885                        let lifecycle_stage = match options.wait_for_stage {
886                            WorkflowUpdateWaitStage::Admitted => {
887                                UpdateWorkflowExecutionLifecycleStage::Admitted
888                            }
889                            WorkflowUpdateWaitStage::Accepted => {
890                                UpdateWorkflowExecutionLifecycleStage::Accepted
891                            }
892                            WorkflowUpdateWaitStage::Completed => {
893                                UpdateWorkflowExecutionLifecycleStage::Completed
894                            }
895                        };
896                        let update_id = options
897                            .update_id
898                            .unwrap_or_else(|| Uuid::new_v4().to_string());
899                        let mut request = UpdateWorkflowExecutionRequest {
900                            namespace: client.namespace(),
901                            workflow_execution: Some(ProtoWorkflowExecution {
902                                workflow_id: workflow_id.clone(),
903                                run_id,
904                            }),
905                            wait_policy: Some(WaitPolicy {
906                                lifecycle_stage: lifecycle_stage.into(),
907                            }),
908                            request: Some(update::v1::Request {
909                                meta: Some(update::v1::Meta {
910                                    update_id: update_id.clone(),
911                                    identity: client.identity(),
912                                }),
913                                input: Some(update::v1::Input {
914                                    header: options.header,
915                                    name: update_name,
916                                    args: Some(Payloads { payloads }),
917                                }),
918                                ..Default::default()
919                            }),
920                            ..Default::default()
921                        }
922                        .into_request();
923                        options.rpc_options.apply_to(&mut request);
924                        let response = WorkflowService::update_workflow_execution(
925                            &mut client,
926                            request,
927                        )
928                        .await
929                        .map_err(WorkflowUpdateError::from_status)?
930                        .into_inner();
931                        let run_id = response
932                            .update_ref
933                            .as_ref()
934                            .and_then(|reference| reference.workflow_execution.as_ref())
935                            .map(|execution| execution.run_id.clone())
936                            .filter(|run_id| !run_id.is_empty());
937                        Ok(StartWorkflowUpdateOutput::new(
938                            update_id,
939                            workflow_id,
940                            run_id,
941                            response.outcome,
942                        ))
943                    })
944                }
945            }),
946        )
947        .await?;
948
949        Ok(WorkflowUpdateHandle {
950            client: self.client.clone(),
951            update_id: output.update_id,
952            workflow_id: output.workflow_id,
953            run_id: output.run_id.or_else(|| self.info().run_id.clone()),
954            known_outcome: output.known_outcome,
955            _output: PhantomData,
956        })
957    }
958
959    /// Request cancellation of this workflow.
960    pub async fn cancel(&self, opts: WorkflowCancelOptions) -> Result<(), WorkflowInteractionError>
961    where
962        CT: NamespacedClient,
963    {
964        interceptors::call_cancel_workflow(
965            self.client.client_interceptors(),
966            CancelWorkflowInput {
967                workflow_id: self.info.workflow_id.clone(),
968                run_id: self.info.run_id.clone().unwrap_or_default(),
969                first_execution_run_id: self
970                    .info
971                    .first_execution_run_id
972                    .clone()
973                    .unwrap_or_default(),
974                options: opts,
975            },
976            Next::new({
977                let mut client = self.client.clone();
978                move |input: CancelWorkflowInput| -> BoxFuture<
979                    '_,
980                    Result<(), WorkflowInteractionError>,
981                > {
982                    Box::pin(async move {
983                        let mut request = RequestCancelWorkflowExecutionRequest {
984                            namespace: client.namespace(),
985                            workflow_execution: Some(ProtoWorkflowExecution {
986                                workflow_id: input.workflow_id,
987                                run_id: input.run_id,
988                            }),
989                            identity: client.identity(),
990                            request_id: input
991                                .options
992                                .request_id
993                                .clone()
994                                .unwrap_or_else(|| Uuid::new_v4().to_string()),
995                            first_execution_run_id: input.first_execution_run_id,
996                            reason: input.options.reason.clone(),
997                            links: vec![],
998                        }
999                        .into_request();
1000                        input.options.rpc_options.apply_to(&mut request);
1001                        WorkflowService::request_cancel_workflow_execution(&mut client, request)
1002                            .await
1003                            .map_err(WorkflowInteractionError::from_status)?;
1004                        Ok(())
1005                    })
1006                }
1007            }),
1008        )
1009        .await
1010    }
1011
1012    /// Terminate this workflow.
1013    pub async fn terminate(
1014        &self,
1015        opts: WorkflowTerminateOptions,
1016    ) -> Result<(), WorkflowInteractionError>
1017    where
1018        CT: NamespacedClient,
1019    {
1020        interceptors::call_terminate_workflow(
1021            self.client.client_interceptors(),
1022            TerminateWorkflowInput {
1023                workflow_id: self.info.workflow_id.clone(),
1024                run_id: self.info.run_id.clone().unwrap_or_default(),
1025                first_execution_run_id: self
1026                    .info
1027                    .first_execution_run_id
1028                    .clone()
1029                    .unwrap_or_default(),
1030                options: opts,
1031            },
1032            Next::new({
1033                let mut client = self.client.clone();
1034                move |input: TerminateWorkflowInput| -> BoxFuture<
1035                    '_,
1036                    Result<(), WorkflowInteractionError>,
1037                > {
1038                    Box::pin(async move {
1039                        let mut request = TerminateWorkflowExecutionRequest {
1040                            namespace: client.namespace(),
1041                            workflow_execution: Some(ProtoWorkflowExecution {
1042                                workflow_id: input.workflow_id,
1043                                run_id: input.run_id,
1044                            }),
1045                            reason: input.options.reason.clone(),
1046                            details: input.options.details.clone(),
1047                            identity: client.identity(),
1048                            first_execution_run_id: input.first_execution_run_id,
1049                            links: vec![],
1050                        }
1051                        .into_request();
1052                        input.options.rpc_options.apply_to(&mut request);
1053                        WorkflowService::terminate_workflow_execution(&mut client, request)
1054                            .await
1055                            .map_err(WorkflowInteractionError::from_status)?;
1056                        Ok(())
1057                    })
1058                }
1059            }),
1060        )
1061        .await
1062    }
1063
1064    /// Get workflow execution description/metadata.
1065    pub async fn describe(
1066        &self,
1067        opts: WorkflowDescribeOptions,
1068    ) -> Result<WorkflowExecutionDescription, WorkflowInteractionError>
1069    where
1070        CT: NamespacedClient,
1071    {
1072        let output = interceptors::call_describe_workflow(
1073            self.client.client_interceptors(),
1074            DescribeWorkflowInput {
1075                workflow_id: self.info.workflow_id.clone(),
1076                run_id: self.info.run_id.clone().unwrap_or_default(),
1077                options: opts,
1078            },
1079            Next::new({
1080                let mut client = self.client.clone();
1081                move |input: DescribeWorkflowInput| -> BoxFuture<
1082                        '_,
1083                        Result<DescribeWorkflowOutput, WorkflowInteractionError>,
1084                    > {
1085                        Box::pin(async move {
1086                            let mut request = DescribeWorkflowExecutionRequest {
1087                                namespace: client.namespace(),
1088                                execution: Some(ProtoWorkflowExecution {
1089                                    workflow_id: input.workflow_id,
1090                                    run_id: input.run_id,
1091                                }),
1092                            }
1093                            .into_request();
1094                            input.options.rpc_options.apply_to(&mut request);
1095                            let response =
1096                                WorkflowService::describe_workflow_execution(&mut client, request)
1097                                    .await
1098                                    .map_err(WorkflowInteractionError::from_status)?
1099                                    .into_inner();
1100                            Ok(DescribeWorkflowOutput::new(response))
1101                        })
1102                    }
1103            }),
1104        )
1105        .await?;
1106        WorkflowExecutionDescription::new(output.response, self.client.data_converter())
1107            .await
1108            .map_err(WorkflowInteractionError::from)
1109    }
1110    /// Fetch workflow execution history.
1111    pub async fn fetch_history(
1112        &self,
1113        opts: WorkflowFetchHistoryOptions,
1114    ) -> Result<WorkflowHistory, WorkflowInteractionError>
1115    where
1116        CT: NamespacedClient,
1117    {
1118        let run_id = self.info.run_id.clone().unwrap_or_default();
1119        self.fetch_history_for_run(&run_id, &opts).await
1120    }
1121
1122    /// Fetch history for a specific run_id, handling pagination.
1123    async fn fetch_history_for_run(
1124        &self,
1125        run_id: &str,
1126        opts: &WorkflowFetchHistoryOptions,
1127    ) -> Result<WorkflowHistory, WorkflowInteractionError>
1128    where
1129        CT: NamespacedClient,
1130    {
1131        let mut all_events = Vec::new();
1132        let mut next_page_token = vec![];
1133
1134        loop {
1135            let output = interceptors::call_fetch_workflow_history_page(
1136                self.client.client_interceptors(),
1137                FetchWorkflowHistoryPageInput {
1138                    workflow_id: self.info.workflow_id.clone(),
1139                    run_id: run_id.to_string(),
1140                    next_page_token,
1141                    options: opts.clone(),
1142                },
1143                Next::new({
1144                    let mut client = self.client.clone();
1145                    move |input: FetchWorkflowHistoryPageInput| -> BoxFuture<
1146                        '_,
1147                        Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>,
1148                    > {
1149                        Box::pin(async move {
1150                            let mut request = GetWorkflowExecutionHistoryRequest {
1151                                namespace: client.namespace(),
1152                                execution: Some(ProtoWorkflowExecution {
1153                                    workflow_id: input.workflow_id,
1154                                    run_id: input.run_id,
1155                                }),
1156                                next_page_token: input.next_page_token,
1157                                skip_archival: input.options.skip_archival,
1158                                wait_new_event: input.options.wait_new_event,
1159                                history_event_filter_type: input.options.event_filter_type as i32,
1160                                ..Default::default()
1161                            }
1162                            .into_request();
1163                            input.options.rpc_options.apply_to(&mut request);
1164                            let response = WorkflowService::get_workflow_execution_history(
1165                                &mut client,
1166                                request,
1167                            )
1168                            .await
1169                            .map_err(WorkflowInteractionError::from_status)?
1170                            .into_inner();
1171                            Ok(FetchWorkflowHistoryPageOutput::new(
1172                                response
1173                                    .history
1174                                    .map(|history| history.events)
1175                                    .unwrap_or_default(),
1176                                response.next_page_token,
1177                            ))
1178                        })
1179                    }
1180                }),
1181            )
1182            .await?;
1183
1184            all_events.extend(output.events);
1185            if output.next_page_token.is_empty() {
1186                break;
1187            }
1188            next_page_token = output.next_page_token;
1189        }
1190
1191        Ok(WorkflowHistory::new(all_events))
1192    }
1193}
1194
1195/// Handle to a workflow update that has been started but may not be complete.
1196///
1197/// Use [`get_result`](Self::get_result) to wait for the update to complete and retrieve its result.
1198pub struct WorkflowUpdateHandle<CT, T> {
1199    client: CT,
1200    update_id: String,
1201    workflow_id: String,
1202    run_id: Option<String>,
1203    /// If the update was started with `Completed` wait stage, the outcome is already available.
1204    known_outcome: Option<update::v1::Outcome>,
1205    _output: PhantomData<T>,
1206}
1207
1208impl<CT, T> WorkflowUpdateHandle<CT, T> {
1209    /// Get the update ID.
1210    pub fn id(&self) -> &str {
1211        &self.update_id
1212    }
1213
1214    /// Get the workflow ID.
1215    pub fn workflow_id(&self) -> &str {
1216        &self.workflow_id
1217    }
1218
1219    /// Get the workflow run ID, if available.
1220    pub fn workflow_run_id(&self) -> Option<&str> {
1221        self.run_id.as_deref()
1222    }
1223}
1224
1225impl<CT, T: 'static> WorkflowUpdateHandle<CT, T>
1226where
1227    CT: WorkflowService + NamespacedClient + Clone,
1228{
1229    /// Wait for the update to complete and return the result using the provided RPC controls.
1230    pub async fn get_result(&self, rpc_options: RpcOptions) -> Result<T, WorkflowUpdateError>
1231    where
1232        T: temporalio_common::data_converters::TemporalDeserializable,
1233    {
1234        let output = interceptors::call_poll_workflow_update(
1235            self.client.client_interceptors(),
1236            PollWorkflowUpdateInput {
1237                update_id: self.update_id.clone(),
1238                workflow_id: self.workflow_id.clone(),
1239                run_id: self.run_id.clone().unwrap_or_default(),
1240                rpc_options,
1241            },
1242            Next::new({
1243                let mut client = self.client.clone();
1244                let known_outcome = self.known_outcome.clone();
1245                move |input: PollWorkflowUpdateInput| -> BoxFuture<
1246                    '_,
1247                    Result<PollWorkflowUpdateOutput, WorkflowUpdateError>,
1248                > {
1249                    Box::pin(async move {
1250                        if let Some(outcome) = known_outcome {
1251                            return Ok(PollWorkflowUpdateOutput::new(outcome));
1252                        }
1253                        // The server's internal long-poll timeout (~60s) may expire before the update
1254                        // completes, returning a response with outcome: None. Keep polling until we
1255                        // get an actual outcome.
1256                        loop {
1257                            let mut request = PollWorkflowExecutionUpdateRequest {
1258                                namespace: client.namespace(),
1259                                update_ref: Some(update::v1::UpdateRef {
1260                                    workflow_execution: Some(ProtoWorkflowExecution {
1261                                        workflow_id: input.workflow_id.clone(),
1262                                        run_id: input.run_id.clone(),
1263                                    }),
1264                                    update_id: input.update_id.clone(),
1265                                }),
1266                                identity: client.identity(),
1267                                wait_policy: Some(WaitPolicy {
1268                                    lifecycle_stage:
1269                                        UpdateWorkflowExecutionLifecycleStage::Completed.into(),
1270                                }),
1271                            }
1272                            .into_request();
1273                            input.rpc_options.apply_to(&mut request);
1274                            let response = WorkflowService::poll_workflow_execution_update(
1275                                &mut client,
1276                                request,
1277                            )
1278                            .await
1279                            .map_err(WorkflowUpdateError::from_status)?
1280                            .into_inner();
1281                            if let Some(outcome) = response.outcome {
1282                                return Ok(PollWorkflowUpdateOutput::new(outcome));
1283                            }
1284                        }
1285                    })
1286                }
1287            }),
1288        )
1289        .await?;
1290        let outcome = output.outcome;
1291
1292        match outcome.value {
1293            Some(update::v1::outcome::Value::Success(success)) => self
1294                .client
1295                .data_converter()
1296                .from_payloads(&SerializationContextData::Workflow, success.payloads)
1297                .await
1298                .map_err(WorkflowUpdateError::from),
1299            Some(update::v1::outcome::Value::Failure(failure)) => {
1300                Err(WorkflowUpdateError::Failed(Box::new(failure)))
1301            }
1302            None => Err(WorkflowUpdateError::Other(
1303                "Update returned no outcome value".into(),
1304            )),
1305        }
1306    }
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311    use super::*;
1312    use crate::test_helpers::XorCodec;
1313    use std::collections::HashMap;
1314    use temporalio_common::{
1315        data_converters::DefaultFailureConverter,
1316        protos::temporal::api::{
1317            common::v1::{Memo, SearchAttributes},
1318            enums::v1::WorkflowExecutionStatus as ProtoWorkflowExecutionStatus,
1319            sdk::v1::UserMetadata,
1320            workflow::v1::WorkflowExecutionConfig,
1321        },
1322    };
1323
1324    #[tokio::test]
1325    async fn workflow_result_details_support_typed_decoding() {
1326        let converter = DataConverter::new(
1327            PayloadConverter::default(),
1328            DefaultFailureConverter,
1329            XorCodec,
1330        );
1331        let payloads = converter
1332            .to_payloads(
1333                &SerializationContextData::Workflow,
1334                &"workflow-result-details".to_owned(),
1335            )
1336            .await
1337            .unwrap();
1338        let details = WorkflowResultDetails::new(payloads.clone(), &converter)
1339            .await
1340            .unwrap();
1341
1342        assert_ne!(details.raw(), payloads);
1343        let decoded_payloads = details.raw().to_vec();
1344        assert_eq!(
1345            details.deserialize::<String>().unwrap(),
1346            "workflow-result-details"
1347        );
1348        assert_eq!(details.into_raw().payloads, decoded_payloads);
1349    }
1350
1351    #[tokio::test]
1352    async fn workflow_result_detail_conversion_errors_are_reported() {
1353        let details =
1354            WorkflowResultDetails::new(vec![Payload::default()], &DataConverter::default())
1355                .await
1356                .unwrap();
1357
1358        assert_eq!(details.raw(), &[Payload::default()]);
1359        assert!(details.deserialize::<String>().is_err());
1360    }
1361
1362    #[tokio::test]
1363    async fn workflow_description_memo_uses_saved_converter() {
1364        let converter = DataConverter::new(
1365            PayloadConverter::default(),
1366            DefaultFailureConverter,
1367            XorCodec,
1368        );
1369        let encoded = converter
1370            .to_payload(
1371                &SerializationContextData::Workflow,
1372                &"memo-value".to_owned(),
1373            )
1374            .await
1375            .unwrap();
1376        let description = WorkflowExecutionDescription::new(
1377            DescribeWorkflowExecutionResponse {
1378                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1379                    memo: Some(Memo {
1380                        fields: HashMap::from([("memo-key".to_owned(), encoded)]),
1381                    }),
1382                    ..Default::default()
1383                }),
1384                ..Default::default()
1385            },
1386            &converter,
1387        )
1388        .await
1389        .unwrap();
1390        let memo = description.memo();
1391
1392        assert_eq!(
1393            memo.get::<String>("memo-key").unwrap(),
1394            Some("memo-value".to_owned())
1395        );
1396    }
1397
1398    #[tokio::test]
1399    async fn workflow_description_accessors_expose_decoded_fields() {
1400        let converter = DataConverter::default();
1401        let memo_payload = converter
1402            .to_payload(&SerializationContextData::Workflow, &"memo-value")
1403            .await
1404            .unwrap();
1405        let search_attr_payload = converter
1406            .to_payload(&SerializationContextData::Workflow, &"search-value")
1407            .await
1408            .unwrap();
1409        let summary_payload = converter
1410            .to_payload(&SerializationContextData::Workflow, &"workflow summary")
1411            .await
1412            .unwrap();
1413        let details_payload = converter
1414            .to_payload(&SerializationContextData::Workflow, &"workflow details")
1415            .await
1416            .unwrap();
1417        let description = WorkflowExecutionDescription::new(
1418            DescribeWorkflowExecutionResponse {
1419                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1420                    execution: Some(ProtoWorkflowExecution {
1421                        workflow_id: "wf-id".to_string(),
1422                        run_id: "run-id".to_string(),
1423                    }),
1424                    r#type: Some(
1425                        temporalio_common::protos::temporal::api::common::v1::WorkflowType {
1426                            name: "wf-type".to_string(),
1427                        },
1428                    ),
1429                    status: ProtoWorkflowExecutionStatus::Completed as i32,
1430                    task_queue: "task-queue".to_string(),
1431                    history_length: 42,
1432                    memo: Some(Memo {
1433                        fields: HashMap::from([("memo-key".to_string(), memo_payload.clone())]),
1434                    }),
1435                    parent_execution: Some(ProtoWorkflowExecution {
1436                        workflow_id: "parent-id".to_string(),
1437                        run_id: "parent-run-id".to_string(),
1438                    }),
1439                    search_attributes: Some(SearchAttributes {
1440                        indexed_fields: HashMap::from([(
1441                            "CustomKeywordField".to_string(),
1442                            search_attr_payload.clone(),
1443                        )]),
1444                    }),
1445                    ..Default::default()
1446                }),
1447                execution_config: Some(WorkflowExecutionConfig {
1448                    user_metadata: Some(UserMetadata {
1449                        summary: Some(summary_payload),
1450                        details: Some(details_payload),
1451                    }),
1452                    ..Default::default()
1453                }),
1454                ..Default::default()
1455            },
1456            &converter,
1457        )
1458        .await
1459        .unwrap();
1460
1461        assert_eq!(description.id(), "wf-id");
1462        assert_eq!(description.run_id(), "run-id");
1463        assert_eq!(description.workflow_type(), "wf-type");
1464        assert_eq!(description.status(), WorkflowExecutionStatus::Completed);
1465        let mut unknown_status_description = description.clone();
1466        unknown_status_description
1467            .raw_description
1468            .workflow_execution_info
1469            .as_mut()
1470            .unwrap()
1471            .status = 123_456;
1472        assert_eq!(
1473            unknown_status_description.status(),
1474            WorkflowExecutionStatus::Unknown
1475        );
1476        assert_eq!(description.task_queue(), "task-queue");
1477        assert_eq!(description.history_length(), 42);
1478        assert_eq!(description.parent_id(), Some("parent-id"));
1479        assert_eq!(description.parent_run_id(), Some("parent-run-id"));
1480        let memo = description.memo();
1481        assert_eq!(memo.raw_value("memo-key"), Some(&memo_payload));
1482        assert_eq!(
1483            memo.get::<String>("memo-key").unwrap(),
1484            Some("memo-value".to_owned())
1485        );
1486        let search_attributes = description.search_attributes();
1487        assert_eq!(
1488            search_attributes.raw_payload("CustomKeywordField"),
1489            Some(&search_attr_payload)
1490        );
1491        assert_eq!(description.static_summary(), Some("workflow summary"));
1492        assert_eq!(description.static_details(), Some("workflow details"));
1493    }
1494
1495    #[tokio::test]
1496    async fn workflow_description_rejects_negative_history_length() {
1497        let err = WorkflowExecutionDescription::new(
1498            DescribeWorkflowExecutionResponse {
1499                workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1500                    history_length: -1,
1501                    ..Default::default()
1502                }),
1503                ..Default::default()
1504            },
1505            &DataConverter::default(),
1506        )
1507        .await
1508        .unwrap_err();
1509
1510        assert_eq!(
1511            err.to_string(),
1512            "Encoding error: workflow history_length must be non-negative, got -1"
1513        );
1514    }
1515}