Files
github-actions[bot] 9092640013 feat(core): support passing parent WritableStream to child workflow via start() (#2059) (#2070)
* test(e2e): cover WritableStream passed as start() argument

Adds an e2e workflow + test where a parent workflow gets a WritableStream
via getWritable(), forwards it through start() to a child workflow, and
the child step writes raw bytes to it. Asserts the external reader on
the parent's stream observes the exact bytes the child wrote.

* fix(core): avoid double-framing when WritableStream is forwarded via start()

When a workflow's getWritable() handle is passed across start() to a
child workflow, the parent step's reviver wraps it in a serialize
transform that pipes into a workflow server stream. Until now,
getExternalReducers.WritableStream then installed a second serialize
transform on top of that — so every chunk the child step wrote got
devalue-framed twice but only deframed once on the reader side, and
external consumers saw the inner frame instead of the original bytes.

Fix: tag every user-visible writable that's already backed by a
workflow server stream with its (runId, name). When the external
reducer recognizes those tags during dehydration, it bridges bytes
straight from the new child-side server stream to the original server
stream instead of piping through the user's writable. That leaves the
producer-side serialize transform (installed once by the child's step
reviver) as the only framing layer in the chain.

* fix(core): forward (runId, name) when a tagged WritableStream crosses start()

Replaces the previous in-process bridge with first-class writable
forwarding at the descriptor level. When a parent workflow's
getWritable() handle is passed as an argument to a child workflow,
the dehydrated descriptor now carries the original (runId, name).
The child run's step-side reviver opens the writable against the
parent's server stream directly and resolves the parent run's
encryption key (encrypt-only) via getEncryptionKeyForRun.

This removes the architectural limitation that the bridge could
only stay alive for the duration of the parent step process — on
Vercel that capped forwarding at ~15 minutes regardless of the
child run's lifetime, dropping any writes the child made after the
parent step process exited.

importKey() now accepts a usages parameter, defaulting to
['encrypt', 'decrypt']. The cross-run forwarding path imports with
['encrypt'] only so a compromised child run cannot decrypt any
existing data on the parent's stream — only contribute new writes.

* test: rename writable-forwarded workflows and cover step-context getWritable()

Addresses PR review:

- Rename writableForwardedToChildChildWorkflow → writableForwardedChildWorkflow
  (drops the duplicated 'Child' segment).
- Split writableForwardedToChildWorkflow into two variants covered by a
  test.each: writableForwardedFromWorkflowWorkflow (workflow-context
  getWritable, the original test) and writableForwardedFromStepWorkflow
  (step-context getWritable passed directly into start() from the same
  step that called getWritable()).
- Terser changeset description.

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-06-01 12:10:36 -07:00
..
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00
2025-10-23 12:07:52 +03:00

Workflows Example App

Local Dev

$ vc link --repo --yes --scope uncurated-tests