From 89c6852d8370154d3c8b005a5c2ccc4916e734de Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 7 Apr 2026 23:10:22 -0700 Subject: [PATCH 1/5] Add features.encryption to WorkflowMetadata Expose a `features` object on `getWorkflowMetadata()` so library authors can detect at runtime whether encryption is enabled for the current workflow run, allowing them to conditionally handle sensitive data serialization. --- .changeset/features-encryption-metadata.md | 5 +++++ .../workflow/get-workflow-metadata.mdx | 22 +++++++++++++++++++ packages/core/e2e/e2e.test.ts | 10 +++++++++ packages/core/src/runtime/step-handler.ts | 3 ++- packages/core/src/serialization.test.ts | 1 + .../core/src/step/writable-stream.test.ts | 4 ++++ packages/core/src/workflow.ts | 1 + .../src/workflow/get-workflow-metadata.ts | 12 ++++++++++ workbench/example/workflows/99_e2e.ts | 1 + 9 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/features-encryption-metadata.md diff --git a/.changeset/features-encryption-metadata.md b/.changeset/features-encryption-metadata.md new file mode 100644 index 0000000000..10017db4a7 --- /dev/null +++ b/.changeset/features-encryption-metadata.md @@ -0,0 +1,5 @@ +--- +"@workflow/core": patch +--- + +Add `features.encryption` to `WorkflowMetadata` returned by `getWorkflowMetadata()` diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index 23b3573a2d..fb6c706ad5 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -13,6 +13,7 @@ You may want to use this function when you need to: * Log workflow run IDs * Access timing information of a workflow +* Detect whether encryption is enabled for the current run If you need to access step context, take a look at [`getStepMetadata`](/docs/api-reference/workflow/get-step-metadata). @@ -29,6 +30,27 @@ async function testWorkflow() { } ``` +### Detecting Encryption + +The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to conditionally handle sensitive data: + +```typescript lineNumbers +import { getWorkflowMetadata } from "workflow" + +async function processUserData(data: SensitiveData) { + "use step" + + const { features } = getWorkflowMetadata() // [!code highlight] + + if (!features.encryption) { // [!code highlight] + // Redact sensitive fields when encryption is not enabled + data = redact(data) + } + + return data +} +``` + ## API Signature ### Parameters diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 6260a5cfdb..6eab1781f9 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -596,6 +596,16 @@ describe('e2e', () => { ); expect(returnValue.stepMetadata.url).toBeUndefined(); + // workflow context should have features and stepMetadata shouldn't + expect(returnValue.workflowMetadata.features).toBeDefined(); + expect(typeof returnValue.workflowMetadata.features.encryption).toBe( + 'boolean' + ); + expect(returnValue.innerWorkflowMetadata.features).toStrictEqual( + returnValue.workflowMetadata.features + ); + expect(returnValue.stepMetadata.features).toBeUndefined(); + // workflow context shouldn't have stepId, stepStartedAt, or attempt expect(returnValue.workflowMetadata.stepId).toBeUndefined(); expect(returnValue.workflowMetadata.stepStartedAt).toBeUndefined(); diff --git a/packages/core/src/runtime/step-handler.ts b/packages/core/src/runtime/step-handler.ts index 822f632a92..292c20a042 100644 --- a/packages/core/src/runtime/step-handler.ts +++ b/packages/core/src/runtime/step-handler.ts @@ -34,6 +34,7 @@ import { getErrorStack, normalizeUnknownError, } from '../types.js'; +import { MAX_QUEUE_DELIVERIES } from './constants.js'; import { getQueueOverhead, getWorkflowQueueName, @@ -42,7 +43,6 @@ import { queueMessage, withHealthCheck, } from './helpers.js'; -import { MAX_QUEUE_DELIVERIES } from './constants.js'; import { getWorld, getWorldHandlers } from './world.js'; const DEFAULT_STEP_MAX_RETRIES = 3; @@ -510,6 +510,7 @@ const stepHandler = getWorldHandlers().createQueueHandler( url: isVercel ? `https://${process.env.VERCEL_URL}` : `http://localhost:${port ?? 3000}`, + features: { encryption: !!encryptionKey }, }, ops, closureVars: hydratedInput.closureVars, diff --git a/packages/core/src/serialization.test.ts b/packages/core/src/serialization.test.ts index b2a807a1c2..3fcab70d48 100644 --- a/packages/core/src/serialization.test.ts +++ b/packages/core/src/serialization.test.ts @@ -2264,6 +2264,7 @@ describe('step function serialization', () => { workflowRunId: 'test-run', workflowStartedAt: new Date(), url: 'http://localhost:3000', + features: { encryption: false }, }, ops: [], }, diff --git a/packages/core/src/step/writable-stream.test.ts b/packages/core/src/step/writable-stream.test.ts index b49e6cff52..8df7341a11 100644 --- a/packages/core/src/step/writable-stream.test.ts +++ b/packages/core/src/step/writable-stream.test.ts @@ -36,6 +36,8 @@ describe('step-level getWritable', () => { workflowName: 'test-workflow', workflowRunId: 'wrun_test123', workflowStartedAt: new Date(), + url: 'http://localhost:3000', + features: { encryption: false }, }, ops, encryptionKey: undefined, @@ -82,6 +84,8 @@ describe('step-level getWritable', () => { workflowName: 'test-workflow', workflowRunId: 'wrun_test123', workflowStartedAt: new Date(), + url: 'http://localhost:3000', + features: { encryption: false }, }, ops, encryptionKey: undefined, diff --git a/packages/core/src/workflow.ts b/packages/core/src/workflow.ts index fdbe341b29..1a16ecf7a0 100644 --- a/packages/core/src/workflow.ts +++ b/packages/core/src/workflow.ts @@ -212,6 +212,7 @@ export async function runWorkflow( workflowRunId: workflowRun.runId, workflowStartedAt: new vmGlobalThis.Date(+startedAt), url, + features: { encryption: !!encryptionKey }, }; // @ts-expect-error - `@types/node` says symbol is not valid, but it does work diff --git a/packages/core/src/workflow/get-workflow-metadata.ts b/packages/core/src/workflow/get-workflow-metadata.ts index 50ef0bca47..c89bb06cd2 100644 --- a/packages/core/src/workflow/get-workflow-metadata.ts +++ b/packages/core/src/workflow/get-workflow-metadata.ts @@ -18,6 +18,18 @@ export interface WorkflowMetadata { * The URL where the workflow can be triggered. */ url: string; + + /** + * Feature flags indicating which capabilities are active for this workflow run. + */ + features: { + /** + * Whether encryption is enabled for this workflow run. + * When `true`, step inputs, outputs, and other serialized data + * are encrypted at rest. + */ + encryption: boolean; + }; } export const WORKFLOW_CONTEXT_SYMBOL = diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index cb86ae83a1..36811e8eed 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -246,6 +246,7 @@ export async function workflowAndStepMetadataWorkflow() { workflowRunId: workflowMetadata.workflowRunId, workflowStartedAt: workflowMetadata.workflowStartedAt, url: workflowMetadata.url, + features: workflowMetadata.features, }, stepMetadata, innerWorkflowMetadata, From dc91e65b990a01941c5ead656073fa0464233787 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 00:07:33 -0700 Subject: [PATCH 2/5] Improve docs example: show workflow-level encryption check Step inputs are serialized before the step body runs, so the encryption check should happen in the workflow function before passing data to steps. --- .../workflow/get-workflow-metadata.mdx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index fb6c706ad5..bcd7b8a693 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -32,22 +32,28 @@ async function testWorkflow() { ### Detecting Encryption -The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to conditionally handle sensitive data: +The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to conditionally handle sensitive data before passing it to steps: ```typescript lineNumbers import { getWorkflowMetadata } from "workflow" -async function processUserData(data: SensitiveData) { +async function storeUserData(data: UserData) { "use step" + await db.insert(data) +} + +async function processUser(data: UserData) { + "use workflow" const { features } = getWorkflowMetadata() // [!code highlight] if (!features.encryption) { // [!code highlight] - // Redact sensitive fields when encryption is not enabled + // Redact sensitive fields before passing to steps, + // since step inputs are serialized to the event log data = redact(data) } - return data + await storeUserData(data) } ``` From bed3fcd3f3c6affc12b77a1d5c6661744f115afa Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 00:08:40 -0700 Subject: [PATCH 3/5] Fix docs example: show encryption check on step return value Step return values are serialized to the event log after the step body runs, so this is where the check is actually useful - controlling what data gets persisted as output. --- .../workflow/get-workflow-metadata.mdx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index bcd7b8a693..2d5aaf637c 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -32,28 +32,25 @@ async function testWorkflow() { ### Detecting Encryption -The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to conditionally handle sensitive data before passing it to steps: +The `features` object indicates which capabilities are active for the current run. Library authors can use `features.encryption` to control whether sensitive data is included in step return values, which are serialized to the event log: ```typescript lineNumbers import { getWorkflowMetadata } from "workflow" -async function storeUserData(data: UserData) { +async function fetchUserProfile(userId: string) { "use step" - await db.insert(data) -} - -async function processUser(data: UserData) { - "use workflow" const { features } = getWorkflowMetadata() // [!code highlight] + const profile = await db.getUserProfile(userId) if (!features.encryption) { // [!code highlight] - // Redact sensitive fields before passing to steps, - // since step inputs are serialized to the event log - data = redact(data) + // Omit sensitive fields from the return value, + // since it will be stored unencrypted in the event log + const { ssn, ...safe } = profile + return safe } - await storeUserData(data) + return profile } ``` From 348f335e6e329630090b6190e423952695cf437d Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 00:12:29 -0700 Subject: [PATCH 4/5] Fix docs typecheck: declare external function in code sample --- .../docs/api-reference/workflow/get-workflow-metadata.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx index 2d5aaf637c..7fc920997b 100644 --- a/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx +++ b/docs/content/docs/api-reference/workflow/get-workflow-metadata.mdx @@ -37,11 +37,13 @@ The `features` object indicates which capabilities are active for the current ru ```typescript lineNumbers import { getWorkflowMetadata } from "workflow" +declare function getUserProfile(userId: string): Promise<{ name: string; ssn: string }>; // @setup + async function fetchUserProfile(userId: string) { "use step" const { features } = getWorkflowMetadata() // [!code highlight] - const profile = await db.getUserProfile(userId) + const profile = await getUserProfile(userId) if (!features.encryption) { // [!code highlight] // Omit sensitive fields from the return value, From a952244325f16ce05a417675a18249daf543093e Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 8 Apr 2026 17:07:01 -0700 Subject: [PATCH 5/5] minor --- .changeset/features-encryption-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/features-encryption-metadata.md b/.changeset/features-encryption-metadata.md index 10017db4a7..c70fdbc0d8 100644 --- a/.changeset/features-encryption-metadata.md +++ b/.changeset/features-encryption-metadata.md @@ -1,5 +1,5 @@ --- -"@workflow/core": patch +"@workflow/core": minor --- Add `features.encryption` to `WorkflowMetadata` returned by `getWorkflowMetadata()`