Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0f62431
Workflows CFG extractor & build plumbing
karthikscale3 Nov 28, 2025
d537846
Adding changeset
karthikscale3 Nov 28, 2025
87a90b8
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 1, 2025
e5f4cd8
Extend manifest.debug.json to include workflow CFG metadata and clean…
karthikscale3 Dec 1, 2025
68b23e0
Extend manifest.debug.json to include workflow CFG metadata and clean…
karthikscale3 Dec 1, 2025
1c45af6
Add unit test coverage for manifest
karthikscale3 Dec 1, 2025
8e21d56
fix
karthikscale3 Dec 2, 2025
b002d81
Add changeset
karthikscale3 Dec 2, 2025
252a0dd
Add changeset
karthikscale3 Dec 2, 2025
8a3f152
Add changeset
karthikscale3 Dec 2, 2025
03cf52c
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 5, 2025
675a989
DCO Remediation Commit for Karthik Kalyanaraman <[email protected]>
karthikscale3 Dec 5, 2025
08f2811
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 5, 2025
a0363a1
fix(builders): extract steps from single-statement control flow bodies
karthikscale3 Dec 5, 2025
2484c01
fix(builders): extract steps from single-statement control flow bodies
karthikscale3 Dec 5, 2025
ef7d741
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 5, 2025
6609b39
Fix imports
karthikscale3 Dec 5, 2025
7437ea3
DCO Remediation Commit for Karthik Kalyanaraman <[email protected]>
karthikscale3 Dec 5, 2025
8208f3c
Merge
karthikscale3 Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/huge-rabbits-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@workflow/world-postgres": patch
"@workflow/world-local": patch
"@workflow/sveltekit": patch
"@workflow/builders": patch
"@workflow/nitro": patch
"@workflow/utils": patch
"@workflow/world": patch
"@workflow/core": patch
"@workflow/next": patch
"@workflow/web": patch
---

Added Control Flow Graph extraction from Workflows and extended manifest.json's schema to incorporate the graph structure into it. Refactored manifest generation to pass manifest as a parameter instead of using instance state. Add e2e tests for manifest validation across all builders.
133 changes: 111 additions & 22 deletions packages/builders/src/base-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createDiscoverEntriesPlugin } from './discover-entries-esbuild-plugin.j
import { createNodeModuleErrorPlugin } from './node-module-esbuild-plugin.js';
import { createSwcPlugin } from './swc-esbuild-plugin.js';
import type { WorkflowConfig } from './types.js';
import { extractWorkflowGraphs } from './workflows-extractor.js';

const enhancedResolve = promisify(enhancedResolveOriginal);

