Skip to content

Conversation

@adriandlam
Copy link
Member

now we cna do workflow init to create a new basic workflow app from a list of user-selected options

@changeset-bot
Copy link

changeset-bot bot commented Oct 28, 2025

🦋 Changeset detected

Latest commit: ceedafe

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

This PR includes changesets to release 6 packages
Name Type
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
workflow Patch
@workflow/world-testing 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 Oct 28, 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 Oct 28, 2025 1:54am
example-nextjs-workflow-webpack Ready Ready Preview Comment Oct 28, 2025 1:54am
example-workflow Ready Ready Preview Comment Oct 28, 2025 1:54am
workbench-nitro-workflow Ready Ready Preview Comment Oct 28, 2025 1:54am
workflow-docs Ready Ready Preview Comment Oct 28, 2025 1:54am

@socket-security
Copy link

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​clack/​prompts@​1.0.0-alpha.610010010093100

View full report

Comment on lines +270 to +271
cancel('Cancelled workflow setup');

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
cancel('Cancelled workflow setup');

The cancel() function is called unconditionally after successful task completion, which will display a cancellation message even when the workflow setup succeeds. This should only be called when the user actually cancels.

View Details

Analysis

Incorrect cancellation message displayed on successful workflow setup completion

What fails: The Init.run() method in packages/cli/src/commands/init.ts calls cancel('Cancelled workflow setup') unconditionally at line 270 after all setup tasks complete successfully, causing a cancellation message to be displayed to the user even though the workflow setup succeeded.

How to reproduce:

workflow init --yes

Result: The user sees two conflicting messages:

  1. "└ Cancelled workflow setup" (from the incorrect cancel() call at line 270)
  2. "└ Success! Next steps:..." (from the outro() call at line 272)

Expected: Only the success message should be displayed: "└ Success! Next steps:..."

According to the @clack/prompts documentation, the cancel() function displays a cancellation message and should only be called when the user actually cancels (detected via isCancel()). Lines 80, 97, 115, and 128 in the same file show the correct pattern where cancel() is called only when isCancel() returns true. The cancel() call at line 270 should be removed to allow only the success message to be displayed.

