Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 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 <karthik@scale3labs.com>
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 <karthik@scale3labs.com>
karthikscale3 Dec 5, 2025
8208f3c
Merge
karthikscale3 Dec 11, 2025
e030d8b
Merge branch 'main' into karthik/workflow-cfg-extractor
pranaygp Dec 15, 2025
fcef5bf
fix merge conflict
pranaygp Dec 15, 2025
a65dc9e
reolve merge
karthikscale3 Dec 15, 2025
5622cf6
Update run-detail-view component
karthikscale3 Dec 15, 2025
0733c80
DCO Remediation Commit for Karthik Kalyanaraman <karthik@scale3labs.com>
karthikscale3 Dec 15, 2025
690bb87
Fix world testing
pranaygp Dec 15, 2025
e286574
Merge branch 'karthik/workflow-cfg-extractor' of github.com:karthiksc…
karthikscale3 Dec 15, 2025
713b55e
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 15, 2025
086824d
Update CLI inspect modules
karthikscale3 Dec 15, 2025
75d251e
Update inspect env configuration
karthikscale3 Dec 15, 2025
8c0c5e2
Update workflow extractor and graph execution mapper
karthikscale3 Dec 16, 2025
f2d1047
Fix graph execution mapper
karthikscale3 Dec 16, 2025
e7fc3e3
Update workflow graph visualization and CFG extraction
karthikscale3 Dec 16, 2025
f9d769a
Improve workflow graph execution mapping and visualization
karthikscale3 Dec 16, 2025
40c8962
Update workflow graph viewer components
karthikscale3 Dec 16, 2025
8e87057
Update workflow extractor and execution mapper
karthikscale3 Dec 16, 2025
c988408
Refine workflow graph viewer components
karthikscale3 Dec 16, 2025
9f25845
Merge branch 'main' of github.com:karthikscale3/workflow into karthik…
karthikscale3 Dec 16, 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.
151 changes: 123 additions & 28 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,16 +287,17 @@ 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(
inputFiles,
dirname(outfile)
);
const { discoveredSteps: stepFiles, discoveredWorkflows: workflowFiles } =
await this.discoverEntries(inputFiles, dirname(outfile));

// log the step files for debugging
await this.writeDebugFile(outfile, { stepFiles });
await this.writeDebugFile(outfile, { stepFiles, workflowFiles });

const stepsBundleStart = Date.now();
const workflowManifest: WorkflowManifest = {};
Expand All @@ -316,7 +319,10 @@ export abstract class BaseBuilder {

// Create a virtual entry that imports all files. All step definitions
// will get registered thanks to the swc transform.
const imports = stepFiles
// We also import workflow files so their metadata is collected by the SWC plugin,
// even though they'll be externalized from the final bundle.
const filesToProcess = [...stepFiles, ...workflowFiles];
const imports = filesToProcess
.map((file) => {
// Normalize both paths to forward slashes before calling relative()
// This is critical on Windows where relative() can produce unexpected results with mixed path formats
Expand Down Expand Up @@ -381,9 +387,14 @@ export abstract class BaseBuilder {
plugins: [
createSwcPlugin({
mode: 'step',
// Include both step and workflow files so the SWC plugin processes them
// and collects metadata. Workflow files need to be included so their
// workflow metadata is collected, even though the workflow function
// bodies are left untouched in step mode.
entriesToBundle: externalizeNonSteps
? [
...stepFiles,
...workflowFiles,
...(resolvedBuiltInSteps ? [resolvedBuiltInSteps] : []),
]
: undefined,
Expand All @@ -403,23 +414,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 +541,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 +892,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;
}
}
19 changes: 16 additions & 3 deletions packages/builders/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,21 @@ 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(
this.config.workflowsBundlePath
);
const manifestDir = this.resolvePath('.well-known/workflow/v1');
await this.createManifest({
workflowBundlePath,
manifestDir,
manifest,
});

await this.createClientLibrary();
}

Expand All @@ -33,18 +44,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