Skip to content

Conversation

@TooTallNate
Copy link
Member

@TooTallNate TooTallNate commented Nov 25, 2025

Enhanced the SWC plugin to transform workflow functions in "step" mode, ensuring they throw appropriate errors when called directly.

This allows the start() function to be used within step function to trigger new workflow runs.

What changed?

This PR updates the SWC plugin to properly handle workflow functions in "step" mode:

  • Workflow functions now throw an error when called directly in step mode, with a message instructing users to use start(functionName) instead
  • Fixed object property step functions to include parent function context in their IDs
  • Improved step ID generation to maintain proper hierarchical relationships
  • Added proper workflowId assignments to workflow functions in step mode
  • Ensured nested step functions within workflow functions are correctly processed and hoisted

Why make this change?

Previously, workflow functions in step mode weren't being transformed, which could lead to unexpected behavior when users tried to call them directly in a step function. This change ensures a consistent developer experience by providing clear error messages when workflow functions are called directly in a step function. The workflowId property is attached in "step" mode so that the start() function may be used.

@changeset-bot
Copy link

changeset-bot bot commented Nov 25, 2025

🦋 Changeset detected

Latest commit: 0435e44

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

This PR includes changesets to release 11 packages
Name Type
@workflow/swc-plugin Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/rollup Patch
@workflow/sveltekit 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 25, 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 27, 2025 7:28am
example-nextjs-workflow-webpack Ready Ready Preview Comment Nov 27, 2025 7:28am
example-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-express-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-fastify-workflow Error Error Nov 27, 2025 7:28am
workbench-hono-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-nitro-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-nuxt-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-sveltekit-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workbench-vite-workflow Ready Ready Preview Comment Nov 27, 2025 7:28am
workflow-docs Ready Ready Preview Comment Nov 27, 2025 7:28am

Copy link
Member Author

TooTallNate commented Nov 25, 2025

Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

Default export workflow functions are not being transformed in step mode. They should have their bodies replaced with error throws like exported named workflow functions do.