Expand Down Expand Up @@ -270,6 +271,7 @@ export abstract class BaseBuilder {
* Steps have full Node.js runtime access and handle side effects, API calls, etc.
*
* @param externalizeNonSteps - If true, only bundles step entry points and externalizes other code
* @returns Build context (for watch mode) and the collected workflow manifest
*/
protected async createStepsBundle({
inputFiles,
Expand All @@ -285,7 +287,10 @@ export abstract class BaseBuilder {
outfile: string;
format?: 'cjs' | 'esm';
externalizeNonSteps?: boolean;
}): Promise<esbuild.BuildContext | undefined> {
}): Promise<{
context: esbuild.BuildContext | undefined;
manifest: WorkflowManifest;
}> {
// These need to handle watching for dev to scan for
// new entries and changes to existing ones
const { discoveredSteps: stepFiles } = await this.discoverEntries(
Expand Down Expand Up @@ -403,23 +408,14 @@ export abstract class BaseBuilder {
this.logEsbuildMessages(stepsResult, 'steps bundle creation');
console.log('Created steps bundle', `${Date.now() - stepsBundleStart}ms`);

const partialWorkflowManifest = {
steps: workflowManifest.steps,
};
// always write to debug file
await this.writeDebugFile(
join(dirname(outfile), 'manifest'),
partialWorkflowManifest,
true
);

// Create .gitignore in .swc directory
await this.createSwcGitignore();

if (this.config.watch) {
return esbuildCtx;
return { context: esbuildCtx, manifest: workflowManifest };
}
await esbuildCtx.dispose();
return { context: undefined, manifest: workflowManifest };
}

/**
Expand Down Expand Up @@ -539,16 +535,6 @@ export abstract class BaseBuilder {
`${Date.now() - bundleStartTime}ms`
);

const partialWorkflowManifest = {
workflows: workflowManifest.workflows,
};

await this.writeDebugFile(
join(dirname(outfile), 'manifest'),
partialWorkflowManifest,
true
);

if (this.config.workflowManifestPath) {
const resolvedPath = resolve(
process.cwd(),
Expand Down Expand Up @@ -900,4 +886,107 @@ export const OPTIONS = handler;`;
// We're intentionally silently ignoring this error - creating .gitignore isn't critical
}
}

/**
* Creates a manifest JSON file containing step/workflow metadata
* and graph data for visualization.
*/
protected async createManifest({
workflowBundlePath,
manifestDir,
manifest,
}: {
workflowBundlePath: string;
manifestDir: string;
manifest: WorkflowManifest;
}): Promise<void> {
const buildStart = Date.now();
console.log('Creating manifest...');

try {
const workflowGraphs = await extractWorkflowGraphs(workflowBundlePath);

const steps = this.convertStepsManifest(manifest.steps);
const workflows = this.convertWorkflowsManifest(
manifest.workflows,
workflowGraphs
);

const output = { version: '1.0.0', steps, workflows };

await mkdir(manifestDir, { recursive: true });
await writeFile(
join(manifestDir, 'manifest.json'),
JSON.stringify(output, null, 2)
);

const stepCount = Object.values(steps).reduce(
(acc, s) => acc + Object.keys(s).length,
0
);
const workflowCount = Object.values(workflows).reduce(
(acc, w) => acc + Object.keys(w).length,
0
);

console.log(
`Created manifest with ${stepCount} step(s) and ${workflowCount} workflow(s)`,
`${Date.now() - buildStart}ms`
);
} catch (error) {
console.warn(
'Failed to create manifest:',
error instanceof Error ? error.message : String(error)
);
}
}

private convertStepsManifest(
steps: WorkflowManifest['steps']
): Record<string, Record<string, { stepId: string }>> {
const result: Record<string, Record<string, { stepId: string }>> = {};
if (!steps) return result;

for (const [filePath, entries] of Object.entries(steps)) {
result[filePath] = {};
for (const [name, data] of Object.entries(entries)) {
result[filePath][name] = { stepId: data.stepId };
}
}
return result;
}

private convertWorkflowsManifest(
workflows: WorkflowManifest['workflows'],
graphs: Record<
string,
Record<string, { graph: { nodes: any[]; edges: any[] } }>
>
): Record<
string,
Record<
string,
{ workflowId: string; graph: { nodes: any[]; edges: any[] } }
>
> {
const result: Record<
string,
Record<
string,
{ workflowId: string; graph: { nodes: any[]; edges: any[] } }
>
> = {};
if (!workflows) return result;

for (const [filePath, entries] of Object.entries(workflows)) {
result[filePath] = {};
for (const [name, data] of Object.entries(entries)) {
result[filePath][name] = {
workflowId: data.workflowId,
graph: graphs[filePath]?.[name]?.graph || { nodes: [], edges: [] },
};
}
}
return result;
}
}
17 changes: 14 additions & 3 deletions packages/builders/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,19 @@ export class StandaloneBuilder extends BaseBuilder {
tsBaseUrl: tsConfig.baseUrl,
tsPaths: tsConfig.paths,
};
await this.buildStepsBundle(options);
const manifest = await this.buildStepsBundle(options);
await this.buildWorkflowsBundle(options);
await this.buildWebhookFunction();

// Build unified manifest from workflow bundle
const workflowBundlePath = this.resolvePath('.swc/workflows.js');
const manifestDir = this.resolvePath('.swc');
await this.createManifest({
workflowBundlePath,
manifestDir,
manifest,
});

await this.createClientLibrary();
}

Expand All @@ -33,18 +42,20 @@ export class StandaloneBuilder extends BaseBuilder {
inputFiles: string[];
tsBaseUrl?: string;
tsPaths?: Record<string, string[]>;
}): Promise<void> {
}) {
console.log('Creating steps bundle at', this.config.stepsBundlePath);

const stepsBundlePath = this.resolvePath(this.config.stepsBundlePath);
await this.ensureDirectory(stepsBundlePath);

await this.createStepsBundle({
const { manifest } = await this.createStepsBundle({
outfile: stepsBundlePath,
inputFiles,
tsBaseUrl,
tsPaths,
});

return manifest;
}

private async buildWorkflowsBundle({
Expand Down
16 changes: 13 additions & 3 deletions packages/builders/src/vercel-build-output-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ export class VercelBuildOutputAPIBuilder extends BaseBuilder {
tsBaseUrl: tsConfig.baseUrl,
tsPaths: tsConfig.paths,
};
await this.buildStepsFunction(options);
const manifest = await this.buildStepsFunction(options);
await this.buildWorkflowsFunction(options);
await this.buildWebhookFunction(options);
await this.createBuildOutputConfig(outputDir);

// Generate unified manifest
const workflowBundlePath = join(workflowGeneratedDir, 'flow.func/index.js');
await this.createManifest({
workflowBundlePath,
manifestDir: workflowGeneratedDir,
manifest,
});

await this.createClientLibrary();
}

Expand All @@ -38,13 +46,13 @@ export class VercelBuildOutputAPIBuilder extends BaseBuilder {
workflowGeneratedDir: string;
tsBaseUrl?: string;
tsPaths?: Record<string, string[]>;
}): Promise<void> {
}) {
console.log('Creating Vercel Build Output API steps function');
const stepsFuncDir = join(workflowGeneratedDir, 'step.func');
await mkdir(stepsFuncDir, { recursive: true });

// Create steps bundle
await this.createStepsBundle({
const { manifest } = await this.createStepsBundle({
inputFiles,
outfile: join(stepsFuncDir, 'index.js'),
tsBaseUrl,
Expand All @@ -57,6 +65,8 @@ export class VercelBuildOutputAPIBuilder extends BaseBuilder {
shouldAddSourcemapSupport: true,
experimentalTriggers: [STEP_QUEUE_TRIGGER],
});

return manifest;
}

private async buildWorkflowsFunction({
Expand Down
Loading
Loading