Skip to content

feat(client): allow setting memo on workflow start - #1443

Open
cgreeno wants to merge 7 commits into
temporalio:mainfrom
cgreeno:feat/memo-on-workflow-start
Open

feat(client): allow setting memo on workflow start#1443
cgreeno wants to merge 7 commits into
temporalio:mainfrom
cgreeno:feat/memo-on-workflow-start

Conversation

@cgreeno

@cgreeno cgreeno commented Aug 3, 2026

Copy link
Copy Markdown

What was changed

Updated per review, this no longer asks callers to build the proto Memo themselves.

  • WorkflowStartOptions::memo is now Option<MemoValues>, the same type continue-as-new and upsert_memo already take
  • Moved MemoValue/MemoValues out of temporalio_workflow and into common-wasm, next to the existing Memo read wrapper
  • Wired into both StartWorkflowExecutionRequest and SignalWithStartWorkflowExecutionRequest
  • Memo is encoded with the payload converter and the codec

On where the types landed

They went to temporalio-common-wasm rather than temporalio-common. temporalio_workflow depends on common-wasm and not on common, so common is too high for the workflow crate to reach... common-wasm is the lowest common point. Memo already lives there and temporalio_common re-exports it, so this follows the same shape. Re-exported from temporalio_common, temporalio_workflow and temporalio_sdk, so existing import paths keep working.

On the codec

Writing this turned up a round-trip bug worth calling out separately. The read side already codec-decodes memo... WorkflowExecutionDescription::new runs decode_payloads over the whole describe response, and the list path decodes execution.memo explicitly. describe_decodes_workflow_payload_fields asserts exactly this with XorCodec. So encoding with the payload converter alone would have broken the round trip for anyone running a real codec, which is mostly people encrypting payloads. Start now runs the memo through encode_payloads too.

The two new client tests fail without this, and the raw bytes on the wire are the plaintext "memo-value", so it is easy to see. The existing integ tests did not catch it because they use the default identity codec.

Why?

There's currently no way to set a memo when starting a workflow from the client.

The rest of memo support is already here.... continue-as-new can set one, WorkflowExecutionDescription::memo() reads one back, and there's even a memo_warn_size threshold in PayloadLimitsOptions for outbound memos. It's only the client start path that was missing.

I ran into this building an Elixir SDK on top of core. Memo at start is the primary path in the other SDKs and in the docs, so I spent a while assuming I'd missed the setter before concluding it wasn't there.

Checklist

  1. Closes N/A

  2. How was this tested:

Client unit tests on the mock start client, all under XorCodec so the codec path is actually exercised:

  • memo reaches StartWorkflowExecutionRequest, is codec-encoded, and reads back through the same decode path describe uses
  • same for SignalWithStartWorkflowExecutionRequest
  • no memo set sends None
  • a memo value that fails to serialize surfaces WorkflowStartError::PayloadConversion and never sends the request

Plus the two integ tests in workflow_client_tests.rs, moved over to MemoValues. The MemoValues::encode unit test moved with the type into common-wasm.

cargo integ-test (490 passed), cargo lint and cargo fmt --all --check all pass locally.

  1. Any docs updates needed?

CHANGELOG updated. Fields are documented inline.


⚠️ One breaking change to flag, since it is a judgement call rather than something the review asked for.

MemoValue held its value in an Rc. WorkflowStartOptions travels through the interceptor chain inside a Send future, and an Rc cannot cross that, so sharing one type between the client and the workflow means it has to be Arc and the value has to be Send + Sync:

// before
pub fn new<T: TemporalSerializable + 'static>(value: T) -> Self
// after
pub fn new<T: TemporalSerializable + Send + Sync + 'static>(value: T) -> Self

This only breaks memo values that are themselves non-Send/non-Sync, so something holding an Rc or a RefCell. Legal in workflow code today, but I'd guess rare. It also matches how DataConverter erases its own converters (Arc<dyn ... + Send + Sync>).

