Skip to content

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Nov 11, 2025

Overview

This PR adds support for serializing step function references when they are passed as arguments between steps within a workflow context. Step functions can now be passed to other step functions and properly serialized/deserialized through the workflow runtime.

Problem

Previously, when a step function reference was passed as an argument to another step function (e.g., await stepWithStepFunctionArg(doubleNumber)), the serialization layer would fail because functions are not serializable by default. This prevented workflows from accepting and invoking step functions as parameters.

Solution

This implementation introduces a custom serialization mechanism for step function references:

1. Symbol-based Function Marking (Compiler Level)

  • Added STEP_FUNCTION_NAME_SYMBOL constant to mark step functions with their unique identifier
  • SWC compiler plugin now marks both regular and arrow function step declarations with this symbol:
    • export async function step() {}
    • export const stepArrow = async () => {}

2. Custom Reducer for Serialization

  • When a step function is serialized, the reducer detects the STEP_FUNCTION_NAME_SYMBOL and returns the step name string instead of trying to serialize the function itself
  • This converts function references into simple string representations that can be passed through devalue

3. Custom Reviver for Deserialization

  • When deserializing, the reviver looks up the step name string using getStepFunction() to retrieve the registered function reference
  • If the step function cannot be found, it throws a helpful error message

4. Enhanced devalue Library

  • Updated devalue to check custom reducers before rejecting functions
  • This allows our custom serialization logic to handle functions that would normally be rejected

Changes

Core Serialization (packages/core/src/)

  • symbols.ts: Added STEP_FUNCTION_NAME_SYMBOL export
  • serialization.ts:
    • Added StepFunction reducer and reviver for handling step function serialization
    • Improved defensive check for empty string step names
    • Exported getWorkflowReducers and getWorkflowRevivers
  • schemas.ts: Updated Serializable type to include step function type
  • serialization.test.ts: Added comprehensive unit tests for step function serialization

SWC Plugin (packages/swc-plugin-workflow/transform/)

  • lib.rs: Enhanced to mark both regular and arrow function step declarations
    • Added handling for Decl::Var cases (arrow functions and function expressions)
    • Generates Object.defineProperty() calls for all step functions with STEP_FUNCTION_NAME_SYMBOL
  • Test Fixtures: Updated all output files to reflect step function marking

E2E Tests (packages/core/e2e/)

  • Added stepFunctionPassingWorkflow test case
  • Verifies step functions can be passed as arguments and invoked correctly
  • Tests the full serialization/deserialization round-trip

Example Workflows (workbench/example/)

  • Added test workflows: stepWithStepFunctionArg, doubleNumber, stepFunctionPassingWorkflow
  • Demonstrates passing step functions as arguments

Testing

All SWC Plugin Tests Pass (36 tests)

  • Workflow mode tests
  • Step mode tests
  • Client mode tests
  • Arrow function marking verified

Unit Tests Added

  • Step function detection with symbol
  • Reducer serialization
  • Reviver deserialization
  • Error handling for missing step functions

E2E Tests

  • Step function passing across boundaries
  • Full workflow execution with step function arguments

Usage Example

// Define a step that accepts another step function as a parameter
async function executeWithStep(stepFn: (x: number) => Promise<number>) {
  'use step';
  const result = await stepFn(10);
  return result * 2;
}

// Define a step to pass
async function doubleNumber(x: number) {
  'use step';
  return x * 2;
}

// Use in a workflow - now works! ✨
export async function myWorkflow() {
  'use workflow';
  // Pass the step function reference to another step
  const result = await executeWithStep(doubleNumber);
  return result; // 40 (doubleNumber(10) = 20, then * 2 = 40)
}

Technical Details

Architecture

  1. Compile Time: SWC plugin marks step functions with Symbol.for('WORKFLOW_STEP_FUNCTION_NAME')
  2. Serialization: Custom reducer detects the symbol and converts to step name string
  3. Storage: Step name is stored as a simple string in the serialized data
  4. Deserialization: Custom reviver looks up the step name and returns the function reference
  5. Runtime: Step function is available in the receiving step and can be invoked

Benefits

  • ✅ Type-safe: Step functions maintain their type signatures
  • ✅ Efficient: Only step names are serialized (not function code)
  • ✅ Extensible: Works with any step function declaration style
  • ✅ Backward compatible: Existing workflows unaffected

Breaking Changes

None. This is a purely additive feature.

Notes

  • The feature requires the updated devalue library that allows custom reducers for functions
  • Step functions must be properly registered at runtime for deserialization to work
  • Works with both regular function declarations and arrow functions

@changeset-bot
Copy link

changeset-bot bot commented Nov 11, 2025

🦋 Changeset detected

Latest commit: bc59732

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@workflow/swc-plugin Patch
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/sveltekit Patch
@workflow/web-shared Patch
workflow Patch
@workflow/world-testing Patch
@workflow/nuxt Patch
@workflow/ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Nov 11, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Comment Nov 13, 2025 1:47am
example-nextjs-workflow-webpack Ready Ready Preview Comment Nov 13, 2025 1:47am
example-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workbench-hono-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workbench-nitro-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workbench-nuxt-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workbench-sveltekit-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workbench-vite-workflow Ready Ready Preview Comment Nov 13, 2025 1:47am
workflow-docs Ready Ready Preview Comment Nov 13, 2025 1:47am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants