Skip to content

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

  1. 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.
  2. 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.
  3. 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.)
  4. Envelope is evolvable. Include a version discriminator from day 1 so you can ship V2 envelopes without breaking in-flight replays.
  5. Dispatcher is the only piece that knows about envelopes. The engine doesn't. The tenant's code doesn't — they interact with env.WORKFLOWS as if it were a normal binding; wrapWorkflowBinding inserts and createDynamicWorkflowEntrypoint removes. "Every interesting line of this library is either a wrapper around .create() on the outbound side or a wrapper around WorkflowEntrypoint on the inbound side."
  6. 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 an env.WORKFLOWS that rewrites every create() call to thread __workerLoaderMetadata: { tenantId } into the payload.
  • Inbound wrapper: createDynamicWorkflowEntrypoint(async ({ env, metadata }) => loadRunnerFor(env, metadata)) is the class registered in wrangler.jsonc. On every run(event, step) call, it reads the envelope out of event.payload, passes it to the loadRunner callback, and forwards the unwrapped payload through.
  • Engine layer: the real Workflows engine persists event.payload verbatim; 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

Last updated · 438 distilled / 1,268 read