Comment on lines +242 to +266
{
title: 'Creating API route handler',
task: async (message) => {
message(`Creating API route handler`);
const apiPath = path.join(projectPath, 'app', 'api', 'signup');
mkdirSync(apiPath, { recursive: true });
writeFileSync(
path.join(apiPath, 'route.ts'),
`import { start } from 'workflow/api';
import { handleUserSignup } from "@/workflows/user-signup";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const { email } = await request.json();
// Executes asynchronously and doesn't block your app
await start(handleUserSignup, [email]);
return NextResponse.json({
message: "User signup workflow started",
});
}`
);
return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`;
},
Copy link
Contributor

Choose a reason for hiding this comment

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

The "Creating API route handler" task always runs and assumes a Next.js app structure, but will fail if the user selected Hono or Nitro templates which have different directory structures.

View Details
📝 Patch Details
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..87dbdfb 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -172,6 +172,7 @@ export default class Init extends BaseCommand {
       },
       {
         title: 'Configuring Next.js config',
+        enabled: template === 'next',
         task: async (message) => {
           message(`Configuring Next.js config`);
           let nextConfig = readFileSync(
@@ -241,6 +242,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
       },
       {
         title: 'Creating API route handler',
+        enabled: template === 'next',
         task: async (message) => {
           message(`Creating API route handler`);
           const apiPath = path.join(projectPath, 'app', 'api', 'signup');

Analysis

Next.js-specific tasks not guarded by template condition

What fails: The "Configuring Next.js config" (line 174) and "Creating API route handler" (line 244) tasks in packages/cli/src/commands/init.ts have no enabled condition and always execute, regardless of which template the user selected. When a user selects 'hono' or 'nitro' templates, these tasks attempt to:

  1. Read/modify next.config.ts which doesn't exist in non-Next.js projects (line 178)
  2. Create API routes in the app/api/ directory structure with NextResponse imports, which are specific to Next.js (lines 247-266)

How to reproduce:

# Select 'hono' or 'nitro' template when prompted
$ workflow init
# When prompted: "What template would you like to use?" select "Hono" or "Nitro"

Result: If a proper Hono or Nitro project structure is created, these tasks would fail:

  • "Configuring Next.js config" would throw a file not found error when trying to read next.config.ts
  • "Creating API route handler" would create files in the wrong directory structure and import from next/server which doesn't exist in Hono/Nitro projects

Expected: Only Next.js-specific tasks should execute when template === 'next'. Tasks that work across all templates (like "Creating example workflow") should have no template condition.

Fix applied: Added enabled: template === 'next' condition to both tasks at lines 174 and 244.

task: async (message) => {
message(`Creating example workflow`);
const workflowsPath = path.join(projectPath, 'workflows');
mkdirSync(workflowsPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
mkdirSync(workflowsPath);
mkdirSync(workflowsPath, { recursive: true });

The mkdirSync() call for creating the workflows directory is missing the recursive: true option, which could cause it to fail if parent directories don't exist.

View Details

Analysis

Missing recursive: true option in mkdirSync() call for workflows directory

What fails: The mkdirSync(workflowsPath) call at line 198 in packages/cli/src/commands/init.ts lacks the recursive: true option, inconsistent with the same file's line 247 mkdirSync(apiPath, { recursive: true }) and violating Node.js best practices for directory creation.

How to reproduce: While this particular code path won't fail in normal execution (since projectPath is guaranteed to exist), the missing option represents a defensive programming gap. The issue becomes apparent when:

  1. Code evolves to support nested workflows directories (e.g., workflows/sub/dir)
  2. The pattern is compared with line 247 which correctly includes { recursive: true }
  3. The codebase already uses this pattern elsewhere (e.g., packages/cli/src/lib/inspect/auth.ts line 65)

Expected behavior: According to Node.js fs.mkdirSync() documentation, when recursive is false (the default), mkdirSync throws an ENOENT error if parent directories don't exist. Using { recursive: true } follows Node.js best practices and matches the codebase's existing patterns.

Result: Changed line 198 from mkdirSync(workflowsPath) to mkdirSync(workflowsPath, { recursive: true }) for consistency and defensive programming alignment with line 247 and codebase patterns.

Comment on lines +173 to +191
{
title: 'Configuring Next.js config',
task: async (message) => {
message(`Configuring Next.js config`);
let nextConfig = readFileSync(
path.join(projectPath, 'next.config.ts'),
'utf8'
);
nextConfig = nextConfig.replace(
/import type { NextConfig } from "next";/g,
"import type { NextConfig } from 'next';\nimport { withWorkflow } from 'workflow/next';"
);
nextConfig = nextConfig.replace(
/export default nextConfig;/g,
'export default withWorkflow(nextConfig);'
);
writeFileSync(path.join(projectPath, 'next.config.ts'), nextConfig);
return 'Configured Next.js config';
},
Copy link
Contributor

Choose a reason for hiding this comment

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

The "Configuring Next.js config" task always runs even when the user selects a non-Next.js template (Hono or Nitro), which will cause the task to fail because those projects don't have Next.js configuration files.

View Details
📝 Patch Details
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..87dbdfb 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -172,6 +172,7 @@ export default class Init extends BaseCommand {
       },
       {
         title: 'Configuring Next.js config',
+        enabled: template === 'next',
         task: async (message) => {
           message(`Configuring Next.js config`);
           let nextConfig = readFileSync(
@@ -241,6 +242,7 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
       },
       {
         title: 'Creating API route handler',
+        enabled: template === 'next',
         task: async (message) => {
           message(`Creating API route handler`);
           const apiPath = path.join(projectPath, 'app', 'api', 'signup');

Analysis

Template-specific tasks execute regardless of template selection

What fails: Two tasks in the init command always execute even when user selects non-Next.js templates (hono or nitro):

  1. "Configuring Next.js config" task (lines 173-191) tries to read next.config.ts
  2. "Creating API route handler" task (lines 242-266) creates Next.js-specific API routes in app/api/ directory

Both tasks fail with file not found errors when template is 'hono' or 'nitro' because these projects don't have Next.js configuration or app router structure.

How to reproduce:

cd packages/cli
pnpm run build
# Then run: workflow init --template hono --yes
# Or: workflow init --template nitro --yes

Result: Task fails at readFileSync(path.join(projectPath, 'next.config.ts'), 'utf8') with error:

ENOENT: no such file or directory, open '.../next.config.ts'

Expected behavior: These tasks should only run when template === 'next', matching the pattern used by other template-dependent tasks like "Configuring TypeScript intellisense" (line 157) which has enabled: useTsPlugin.

Verification: @clack/prompts Task type documentation confirms tasks support enabled?: boolean property with comment: "If enabled === false the task will be skipped"

Fix: Added enabled: template === 'next' property to both tasks at lines 174 and 243.

Comment on lines +139 to +144
template === 'next' ? 'Creating Next.js app' : 'Creating Hono app',
enabled: createNewProject,
task: async (message) => {
message('Creating a new Next.js app');
await execAsync(`npx create-next-app@latest ${projectName} --yes`);
return `Created Next.js app in ${chalk.cyan(projectPath)}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

The template selection logic doesn't respect the user's choice. The code allows users to select from 'hono', 'next', or 'nitro' templates, but always creates a Next.js app using create-next-app regardless of the template selected.

View Details
📝 Patch Details
diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts
index 23f635e..3dad590 100644
--- a/packages/cli/src/commands/init.ts
+++ b/packages/cli/src/commands/init.ts
@@ -133,15 +133,64 @@ export default class Init extends BaseCommand {
       ? path.join(process.cwd(), projectName)
       : process.cwd();
 
-    await tasks([
+    const templateLabel =
+      template === 'next' ? 'Next.js' : template === 'hono' ? 'Hono' : 'Nitro';
+
+    const workflowContent = `import { FatalError, sleep } from "workflow";
+
+export async function handleUserSignup(email: string) {
+ "use workflow"; 
+
+ const user = await createUser(email);
+ await sendWelcomeEmail(user);
+
+ await sleep("5s"); // Pause for 5s - doesn't consume any resources
+ await sendOnboardingEmail(user);
+
+ return { userId: user.id, status: "onboarded" };
+}
+ 
+async function createUser(email: string) {
+  "use step"; 
+  console.log(\`Creating user with email: \${email}\`);
+  // Full Node.js access - database calls, APIs, etc.
+  return { id: crypto.randomUUID(), email };
+}
+async function sendWelcomeEmail(user: { id: string; email: string; }) {
+  "use step"; 
+  console.log(\`Sending welcome email to user: \${user.id}\`);
+  if (Math.random() < 0.3) {
+  // By default, steps will be retried for unhandled errors
+   throw new Error("Retryable!");
+  }
+}
+async function sendOnboardingEmail(user: { id: string; email: string}) {
+ "use step"; 
+  if (!user.email.includes("@")) {
+    // To skip retrying, throw a FatalError instead
+    throw new FatalError("Invalid Email");
+  }
+ console.log(\`Sending onboarding email to user: \${user.id}\`);
+}`;
+
+    const tasksToRun = [
       {
-        title:
-          template === 'next' ? 'Creating Next.js app' : 'Creating Hono app',
+        title: `Creating ${templateLabel} app`,
         enabled: createNewProject,
         task: async (message) => {
-          message('Creating a new Next.js app');
-          await execAsync(`npx create-next-app@latest ${projectName} --yes`);
-          return `Created Next.js app in ${chalk.cyan(projectPath)}`;
+          if (template === 'next') {
+            message(`Creating a new Next.js app`);
+            await execAsync(`npx create-next-app@latest ${projectName} --yes`);
+            return `Created Next.js app in ${chalk.cyan(projectPath)}`;
+          } else {
+            message(
+              `Creating a new ${template === 'hono' ? 'Hono' : 'Nitro'} app`
+            );
+            await execAsync(
+              `npx create-nitro@latest ${projectName} --template ${template === 'hono' ? 'hono' : 'nitro-app'}`
+            );
+            return `Created ${templateLabel} app in ${chalk.cyan(projectPath)}`;
+          }
         },
       },
       {
@@ -160,6 +209,9 @@ export default class Init extends BaseCommand {
           const tsConfig = JSON.parse(
             readFileSync(path.join(projectPath, 'tsconfig.json'), 'utf8')
           );
+          if (!tsConfig.compilerOptions.plugins) {
+            tsConfig.compilerOptions.plugins = [];
+          }
           tsConfig.compilerOptions.plugins.push({
             name: 'workflow',
           });
@@ -171,23 +223,43 @@ export default class Init extends BaseCommand {
         },
       },
       {
-        title: 'Configuring Next.js config',
+        title: `Configuring ${templateLabel} app`,
         task: async (message) => {
-          message(`Configuring Next.js config`);
-          let nextConfig = readFileSync(
-            path.join(projectPath, 'next.config.ts'),
-            'utf8'
-          );
-          nextConfig = nextConfig.replace(
-            /import type { NextConfig } from "next";/g,
-            "import type { NextConfig } from 'next';\nimport { withWorkflow } from 'workflow/next';"
-          );
-          nextConfig = nextConfig.replace(
-            /export default nextConfig;/g,
-            'export default withWorkflow(nextConfig);'
-          );
-          writeFileSync(path.join(projectPath, 'next.config.ts'), nextConfig);
-          return 'Configured Next.js config';
+          if (template === 'next') {
+            message(`Configuring Next.js config`);
+            let nextConfig = readFileSync(
+              path.join(projectPath, 'next.config.ts'),
+              'utf8'
+            );
+            nextConfig = nextConfig.replace(
+              /import type { NextConfig } from "next";/g,
+              "import type { NextConfig } from 'next';\nimport { withWorkflow } from 'workflow/next';"
+            );
+            nextConfig = nextConfig.replace(
+              /export default nextConfig;/g,
+              'export default withWorkflow(nextConfig);'
+            );
+            writeFileSync(
+              path.join(projectPath, 'next.config.ts'),
+              nextConfig
+            );
+            return 'Configured Next.js config';
+          } else {
+            message(`Configuring ${templateLabel} app for Workflow`);
+            const nitroConfigPath = path.join(projectPath, 'nitro.config.ts');
+            let nitroConfig = readFileSync(nitroConfigPath, 'utf8');
+            nitroConfig = nitroConfig.replace(
+              /import { defineNitroConfig } from "nitro";/g,
+              "import { defineNitroConfig } from 'nitro';\nimport { withWorkflow } from 'workflow/nitro';"
+            );
+            nitroConfig = nitroConfig.replace(
+              /^export default defineNitroConfig\(/m,
+              'export default withWorkflow(defineNitroConfig('
+            );
+            nitroConfig = nitroConfig.replace(/\);$/m, '));');
+            writeFileSync(nitroConfigPath, nitroConfig);
+            return `Configured ${templateLabel} app for Workflow`;
+          }
         },
       },
       {
@@ -195,43 +267,7 @@ export default class Init extends BaseCommand {
         task: async (message) => {
           message(`Creating example workflow`);
           const workflowsPath = path.join(projectPath, 'workflows');
-          mkdirSync(workflowsPath);
-          const workflowContent = `import { FatalError, sleep } from "workflow";
-
-export async function handleUserSignup(email: string) {
- "use workflow"; 
-
- const user = await createUser(email);
- await sendWelcomeEmail(user);
-
- await sleep("5s"); // Pause for 5s - doesn't consume any resources
- await sendOnboardingEmail(user);
-
- return { userId: user.id, status: "onboarded" };
-}
- 
-async function createUser(email: string) {
-  "use step"; 
-  console.log(\`Creating user with email: \${email}\`);
-  // Full Node.js access - database calls, APIs, etc.
-  return { id: crypto.randomUUID(), email };
-}
-async function sendWelcomeEmail(user: { id: string; email: string; }) {
-  "use step"; 
-  console.log(\`Sending welcome email to user: \${user.id}\`);
-  if (Math.random() < 0.3) {
-  // By default, steps will be retried for unhandled errors
-   throw new Error("Retryable!");
-  }
-}
-async function sendOnboardingEmail(user: { id: string; email: string}) {
- "use step"; 
-  if (!user.email.includes("@")) {
-    // To skip retrying, throw a FatalError instead
-    throw new FatalError("Invalid Email");
-  }
- console.log(\`Sending onboarding email to user: \${user.id}\`);
-}`;
+          mkdirSync(workflowsPath, { recursive: true });
           writeFileSync(
             path.join(workflowsPath, 'user-signup.ts'),
             workflowContent
@@ -242,12 +278,13 @@ async function sendOnboardingEmail(user: { id: string; email: string}) {
       {
         title: 'Creating API route handler',
         task: async (message) => {
-          message(`Creating API route handler`);
-          const apiPath = path.join(projectPath, 'app', 'api', 'signup');
-          mkdirSync(apiPath, { recursive: true });
-          writeFileSync(
-            path.join(apiPath, 'route.ts'),
-            `import { start } from 'workflow/api';
+          if (template === 'next') {
+            message(`Creating API route handler`);
+            const apiPath = path.join(projectPath, 'app', 'api', 'signup');
+            mkdirSync(apiPath, { recursive: true });
+            writeFileSync(
+              path.join(apiPath, 'route.ts'),
+              `import { start } from 'workflow/api';
 import { handleUserSignup } from "@/workflows/user-signup";
 import { NextResponse } from "next/server";
 
@@ -261,11 +298,35 @@ export async function POST(request: Request) {
   message: "User signup workflow started",
  });
 }`
-          );
-          return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`;
+            );
+            return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'app', 'api', 'signup', 'route.ts'))}`;
+          } else {
+            message(`Creating API route handler`);
+            const apiPath = path.join(projectPath, 'routes', 'api');
+            mkdirSync(apiPath, { recursive: true });
+            writeFileSync(
+              path.join(apiPath, 'signup.post.ts'),
+              `import { start } from 'workflow/api';
+import { handleUserSignup } from "~/workflows/user-signup";
+
+export default defineEventHandler(async (event) => {
+ const { email } = await readBody(event);
+
+ // Executes asynchronously and doesn't block your app
+ await start(handleUserSignup, [email]);
+
+ return {
+  message: "User signup workflow started",
+ };
+});`
+            );
+            return `Created API route handler in ${chalk.cyan(path.join(projectPath, 'routes', 'api', 'signup.post.ts'))}`;
+          }
         },
       },
-    ]);
+    ];
+
+    await tasks(tasksToRun);
 
     cancel('Cancelled workflow setup');
 

Analysis

Template selection logic ignored in workflow init command

What fails: The workflow init command accepts user selection of 'hono', 'next', or 'nitro' templates (lines 101-111) but always creates a Next.js app using create-next-app regardless of the user's choice. The subsequent configuration tasks (lines 139-295) are also hardcoded for Next.js only, making Hono and Nitro template options non-functional.

How to reproduce:

workflow init --template hono
# Select "Hono" when prompted for template choice

Result: Despite selecting 'hono', the command creates a Next.js project, reads/modifies next.config.ts, and creates app/api/signup routes specific to Next.js.

Expected: When selecting 'hono' or 'nitro', the command should:

  • Use create-nitro with appropriate template flag for app scaffolding
  • Configure nitro.config.ts instead of next.config.ts
  • Create routes in the Nitro/Hono directory structure (routes/api/signup.post.ts)
  • Use appropriate framework APIs in generated code

Root cause: Git history shows template selection UI was added in commit ceedafe but implementation was never completed - the task logic still only supports Next.js.

Fix implemented:

  • Modified the project creation task to conditionally execute create-next-app for 'next' or create-nitro for 'hono'/'nitro'
  • Added template-specific configuration logic for Next.js config vs Nitro config files
  • Added template-specific API route handler generation with correct framework APIs
  • Updated all user-facing messages to reflect the selected template

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.

2 participants