View Details
📝 Patch Details
diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs
index 5a054fe..b0a5a17 100644
--- a/packages/swc-plugin-workflow/transform/src/lib.rs
+++ b/packages/swc-plugin-workflow/transform/src/lib.rs
@@ -4531,7 +4531,14 @@ impl VisitMut for StepTransform {
 
                                             match self.mode {
                                                 TransformMode::Step => {
-                                                    // Workflow functions are not processed in step mode
+                                                    // In step mode, remove the directive for now
+                                                    // We'll visit children to process nested steps,
+                                                    // then replace the body with throw error afterwards
+                                                    self.remove_use_workflow_directive(
+                                                        &mut fn_expr.function.body,
+                                                    );
+                                                    self.workflow_functions_needing_id
+                                                        .push((name.clone(), fn_expr.function.span));
                                                 }
                                                 TransformMode::Workflow => {
                                                     // In workflow mode, just remove the directive
@@ -5769,7 +5776,12 @@ impl VisitMut for StepTransform {
 
                         match self.mode {
                             TransformMode::Step => {
-                                // Workflow functions are not processed in step mode
+                                // In step mode, remove the directive for now
+                                // We'll visit children to process nested steps,
+                                // then replace the body with throw error afterwards
+                                self.remove_use_workflow_directive(&mut fn_expr.function.body);
+                                self.workflow_functions_needing_id
+                                    .push((const_name.clone(), fn_expr.function.span));
                             }
                             TransformMode::Workflow => {
                                 // In workflow mode, just remove the directive
@@ -5917,6 +5929,50 @@ impl VisitMut for StepTransform {
                 }
 
                 decl.visit_mut_children_with(self);
+
+                // After visiting, process the function again for cleanup
+                if let DefaultDecl::Fn(fn_expr) = &mut decl.decl {
+                    let is_workflow = self.workflow_function_names.contains("default");
+
+                    // In Step mode, replace workflow function body with throw after processing nested steps
+                    if matches!(self.mode, TransformMode::Step) {
+                        if is_workflow {
+                            // Replace workflow function body with error throw
+                            let const_name = self.workflow_export_to_const_name
+                                .get("default")
+                                .map(|s| s.to_string())
+                                .unwrap_or_else(|| "default".to_string());
+                            if let Some(body) = &mut fn_expr.function.body {
+                                let error_msg = format!(
+                                    "You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
+                                    const_name, const_name
+                                );
+                                let error_expr = Expr::New(NewExpr {
+                                    span: DUMMY_SP,
+                                    ctxt: SyntaxContext::empty(),
+                                    callee: Box::new(Expr::Ident(Ident::new(
+                                        "Error".into(),
+                                        DUMMY_SP,
+                                        SyntaxContext::empty(),
+                                    ))),
+                                    args: Some(vec![ExprOrSpread {
+                                        spread: None,
+                                        expr: Box::new(Expr::Lit(Lit::Str(Str {
+                                            span: DUMMY_SP,
+                                            value: error_msg.into(),
+                                            raw: None,
+                                        }))),
+                                    }]),
+                                    type_args: None,
+                                });
+                                body.stmts = vec![Stmt::Throw(ThrowStmt {
+                                    span: DUMMY_SP,
+                                    arg: Box::new(error_expr),
+                                })];
+                            }
+                        }
+                    }
+                }
             }
             _ => {
                 decl.visit_mut_children_with(self);
@@ -5942,7 +5998,12 @@ impl VisitMut for StepTransform {
 
                         match self.mode {
                             TransformMode::Step => {
-                                // Workflow functions are not processed in step mode
+                                // In step mode, remove the directive for now
+                                // We'll visit children to process nested steps,
+                                // then replace the body with throw error afterwards
+                                self.remove_use_workflow_directive(&mut fn_expr.function.body);
+                                self.workflow_functions_needing_id
+                                    .push((const_name.clone(), fn_expr.function.span));
                             }
                             TransformMode::Workflow => {
                                 // In workflow mode, convert to const declaration
@@ -6043,7 +6104,12 @@ impl VisitMut for StepTransform {
 
                         match self.mode {
                             TransformMode::Step => {
-                                // Workflow functions are not processed in step mode
+                                // In step mode, remove the directive for now
+                                // We'll visit children to process nested steps,
+                                // then replace the body with throw error afterwards
+                                self.remove_use_workflow_directive(&mut fn_expr.function.body);
+                                self.workflow_functions_needing_id
+                                    .push((const_name.clone(), fn_expr.function.span));
                             }
                             TransformMode::Workflow => {
                                 // In workflow mode, convert to const declaration
@@ -6137,6 +6203,88 @@ impl VisitMut for StepTransform {
         }
 
         expr.visit_mut_children_with(self);
+
+        // After visiting, process the function again for cleanup
+        if matches!(self.mode, TransformMode::Step) {
+            let is_workflow = self.workflow_function_names.contains("default");
+            if is_workflow {
+                match &mut *expr.expr {
+                    Expr::Fn(fn_expr) => {
+                        // Replace workflow function body with error throw
+                        let const_name = self.workflow_export_to_const_name
+                            .get("default")
+                            .map(|s| s.to_string())
+                            .unwrap_or_else(|| "default".to_string());
+                        if let Some(body) = &mut fn_expr.function.body {
+                            let error_msg = format!(
+                                "You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
+                                const_name, const_name
+                            );
+                            let error_expr = Expr::New(NewExpr {
+                                span: DUMMY_SP,
+                                ctxt: SyntaxContext::empty(),
+                                callee: Box::new(Expr::Ident(Ident::new(
+                                    "Error".into(),
+                                    DUMMY_SP,
+                                    SyntaxContext::empty(),
+                                ))),
+                                args: Some(vec![ExprOrSpread {
+                                    spread: None,
+                                    expr: Box::new(Expr::Lit(Lit::Str(Str {
+                                        span: DUMMY_SP,
+                                        value: error_msg.into(),
+                                        raw: None,
+                                    }))),
+                                }]),
+                                type_args: None,
+                            });
+                            body.stmts = vec![Stmt::Throw(ThrowStmt {
+                                span: DUMMY_SP,
+                                arg: Box::new(error_expr),
+                            })];
+                        }
+                    }
+                    Expr::Arrow(arrow_expr) => {
+                        // Replace arrow body with throw error
+                        let const_name = self.workflow_export_to_const_name
+                            .get("default")
+                            .map(|s| s.to_string())
+                            .unwrap_or_else(|| "default".to_string());
+                        let error_msg = format!(
+                            "You attempted to execute workflow {} function directly. To start a workflow, use start({}) from workflow/api",
+                            const_name, const_name
+                        );
+                        let error_expr = Expr::New(NewExpr {
+                            span: DUMMY_SP,
+                            ctxt: SyntaxContext::empty(),
+                            callee: Box::new(Expr::Ident(Ident::new(
+                                "Error".into(),
+                                DUMMY_SP,
+                                SyntaxContext::empty(),
+                            ))),
+                            args: Some(vec![ExprOrSpread {
+                                spread: None,
+                                expr: Box::new(Expr::Lit(Lit::Str(Str {
+                                    span: DUMMY_SP,
+                                    value: error_msg.into(),
+                                    raw: None,
+                                }))),
+                            }]),
+                            type_args: None,
+                        });
+                        arrow_expr.body = Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
+                            span: DUMMY_SP,
+                            ctxt: SyntaxContext::empty(),
+                            stmts: vec![Stmt::Throw(ThrowStmt {
+                                span: DUMMY_SP,
+                                arg: Box::new(error_expr),
+                            })],
+                        }));
+                    }
+                    _ => {}
+                }
+            }
+        }
     }
 
     fn visit_mut_module_decl(&mut self, decl: &mut ModuleDecl) {

Analysis

Default export workflow functions not transformed in step mode

What fails: Workflow functions exported as default exports (e.g., export default async function defaultWorkflow() { 'use workflow'; ... }) are not having their bodies replaced with error throws in step mode, while named exports and const exports receive this transformation.

How to reproduce:

The issue is evident by comparing test fixture output files. The input file packages/swc-plugin-workflow/transform/tests/fixture/workflow-client-property/input.js contains:

export async function myWorkflow() {
  'use workflow';
  // ...
}

export default async function defaultWorkflow() {
  'use workflow';
  // ...
}

When transformed in step mode, the named export myWorkflow correctly becomes:

export async function myWorkflow() {
    throw new Error("You attempted to execute workflow myWorkflow function directly...");
}

But the default export defaultWorkflow incorrectly remains:

export default async function defaultWorkflow() {
    'use workflow';
    // original body unchanged
}

Expected behavior: The default export should also throw an error in step mode, matching the behavior of named exports. This was the intent of commit ef73dc7 "Apply workflow function transformation in 'step' mode" which updated the test fixture's expected output to show default exports throwing errors.

Root cause: The code in visit_mut_export_default_decl() (line 5771-5773) had Step mode handling that did nothing with a comment stating "Workflow functions are not processed in step mode". This was incorrect - the PR that introduced this function explicitly intended for workflow functions to throw errors in step mode. The named export handler visit_mut_export_decl() correctly implements this transformation, but the default export handler did not.

Fix implemented:

  • Added Step mode handling in visit_mut_export_default_decl to remove the workflow directive and track the function for workflowId assignment
  • Added post-processing after visiting children to replace the function body with an error throw
  • Applied the same fix to visit_mut_export_default_expr for arrow function default exports
  • Applied the same fix to variable declarations with workflow functions in visit_mut_export_decl's Var handling
  • All fixes follow the same pattern used for named function exports

The transformation now properly handles all three forms of default exports:

  1. export default async function name() { 'use workflow'; ... }
  2. export default async () => { 'use workflow'; ... }
  3. Function expressions assigned to default export

All now correctly throw errors in step mode, consistent with named exports and const exports.

Fix on Vercel

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.

3 participants