* Add initial skill for testing, which is simply Steve's skill (#1) * Add initial skill for testing, which is simply Steve's skill * Rename skill to 'temporal-dev' and update version Updated skill name and version for Temporal Python. * Use claude to merge Steve's, Max's, and Mason's skills. (#2) * Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills * Add missing things from Steve * trigger tweaks * Add in common gotchas from Johann * add simple feedback mechanism (#3) * Change skill name to kebab-case, for compatibility with Amp and Cline (#7) * Clean up references/core/ai-integration.md * Clean up references/core/common-gotchas.md * Clean up references/core/common-gotchas.md * Clean up references/core/determinism.md * Clean up references/core/determinism.md * Update error-reference.md * Update interactive-workflows.md * Clean up patterns.md * Cut shell scripts * Edit troubleshooting.md * remove interceptors for now * remove dynamic workflows * clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals * Improve references/python/advanced-features.md * Use explicit namespace in connect * remove duplicated content from determinism.md, clean up * Improve references/python/data-handling.md * Prefer start_to_close_timeout * don't explicitely provide defaults for retry policies * error-handling.md cleanup * move idempotency patterns to patterns.md * remove multi-param activities * small edits * Unify sandbox stuff into one file * local activities aren't experimental * Clean up references/python/sync-vs-async.md * Cleanup observability.md, remove duplicated search attributes * Cut otel for now * cut a lot of duplicate stuff from python gotchas, address comments * de-duplicate content * Lots of improvements to testing * cleanup to top level of skill (like CLI install instructions), and to top-level of python * Improve patterns.md * clean up ai-patterns.md * Update readme with installation instructions * remove ts directory * De-couple core from python and TypeScript as much as possible * Remove TypeScript hints * add prompting for feedback at startup - wait for ethan on slack channel * shorten url * Update slack channel * Automated pass over on python cleanup & deduplication * Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34) * Add TypeScript (#31) Adds initial support for TypeScript to the skill --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> * Fix typos and reference links (#36) * Fix typos and reference links * 2 more typo fixes * quick edit to readme (#37) * Fix saga compensations to run under cancellation protection (#43) When a workflow is cancelled mid-saga, compensations must run in a cancellation-protected scope, otherwise they are immediately cancelled before they can execute. - Python: wrap compensation loop in asyncio.shield() so it runs even when the workflow receives a CancelledError - TypeScript: wrap compensation loop in CancellationScope.nonCancellable() so it runs even when the root scope is cancelled (per official docs: "Cleanup logic must be in a nonCancellable scope") - TypeScript: also fix compensation registration order — register BEFORE calling the activity (was already correct in Python) Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * Update readme for public preview (#45) * a few more readme tweaks (#46) * Add MIT License to the project (#47) * Add Go (supersedes other PR) (#38) * progress on go * Go translation workflow completed. * missed a few spots * Manual edits * Address feedback * Add gotcha about anonymous local activities * Sample code for payload converter * clarify sdk protection mechanisms * Setup CODEOWNERS to AI SDK team (#48) * Align version number in SKILL.md and plugin.json. (#49) --------- Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com> Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
7.6 KiB
Go SDK Data Handling
Overview
The Go SDK uses the converter.DataConverter interface to serialize/deserialize workflow inputs, outputs, and activity parameters. The default converter converts values to JSON.
Default Data Converter
The default CompositeDataConverter applies converters in order until one returns a non-nil Payload:
converter.NewNilPayloadConverter()-- nil valuesconverter.NewByteSlicePayloadConverter()--[]byteconverter.NewProtoJSONPayloadConverter()-- Protobuf messages as JSONconverter.NewProtoPayloadConverter()-- Protobuf messages as binaryconverter.NewJSONPayloadConverter()-- anything JSON-serializable
Structs must have exported fields to be serialized.
Custom Data Converter
In most cases you don't implement the full DataConverter interface directly. Instead, implement a PayloadConverter for your specific type and insert it into a CompositeDataConverter. The PayloadConverter interface has four methods:
type PayloadConverter interface {
ToPayload(value interface{}) (*commonpb.Payload, error) // return nil if this type isn't handled
FromPayload(payload *commonpb.Payload, valuePtr interface{}) error
ToString(payload *commonpb.Payload) string
Encoding() string // e.g. "json/msgpack"
}
Example — custom msgpack PayloadConverter:
import (
"encoding/json"
"fmt"
commonpb "go.temporal.io/api/common/v1"
"go.temporal.io/sdk/converter"
"github.com/vmihailenco/msgpack/v5"
)
const encodingMsgpack = "binary/msgpack"
type MsgpackPayloadConverter struct{}
func (c *MsgpackPayloadConverter) Encoding() string {
return encodingMsgpack
}
func (c *MsgpackPayloadConverter) ToPayload(value interface{}) (*commonpb.Payload, error) {
if value == nil {
return nil, nil
}
data, err := msgpack.Marshal(value)
if err != nil {
return nil, fmt.Errorf("msgpack marshal: %w", err)
}
return &commonpb.Payload{
Metadata: map[string][]byte{
converter.MetadataEncoding: []byte(encodingMsgpack),
},
Data: data,
}, nil
}
func (c *MsgpackPayloadConverter) FromPayload(payload *commonpb.Payload, valuePtr interface{}) error {
if string(payload.GetMetadata()[converter.MetadataEncoding]) != encodingMsgpack {
return fmt.Errorf("unsupported encoding")
}
return msgpack.Unmarshal(payload.Data, valuePtr)
}
func (c *MsgpackPayloadConverter) ToString(payload *commonpb.Payload) string {
// Decode to a map for human-readable display
var v interface{}
if err := msgpack.Unmarshal(payload.Data, &v); err != nil {
return fmt.Sprintf("<msgpack: %v>", err)
}
b, _ := json.Marshal(v)
return string(b)
}
Register in a CompositeDataConverter and pass to the client:
dataConverter := converter.NewCompositeDataConverter(
converter.NewNilPayloadConverter(),
converter.NewByteSlicePayloadConverter(),
&MsgpackPayloadConverter{}, // handles your type; falls through to JSON for everything else
converter.NewJSONPayloadConverter(),
)
c, err := client.Dial(client.Options{
DataConverter: dataConverter,
})
Per-activity/child-workflow override — use a different converter for specific calls:
actCtx := workflow.WithDataConverter(ctx, mySpecialConverter)
workflow.ExecuteActivity(actCtx, SensitiveActivity, input)
Note: If your converter makes remote calls (e.g., to a KMS for encryption), wrap it with workflow.DataConverterWithoutDeadlockDetection to avoid deadlock detection timeouts in workflow code.
Composition of Payload Converters
Use converter.NewCompositeDataConverter to chain type-specific converters. The first converter that can handle the type wins.
dataConverter := converter.NewCompositeDataConverter(
converter.NewNilPayloadConverter(),
converter.NewByteSlicePayloadConverter(),
converter.NewProtoJSONPayloadConverter(),
converter.NewProtoPayloadConverter(),
YourCustomPayloadConverter(),
converter.NewJSONPayloadConverter(),
)
Protobuf Support
Binary protobuf:
converter.NewProtoPayloadConverter()
JSON protobuf:
converter.NewProtoJSONPayloadConverter()
Both are included in the default data converter. SDK v1.26.0 (March 2024) migrated from gogo/protobuf to google/protobuf. If you need backward compatibility with older payloads encoded with gogo, use the LegacyTemporalProtoCompat option.
Payload Encryption
Implement the converter.PayloadCodec interface (Encode and Decode) and wrap the default data converter:
// Codec implements converter.PayloadCodec for encryption.
type Codec struct{}
func (Codec) Encode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) {
result := make([]*commonpb.Payload, len(payloads))
for i, p := range payloads {
origBytes, err := p.Marshal()
if err != nil {
return payloads, err
}
encrypted := encrypt(origBytes) // your encryption logic
result[i] = &commonpb.Payload{
Metadata: map[string][]byte{converter.MetadataEncoding: []byte("binary/encrypted")},
Data: encrypted,
}
}
return result, nil
}
func (Codec) Decode(payloads []*commonpb.Payload) ([]*commonpb.Payload, error) {
result := make([]*commonpb.Payload, len(payloads))
for i, p := range payloads {
if string(p.Metadata[converter.MetadataEncoding]) != "binary/encrypted" {
result[i] = p
continue
}
decrypted := decrypt(p.Data) // your decryption logic
result[i] = &commonpb.Payload{}
err := result[i].Unmarshal(decrypted)
if err != nil {
return payloads, err
}
}
return result, nil
}
Wrap with CodecDataConverter and pass to client:
var DataConverter = converter.NewCodecDataConverter(
converter.GetDefaultDataConverter(),
&Codec{},
)
c, err := client.Dial(client.Options{
DataConverter: DataConverter,
})
Search Attributes
Set at workflow start:
handle, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "order-123",
TaskQueue: "orders",
SearchAttributes: map[string]interface{}{
"OrderStatus": "pending",
"CustomerId": "cust-456",
},
}, OrderWorkflow, input)
Upsert from within a workflow:
err := workflow.UpsertSearchAttributes(ctx, map[string]interface{}{
"OrderStatus": "completed",
})
Typed search attributes (v1.26.0+, preferred):
var OrderStatusKey = temporal.NewSearchAttributeKeyKeyword("OrderStatus")
err := workflow.UpsertTypedSearchAttributes(ctx, OrderStatusKey.ValueSet("completed"))
Query workflows by search attributes:
resp, err := c.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
Query: `OrderStatus = "pending" AND CustomerId = "cust-456"`,
})
Workflow Memo
Set in start options:
handle, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: "order-123",
TaskQueue: "orders",
Memo: map[string]interface{}{
"customerName": "Alice",
"notes": "Priority customer",
},
}, OrderWorkflow, input)
Read memo from workflow info. Upsert memo (Go SDK only):
err := workflow.UpsertMemo(ctx, map[string]interface{}{
"notes": "Updated notes",
})
Best Practices
- Use structs with exported fields for inputs and outputs
- Prefer JSON for readability during development, protobuf for performance in production
- Keep payloads small -- see
references/core/gotchas.mdfor limits - Use
PayloadCodecfor encryption; never store sensitive data unencrypted - Configure the same data converter on both client and worker