Skip to content

Commit d20887c

Browse files
deploy: 5988af9
1 parent 406f8db commit d20887c

272 files changed

Lines changed: 1677 additions & 1024 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2.0/llms-full.txt

Lines changed: 237 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ class MyActivity extends Activity
239239

240240
In general, you should only pass small amounts of data in this manner. Rather than passing large amounts of data, you should write the data to the database, cache or file system. Then pass the key or file path to the workflow and activities. The activities can then use the key or file path to read the data.
241241

242+
When the application genuinely needs to carry large bytes through history — documents, media blobs, serialized exports — enable [External Payload Storage](../features/external-payload-storage.md) on the namespace. The runtime offloads over-threshold payloads to a configured object store and records a verifiable reference envelope in history, keeping replay integrity while staying under the [`payload_size_bytes` structural limit](../constraints/structural-limits.md#payload-size).
243+
242244
## Output
243245

244246
Once the workflow has completed, you can retrieve the output using the `output()` method.
@@ -887,13 +889,21 @@ This applies to:
887889
activity(ProcessDocumentActivity::class, $threeMegabyteBlob);
888890
```
889891

890-
To work within the limit, store large data externally and pass a reference:
892+
To work within the limit, either enable
893+
[External Payload Storage](../features/external-payload-storage.md) on the
894+
namespace so the runtime transparently offloads over-threshold payloads to a
895+
configured object store, or store the bytes yourself and pass an
896+
application-level reference:
891897

892898
```php
893899
$ref = Storage::put('docs/incoming.pdf', $blob);
894900
activity(ProcessDocumentActivity::class, $ref);
895901
```
896902

903+
External payload storage preserves replay integrity by recording a hashed
904+
`durable-workflow.v2.external-payload-reference.v1` envelope in history, so
905+
the reference envelope becomes the payload the limit sees — not the bytes.
906+
897907
### Memo size
898908

899909
When a workflow upserts memo entries via `upsertMemo()`, the executor merges the new entries into the existing memo map, then JSON-encodes the merged result and checks the byte length against `memo_size_bytes`. If the merged memo exceeds the limit, the run fails before the memo is persisted.
@@ -5215,6 +5225,232 @@ The schedule table (`workflow_schedules`) is created by migration `2026_04_14_00
52155225

52165226
If your deployment runs package migrations alongside application migrations, migration 157 detects a pre-existing `workflow_schedules` table and handles it gracefully: if the table already matches the package schema it is left as-is; if it was created by an earlier shim migration with a different schema, it is replaced.
52175227

5228+
<!-- Source: docs/features/external-payload-storage.md -->
5229+
5230+
# External Payload Storage
5231+
5232+
External payload storage offloads large workflow payloads to a pluggable
5233+
object store (S3, GCS, Azure Blob, or a local filesystem) and replaces the
5234+
inline bytes in workflow history with a small, verifiable reference envelope.
5235+
Use it when activity or child-workflow arguments, results, signals, or update
5236+
payloads are too large to live inline in the database row that backs workflow
5237+
history.
5238+
5239+
The runtime still carries inline payloads as long as the encoded size stays
5240+
under the namespace threshold. Only payloads that cross the threshold are
5241+
written to the configured driver and recorded in history as a
5242+
`durable-workflow.v2.external-payload-reference.v1` envelope. Replay and
5243+
history export fail closed when a reference is missing, mutated, or outside
5244+
the configured prefix — the system never silently substitutes an empty value
5245+
for a missing blob.
5246+
5247+
## When To Use It
5248+
5249+
Prefer external payload storage whenever the application legitimately needs
5250+
to pass bytes larger than a few hundred kilobytes through a workflow:
5251+
5252+
- Document and media processing pipelines that hand PDFs, images, or audio
5253+
blobs from one activity to the next.
5254+
- Reports, exports, or archives whose final output is a large serialized
5255+
artifact.
5256+
- Message stream payloads produced by external systems that do not expose a
5257+
stable object URL the workflow can reference directly.
5258+
- Any payload that would otherwise trip the
5259+
[`payload_size_bytes` structural limit](../constraints/structural-limits.md#payload-size).
5260+
5261+
Small payloads — control-plane fields, ids, status flags, typical JSON —
5262+
stay inline and pay nothing extra. The policy is threshold-gated, so enabling
5263+
external storage on a namespace does not move small payloads.
5264+
5265+
## How Offload Works
5266+
5267+
Each namespace carries an independent external payload storage policy. When
5268+
the runtime encodes a payload for durable storage, it checks the encoded byte
5269+
length against the configured `threshold_bytes`:
5270+
5271+
- Encoded size is under the threshold. The payload is stored inline, as
5272+
today. Nothing in history changes.
5273+
- Encoded size is at or over the threshold. The runtime hands the encoded
5274+
bytes to the configured driver, receives back a driver-owned URI, and
5275+
records an external payload reference in history. The reference carries the
5276+
URI, a SHA-256 hash, the exact byte length, the payload codec, and an
5277+
optional `expires_at` hint.
5278+
5279+
On replay, workers fetch the referenced bytes through the same driver,
5280+
verify that the returned object has the expected size and SHA-256, and only
5281+
then hand the payload to the decoder. A size or hash mismatch raises
5282+
`ExternalPayloadIntegrityException` (PHP) or `ExternalPayloadIntegrityError`
5283+
(Python) and surfaces as a replay failure — never as a silent empty payload.
5284+
5285+
The reference envelope is a stable wire format. It is identical whether the
5286+
producer is a PHP workflow, a Python SDK worker, or a direct HTTP API caller.
5287+
For the full field contract see
5288+
[External Payload Reference Envelope](../polyglot/server-api-reference.md#external-payload-reference-envelope).
5289+
5290+
## Driver Choices
5291+
5292+
| Driver | URI scheme | Typical use |
5293+
| --- | --- | --- |
5294+
| `local` | `file://` | Local development, CI, and single-node deployments where the server and workers share a filesystem. Not suitable when workers run on different hosts than the server. |
5295+
| `s3` | `s3://` | Amazon S3 and S3-compatible object stores (MinIO, Cloudflare R2, etc.) through a server-side filesystem disk. |
5296+
| `gcs` | `gs://` | Google Cloud Storage through a server-side filesystem disk. |
5297+
| `azure` | `azure://` | Azure Blob Storage through a server-side filesystem disk. |
5298+
5299+
Object-store drivers configure the actual bucket/container credentials
5300+
through a named server-side filesystem disk, so secrets live in the server's
5301+
configuration rather than in the namespace policy record.
5302+
5303+
## Configuring A Namespace
5304+
5305+
Configure the policy with the [CLI](../polyglot/cli-reference.md#namespace-and-search-attribute-commands)
5306+
or the [server HTTP API](../polyglot/server-api-reference.md#namespace-and-storage).
5307+
Both write the same `external_payload_storage` envelope on the namespace
5308+
record.
5309+
5310+
### With The CLI
5311+
5312+
```bash
5313+
# Production namespace using Amazon S3 through the 'external-payload-objects' disk.
5314+
dw namespace:set-storage-driver billing s3 \
5315+
--disk=external-payload-objects \
5316+
--bucket=dw-payloads \
5317+
--prefix=billing/ \
5318+
--threshold-bytes=2097152
5319+
5320+
# Development namespace using the local filesystem.
5321+
dw namespace:set-storage-driver dev local \
5322+
--uri=file:///var/lib/durable-workflow/payloads
5323+
5324+
# Disable offload while keeping the policy record (all payloads stay inline).
5325+
dw namespace:set-storage-driver billing s3 \
5326+
--disk=external-payload-objects \
5327+
--bucket=dw-payloads \
5328+
--disable
5329+
```
5330+
5331+
### With The Server API
5332+
5333+
```bash
5334+
curl -sS -X PUT "$DURABLE_WORKFLOW_SERVER_URL/api/namespaces/billing/external-storage" \
5335+
-H "Authorization: Bearer $DURABLE_WORKFLOW_AUTH_TOKEN" \
5336+
-H "X-Durable-Workflow-Control-Plane-Version: 2" \
5337+
-H "Content-Type: application/json" \
5338+
-d '{
5339+
"enabled": true,
5340+
"driver": "s3",
5341+
"threshold_bytes": 2097152,
5342+
"config": {
5343+
"disk": "external-payload-objects",
5344+
"bucket": "dw-payloads",
5345+
"prefix": "billing/"
5346+
}
5347+
}'
5348+
```
5349+
5350+
The namespace description returned by `GET /api/namespaces/{name}` or
5351+
`dw namespace:describe` carries the resolved `external_payload_storage`
5352+
envelope so operators and automation can verify the active policy without
5353+
re-issuing a write.
5354+
5355+
## Verifying The Policy
5356+
5357+
Use the round-trip diagnostic to prove a configured policy can actually
5358+
write and read bytes under the namespace's credentials before opening it to
5359+
workflow traffic:
5360+
5361+
```bash
5362+
dw storage:test --namespace=billing --large-bytes=2097152 --json
5363+
```
5364+
5365+
The diagnostic writes a small inline payload plus one payload that crosses
5366+
the threshold, fetches both back, verifies size and SHA-256, and returns
5367+
machine-readable `small_payload` and `large_payload` result objects.
5368+
A passing large-payload result proves the driver can produce a valid
5369+
`durable-workflow.v2.external-payload-reference.v1` envelope end to end. A
5370+
failing diagnostic should be treated as a storage-policy problem — do not
5371+
enable workflow traffic through a namespace whose policy cannot pass the
5372+
round trip.
5373+
5374+
## Picking A Threshold
5375+
5376+
The default behavior is to leave inline payloads alone unless they cross
5377+
`threshold_bytes`. Good starting points:
5378+
5379+
- Match the threshold to the point at which inline payloads start creating
5380+
operational pressure — usually somewhere between 256 KiB and 2 MiB of
5381+
encoded bytes.
5382+
- Leave comfortable headroom under the namespace
5383+
[`payload_size_bytes`](../constraints/structural-limits.md#payload-size)
5384+
structural limit so that the reference envelope is the cap, not the bytes
5385+
themselves.
5386+
- Set a single threshold per namespace. Choose it from the payload-producing
5387+
activity or workflow that drives the highest bytes-per-run, rather than
5388+
tuning it for the median payload.
5389+
5390+
There is no benefit to setting a very low threshold: small payloads round
5391+
trip through the database faster than they round trip through external
5392+
storage, and the reference envelope itself consumes a (small) amount of
5393+
history space.
5394+
5395+
## Replay, Retention, And Cleanup
5396+
5397+
- **Replay integrity.** Every fetch verifies the stored object against the
5398+
reference's `size_bytes` and `sha256`. A mutated or missing blob raises an
5399+
integrity exception rather than silently substituting a different value.
5400+
- **Verified-fetch cache.** Workers cache verified bytes by
5401+
`(uri, sha256, size, codec)` with a bounded entry count and byte ceiling.
5402+
Repeated history reads on the same run avoid refetching the same object
5403+
without weakening the integrity check on first load.
5404+
- **Retention.** When the server's retention pass removes a workflow run,
5405+
it also deletes the external payload objects referenced by that run's
5406+
history. Orphan objects do not accumulate as long as retention is running.
5407+
- **History export.** Exported history preserves the reference envelope.
5408+
Downstream consumers that need the referenced bytes should fetch through
5409+
the same driver and verify against the envelope before decode — the export
5410+
format does not inline external bytes.
5411+
5412+
## Using It From Code
5413+
5414+
Most applications never call the storage API directly: the runtime offloads
5415+
transparently based on the namespace policy, and the SDK decodes references
5416+
on replay. Applications that need to build or consume envelopes outside the
5417+
runtime — for example, a language-neutral bridge handler or a test that
5418+
synthesizes a large payload — use the SDK helpers.
5419+
5420+
- **PHP (workflow package).** The
5421+
`Workflow\V2\Support\ExternalPayloadStorage` helper stores and fetches
5422+
bytes through any driver implementing
5423+
`Workflow\V2\Contracts\ExternalPayloadStorageDriver`.
5424+
`LocalFilesystemExternalPayloadStorage` handles `file://` URIs, and the
5425+
standalone server ships a filesystem-disk driver that backs the `s3`,
5426+
`gcs`, and `azure` policy drivers through a named Laravel disk.
5427+
- **Python SDK.** See
5428+
[External Payload Storage](../polyglot/python.md#external-payload-storage)
5429+
for `ExternalPayloadReference`, `ExternalPayloadCache`,
5430+
`store_external_payload()`, `fetch_external_payload()`, and the
5431+
`LocalFilesystemExternalStorage`, `S3ExternalStorage`,
5432+
`GCSExternalStorage`, and `AzureBlobExternalStorage` adapters. Cloud SDK
5433+
clients remain application-owned; the SDK does not add boto3,
5434+
google-cloud-storage, or azure-storage-blob as runtime dependencies.
5435+
- **Direct HTTP.** HTTP callers that encode payloads manually can store
5436+
bytes through the driver, then submit the reference envelope as the
5437+
payload field on the request. The worker-protocol payload envelope
5438+
(`{codec, blob}`) still carries references for activity arguments,
5439+
results, signal payloads, and update payloads.
5440+
5441+
## See Also
5442+
5443+
- [Passing Data](../defining-workflows/passing-data.md) for the default
5444+
inline payload contract.
5445+
- [Structural Limits: Payload Size](../constraints/structural-limits.md#payload-size)
5446+
for the engine-enforced ceiling that external storage lets you work under.
5447+
- [Server API Reference: Namespace And Storage](../polyglot/server-api-reference.md#namespace-and-storage)
5448+
for the full HTTP contract, including the reference envelope fields.
5449+
- [CLI Reference: Namespace And Search Attribute Commands](../polyglot/cli-reference.md#namespace-and-search-attribute-commands)
5450+
for `dw namespace:set-storage-driver` and `dw storage:test` usage.
5451+
- [Python SDK: External Payload Storage](../polyglot/python.md#external-payload-storage)
5452+
for Python-side drivers, helpers, and replay-cache guidance.
5453+
52185454
<!-- Source: docs/configuration/publishing-config.md -->
52195455

52205456
# Publishing Config

0 commit comments

Comments
 (0)