I looked at avoiding it. An enum with both Rc and Arc variants doesn't work, auto traits are per type and not per value. Box<dyn Trait + Send> drops the Sync requirement but needs T: Clone for MemoValue: Clone, which is a worse trade. Encoding to proto before the options enter the interceptor chain would leave a non-Send field on WorkflowStartOptions itself, which seems worse again. Happy to go a different way if you'd rather, this just seemed like the cheapest option.

`WorkflowStartOptions` had no memo field, so there was no way to attach a
memo when starting a workflow from the client. The read side and the
workflow side already exist - continue-as-new can set one, and
`WorkflowExecutionDescription::memo()` reads one back - it was only the
client start path that was missing.

Adds `memo: Option<ProtoMemo>` and wires it into both
`StartWorkflowExecutionRequest` and `SignalWithStartWorkflowExecutionRequest`.

Uses the proto type directly, matching the existing `header: Option<Header>`
field. Both are `map<string, Payload>` and the caller has to convert values
either way. Imported as `ProtoMemo` so it doesn't collide with the
re-exported `temporalio_common::Memo`, which is the read-side wrapper.

Two integ tests in workflow_client_tests.rs: one that a memo set at start
comes back through describe, one that it's empty when unset.
@cgreeno
cgreeno requested a review from a team as a code owner August 3, 2026 22:14
@CLAassistant

CLAassistant commented Aug 3, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@Sushisource

Copy link
Copy Markdown
Member

Thanks for making a PR! We definitely do want to avoid asking users to supply the proto fields directly. That work is mostly complete. For memo, we should do something like what we're doing in the workflow: https://github.com/temporalio/sdk-core/blob/2e725f1349b8bcdc9ecf3b153c7a02cb9ba1c44e/crates/workflow/src/workflow_context.rs#L1976

But this looks like it will require moving the MemoValue type down into the common crate.

@chris-olszewski May have some input

@chris-olszewski chris-olszewski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As Spencer mentioned, we should use MemoValues here. It is currently defined in temporalio_workflow, but only has deps on stdlib/temporalio_common so it should be a straightforward to move it to temporalio_common so we can use it from workflows and clients. Only thing to call out is to make sure we re-export it from temporalio_workflow after the move.

cgreeno added 3 commits August 4, 2026 22:04
Per review, the client shouldn't ask callers to build the proto Memo
themselves. It now takes `MemoValues`, the same type the workflow surface
already uses for continue-as-new and `upsert_memo`.

`MemoValue`/`MemoValues` move out of `temporalio_workflow` and into
common-wasm, next to the existing `Memo` read wrapper. That's where they
have to go for both sides to reach them... `temporalio_workflow` depends on
common-wasm, not common. Re-exported from `temporalio_common`,
`temporalio_workflow` and `temporalio_sdk`, so existing imports still work.

`MemoValue` holds its value in an `Arc` now instead of an `Rc`, and wants
`Send + Sync`. `WorkflowStartOptions` travels through the interceptor chain
inside a `Send` future and an `Rc` can't cross that. Same shape as
`DataConverter`, which already erases its converters as
`Arc<dyn ... + Send + Sync>`.

Also runs the memo through the payload codec, not just the payload
converter. The read side already codec-decodes it... describe and list both
run `decode_payloads` over the memo. So converter-only encoding broke the
round trip for anyone with a real codec. Caught this after wiring up the
tests below, the two client tests fail without it.

Client unit tests cover start and signal-with-start under a codec, plus the
serialization failure path. Integ tests moved over to `MemoValues`.
@cgreeno

cgreeno commented Aug 5, 2026

Copy link
Copy Markdown
Author

@chris-olszewski - this look any better? I was trying to avoid breaking changes before, tried this time to obut didnt like the options. Clauded the PR Desc and got a code review and fixed some bugs with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants