PATTERN Cited by 1 source
Metadata envelope in durable payload¶
Pattern¶
When a durable engine (workflow engine, queue, scheduler, event log) persists a payload and replays it later, thread routing metadata through an envelope inside the payload rather than a side channel. The engine stays blind to the envelope; the dispatcher at both ends handles wrap (on invocation) and unwrap (on replay).
┌──────────────────────┐
outbound (dispatcher): │ wrap({ __metadata, │
user_payload ──────> │ user_payload})│ ──> engine
└──────────────────────┘
↓
engine persists
↓
hours / days later
↓
┌──────────────────────┐
inbound (dispatcher): │ unwrap({ __metadata, │ <── engine replays
user_payload <────── │ user_payload})│
└──────────────────────┘ ↓
route into right tenant's code
Cloudflare canonicalises this pattern in Dynamic Workflows:
tenant calls: create({ params: { name: 'Alice' } })
│
▼
engine sees: create({ params: {
__workerLoaderMetadata: { tenantId: 't-42' },
params: { name: 'Alice' }
}})
(Source: Cloudflare Dynamic Workflows.)
When to apply¶
- You have a durable engine that persists payloads and replays them across crashes / sleeps / redeploys.
- You need to route that replay to different code depending on context the engine doesn't understand (tenant ID, version, agent ID, etc.).
- You don't control the engine's internal code or schema — you can only shape the payload.
- You want zero changes to the engine itself; all the new behaviour sits in dispatcher glue on top.
Load-bearing design choices¶
- Envelope lives inside the payload, not in headers or a side channel. Engines typically strip headers before persisting; only body bytes reliably survive the replay path.
- Envelope shape is stable and opaque to the engine. Engines typically treat the payload as user-controlled bytes (JSON, MessagePack, Protobuf). As long as the dispatcher's wrap shape matches its unwrap shape, the engine doesn't need to know.
- Metadata is routing, not authorization. The envelope
is readable by the tenant (e.g. via
instance.status()) — treat it accordingly. Put secrets in a separate per- tenant credential store (KMS, DO key-holder, etc.). (Source: sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant.) - Envelope is evolvable. Include a version discriminator from day 1 so you can ship V2 envelopes without breaking in-flight replays.
- Dispatcher is the only piece that knows about envelopes.
The engine doesn't. The tenant's code doesn't — they
interact with
env.WORKFLOWSas if it were a normal binding;wrapWorkflowBindinginserts andcreateDynamicWorkflowEntrypointremoves. "Every interesting line of this library is either a wrapper around.create()on the outbound side or a wrapper aroundWorkflowEntrypointon the inbound side." - Wrap + unwrap must be deterministic. Replay-based durable execution relies on deterministic state reconstruction; the envelope shape must not depend on runtime state that isn't available at replay time.
Trade-offs¶
Upsides:
- Additive-only implementation. No engine fork, no schema migration, no capacity-planning discontinuity.
- Survives every persistence boundary. Sleep, crash, redeploy, eviction — if the payload survives, the envelope rides along.
- Cheap to generalise. Once you have the pattern, you can reapply it to every durable primitive.
- Debug-friendly. The envelope is introspectable via the
same APIs the tenant uses (e.g.
instance.status()).
Downsides:
- Envelope shape becomes a compatibility contract. Breaking changes require migration.
- Tenant can read the envelope. Don't put secrets in it. Don't put anything load-bearing for authorization in it.
- Payload bloat. Small, but nonzero per-invocation overhead.
- Replay-determinism interactions. Cloudflare doesn't yet disclose how mid-flight tenant redeploys interact with envelope-identified code fetch. See concepts/workflow-determinism-requirement.
Canonical instance¶
Cloudflare Dynamic Workflows:
- Outbound wrapper:
wrapWorkflowBinding({ tenantId })returns anenv.WORKFLOWSthat rewrites everycreate()call to thread__workerLoaderMetadata: { tenantId }into the payload. - Inbound wrapper:
createDynamicWorkflowEntrypoint(async ({ env, metadata }) => loadRunnerFor(env, metadata))is the class registered inwrangler.jsonc. On everyrun(event, step)call, it reads the envelope out ofevent.payload, passes it to theloadRunnercallback, and forwards the unwrapped payload through. - Engine layer: the real Workflows engine persists
event.payloadverbatim; has no concept of tenant routing.
Generalises to¶
- Queues: envelope the producer ID so the consumer fleet can dispatch into the right per-producer handler at replay.
- Durable timers: envelope the tenant ID so the timer firing routes into the right tenant's handler.
- Event-sourced aggregates: envelope the aggregate key so projection replays route into the right handler.
- Outbound webhooks: envelope the subscription ID so the retry path knows which tenant's destination it's talking to.
Seen in¶
- sources/2026-05-01-cloudflare-introducing-dynamic-workflows-durable-execution-that-follows-the-tenant — canonicalises the wrap-on-outbound + unwrap-on-inbound shape as the load-bearing wire-format primitive that makes Dynamic Workflows work without engine-level changes.
Related¶
- systems/cloudflare-dynamic-workflows
- systems/cloudflare-workflows
- systems/dynamic-workers
- concepts/envelope-wrap-and-unwrap-metadata-routing
- concepts/per-tenant-dynamic-code-dispatch
- concepts/byo-workflow-per-tenant
- concepts/durable-execution
- concepts/workflow-replay-from-checkpointed-actions
- patterns/dynamic-binding-over-static-binding
- patterns/workflow-primitives-as-annotated-classes
- companies/cloudflare