Skip to content

Commit 792c07b

Browse files
authored
feat: support DAG view for workflow wrappers in typescript (#3410)
* feat: support DAG view for workflow wrappers in typescript * version bump and changelog * address pr review, add comments review file
1 parent 0e3f295 commit 792c07b

13 files changed

Lines changed: 846 additions & 149 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
applyTo: "**"
3+
---
4+
5+
# Comment Style
6+
7+
Comments must explain **why** or **how** — not **what**. If a comment only restates
8+
the function name or describes what the next line of code does, delete it.
9+
10+
## Remove
11+
12+
- Section banners that label blocks of code by name or language
13+
- Doc comments that paraphrase the function or field name
14+
- Inline narration of the next line of code
15+
16+
## Keep
17+
18+
- Non-obvious design decisions and the reason behind them
19+
- Constraints with their cause: why a limit exists, why a fallback is needed
20+
- "Why something is absent": why a guard is missing, why a default is chosen
21+
- Algorithmic details that aren't self-evident from the code

‎frontend/vscode-extension/README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,40 @@ This will then open a webview showing the DAG definition. As you update your def
1212

1313
![Webview](https://raw.githubusercontent.com/hatchet-dev/hatchet/main/frontend/vscode-extension/assets/vscode_dag.png)
1414

15+
## Custom workflow wrappers
16+
17+
If your codebase wraps the Hatchet workflow builder, annotate your factory function with `@hatchet-workflow` so the extension can detect variables it creates:
18+
19+
```typescript
20+
/**
21+
* @hatchet-workflow
22+
* @hatchet-task-method task
23+
* @hatchet-task-parents parents
24+
*/
25+
export function createWorkflowBuilder(options) { ... }
26+
```
27+
28+
The extension scans your workspace for annotated functions on startup and places a **Show Hatchet DAG** CodeLens above every variable created by one. Two optional tags let you match your wrapper's API:
29+
30+
| Tag | Default | Purpose |
31+
|---|---|---|
32+
| `@hatchet-task-method` | `task` | Method name used to register tasks |
33+
| `@hatchet-task-parents` | `parents` | Option property that lists parent tasks |
34+
35+
Both `wf.task('name', { parents })` and `wf.task({ name, parents })` call signatures are supported automatically.
36+
1537
## Reporting issues
1638

1739
You can report issues in our [Github issues](https://github.com/hatchet-dev/hatchet/issues).
40+
41+
## Changelog
42+
43+
### 0.2.0
44+
45+
- **Custom wrapper support** — annotate any factory function with `@hatchet-workflow` to get DAG visualization for variables it creates, with no other config required. Use `@hatchet-task-method` and `@hatchet-task-parents` to match your wrapper's API when it differs from the Hatchet defaults.
46+
- Both positional-name (`wf.task('name', opts)`) and options-object (`wf.task({ name, parents })`) task call signatures are now supported.
47+
- Cross-file task resolution via LSP now honours custom method and parents property names.
48+
49+
### 0.1.2
50+
51+
- Initial release with DAG visualization for TypeScript, Python, Go, and Ruby workflows.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Example: custom wrapper with non-default task method and parent property names.
3+
*
4+
* When your wrapper uses a method name other than `task`, or a dependency
5+
* property name other than `parents`, declare them with:
6+
* @hatchet-task-method <methodName>
7+
* @hatchet-task-parents <propName>
8+
*/
9+
import Hatchet, { type WorkflowDeclaration } from '@hatchet-dev/typescript-sdk';
10+
11+
type JsonObject = Record<string, unknown>;
12+
13+
interface StepOptions<TInput extends JsonObject, TOutput extends JsonObject> {
14+
fn: (input: TInput) => Promise<TOutput>;
15+
/** Steps that must complete before this one. */
16+
dependsOn?: StepHandle<JsonObject>[];
17+
}
18+
19+
export interface StepHandle<TOutput extends JsonObject> {
20+
readonly name: string;
21+
readonly _def: ReturnType<WorkflowDeclaration['task']>;
22+
}
23+
24+
export interface PipelineBuilder<TInput extends JsonObject> {
25+
/** Register a pipeline step. */
26+
step<TOutput extends JsonObject>(
27+
name: string,
28+
options: StepOptions<TInput, TOutput>,
29+
): StepHandle<TOutput>;
30+
build(): WorkflowDeclaration;
31+
}
32+
33+
const hatchet = Hatchet.init();
34+
35+
/**
36+
* Create a pipeline builder where steps are registered with `.step()` and
37+
* dependencies are expressed via `dependsOn`.
38+
*
39+
* @hatchet-workflow
40+
* @hatchet-task-method step
41+
* @hatchet-task-parents dependsOn
42+
*/
43+
export function createPipelineBuilder<TInput extends JsonObject>(options: {
44+
name: string;
45+
}): PipelineBuilder<TInput> {
46+
const wf = hatchet.workflow<TInput>({ name: options.name });
47+
48+
return {
49+
step<TOutput extends JsonObject>(
50+
name: string,
51+
stepOpts: StepOptions<TInput, TOutput>,
52+
): StepHandle<TOutput> {
53+
const def = wf.task({
54+
name,
55+
parents: stepOpts.dependsOn?.map((s) => s._def),
56+
fn: async (input: TInput) => stepOpts.fn(input),
57+
});
58+
return { name, _def: def };
59+
},
60+
build(): WorkflowDeclaration {
61+
return wf;
62+
},
63+
};
64+
}
65+
66+
// ── Usage ─────────────────────────────────────────────────────────────────────
67+
// The extension detects `imagePipeline` via the @hatchet-workflow annotation
68+
// on createPipelineBuilder, using `step` as the task method and `dependsOn`
69+
// as the parents property.
70+
71+
const imagePipeline = createPipelineBuilder<{ imageUrl: string }>({
72+
name: 'image-processing-pipeline',
73+
});
74+
75+
const download = imagePipeline.step('download', {
76+
fn: async (input) => ({ bytes: new Uint8Array() }),
77+
});
78+
79+
const resize = imagePipeline.step('resize', {
80+
dependsOn: [download],
81+
fn: async (input) => ({ resized: true }),
82+
});
83+
84+
const compress = imagePipeline.step('compress', {
85+
dependsOn: [resize],
86+
fn: async (input) => ({ compressed: true }),
87+
});
88+
89+
const upload = imagePipeline.step('upload', {
90+
dependsOn: [compress],
91+
fn: async (input) => ({ url: '' }),
92+
});
93+
94+
export default imagePipeline.build();
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Example: custom workflow builder wrapper.
3+
*
4+
* Annotate any factory function that returns a workflow-builder-like object
5+
* with `@hatchet-workflow`. The VS Code extension will then detect variables
6+
* created by this function and render their DAGs, just like standard
7+
* `hatchet.workflow(...)` calls.
8+
*
9+
* Optional tags:
10+
* @hatchet-task-method — name of the method used to register tasks
11+
* (defaults to "task")
12+
* @hatchet-task-parents — name of the option property that lists parent
13+
* tasks (defaults to "parents")
14+
*/
15+
import Hatchet, {
16+
type Context,
17+
type WorkflowDeclaration,
18+
} from '@hatchet-dev/typescript-sdk';
19+
import type { CreateWorkflowTaskOpts } from '@hatchet-dev/typescript-sdk';
20+
21+
// ── Supporting types ──────────────────────────────────────────────────────────
22+
23+
type JsonObject = Record<string, unknown>;
24+
25+
interface Services {
26+
db: unknown; // replace with your actual DB/service client types
27+
}
28+
29+
interface TaskOptions<TInput extends JsonObject, TOutput extends JsonObject> {
30+
/** Business logic to execute for this task. */
31+
fn: (args: { input: TInput; services: Services }) => Promise<TOutput>;
32+
/** Tasks that must complete before this one runs. */
33+
parents?: TaskHandle<JsonObject>[];
34+
retries?: number;
35+
timeoutSeconds?: number;
36+
}
37+
38+
/**
39+
* A lightweight handle returned by `WorkflowBuilder.task()`.
40+
* Pass these as `parents` to express DAG dependencies.
41+
*/
42+
export interface TaskHandle<TOutput extends JsonObject> {
43+
/** Display name shown in the DAG. */
44+
readonly name: string;
45+
/** @internal Raw Hatchet task opts — used to wire up parents internally. */
46+
readonly _taskDef: CreateWorkflowTaskOpts<any, any>;
47+
}
48+
49+
interface WorkflowBuilderOptions<TInput extends JsonObject> {
50+
/** Workflow display name (shown in the Hatchet UI and the VS Code DAG view). */
51+
name: string;
52+
version?: string;
53+
/** Called once per task invocation to produce scoped service clients. */
54+
createServices: (input: TInput) => Promise<Services>;
55+
}
56+
57+
/**
58+
* A fluent builder for Hatchet workflows with injected service context.
59+
*
60+
* Usage:
61+
* ```ts
62+
* const wf = createWorkflowBuilder({ name: 'my-workflow', createServices });
63+
* const step1 = wf.task('step1', { fn: async ({ input, services }) => ... });
64+
* const step2 = wf.task('step2', { parents: [step1], fn: async (...) => ... });
65+
* export default wf.build();
66+
* ```
67+
*/
68+
export interface WorkflowBuilder<TInput extends JsonObject> {
69+
/**
70+
* Register a task on this workflow.
71+
*
72+
* @param name Unique task name shown in the DAG.
73+
* @param options Task configuration including the `fn` and optional `parents`.
74+
*/
75+
task<TOutput extends JsonObject>(
76+
name: string,
77+
options: TaskOptions<TInput, TOutput>,
78+
): TaskHandle<TOutput>;
79+
80+
/** Finalise the workflow and return the underlying `WorkflowDeclaration`. */
81+
build(): WorkflowDeclaration;
82+
}
83+
84+
// ── Factory function ──────────────────────────────────────────────────────────
85+
86+
const hatchet = Hatchet.init();
87+
88+
/**
89+
* Create a workflow builder that automatically injects service context into
90+
* every task function.
91+
*
92+
* Annotated with `@hatchet-workflow` so the VS Code extension can detect
93+
* variables created by this function and render their task graphs.
94+
*
95+
* @hatchet-workflow
96+
* @hatchet-task-method task
97+
* @hatchet-task-parents parents
98+
*/
99+
export function createWorkflowBuilder<TInput extends JsonObject>(
100+
options: WorkflowBuilderOptions<TInput>,
101+
): WorkflowBuilder<TInput> {
102+
const wf = hatchet.workflow<TInput>({ name: options.name });
103+
104+
return {
105+
task<TOutput extends JsonObject>(
106+
name: string,
107+
taskOpts: TaskOptions<TInput, TOutput>,
108+
): TaskHandle<TOutput> {
109+
const wrappedFn = async (input: TInput, _ctx: Context<TInput>): Promise<TOutput> => {
110+
const services = await options.createServices(input);
111+
return taskOpts.fn({ input, services });
112+
};
113+
114+
const taskDef = wf.task({
115+
name,
116+
retries: taskOpts.retries,
117+
...(taskOpts.timeoutSeconds
118+
? { executionTimeout: `${taskOpts.timeoutSeconds}s` }
119+
: {}),
120+
parents: taskOpts.parents?.map((p) => p._taskDef),
121+
fn: wrappedFn,
122+
});
123+
124+
return { name, _taskDef: taskDef };
125+
},
126+
127+
build(): WorkflowDeclaration {
128+
return wf;
129+
},
130+
};
131+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Standard Hatchet workflow — detected automatically by the VS Code extension.
3+
*
4+
* The extension looks for `hatchet.workflow(...)` calls and renders a DAG
5+
* for each one. No annotation is needed for this pattern.
6+
*/
7+
import Hatchet from '@hatchet-dev/typescript-sdk';
8+
9+
const hatchet = Hatchet.init();
10+
11+
// The extension detects this variable as a workflow and places a CodeLens above it.
12+
const dataProcessing = hatchet.workflow({
13+
name: 'data-processing',
14+
});
15+
16+
const ingest = dataProcessing.task({
17+
name: 'ingest',
18+
fn: async (input) => {
19+
return { raw: 'data' };
20+
},
21+
});
22+
23+
const validate = dataProcessing.task({
24+
name: 'validate',
25+
parents: [ingest],
26+
fn: async (input, ctx) => {
27+
const { raw } = await ctx.parentOutput(ingest);
28+
return { valid: true };
29+
},
30+
});
31+
32+
const transform = dataProcessing.task({
33+
name: 'transform',
34+
parents: [validate],
35+
fn: async (input, ctx) => {
36+
return { transformed: true };
37+
},
38+
});
39+
40+
const load = dataProcessing.task({
41+
name: 'load',
42+
parents: [transform],
43+
fn: async (input, ctx) => {
44+
return { loaded: true };
45+
},
46+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Example: using the custom workflow builder.
3+
*
4+
* Because `createWorkflowBuilder` is annotated with `@hatchet-workflow`,
5+
* the VS Code extension detects `reportWorkflow` as a workflow variable and
6+
* shows a "Show Hatchet DAG" CodeLens above its declaration — no config needed.
7+
*/
8+
import { createWorkflowBuilder } from './custom-wrapper';
9+
10+
interface ReportInput {
11+
reportId: string;
12+
format: 'pdf' | 'csv';
13+
}
14+
15+
// The extension detects this as a workflow (via the @hatchet-workflow annotation
16+
// on createWorkflowBuilder) and places a CodeLens above this line.
17+
const reportWorkflow = createWorkflowBuilder<ReportInput>({
18+
name: 'generate-report',
19+
createServices: async (_input) => ({
20+
db: null, // replace with real DB client
21+
}),
22+
});
23+
24+
const fetchData = reportWorkflow.task('fetch-data', {
25+
fn: async ({ input, services }) => {
26+
return { rows: [] as unknown[] };
27+
},
28+
});
29+
30+
const formatData = reportWorkflow.task('format-data', {
31+
parents: [fetchData],
32+
fn: async ({ input, services }) => {
33+
return { formatted: '' };
34+
},
35+
});
36+
37+
const generateFile = reportWorkflow.task('generate-file', {
38+
parents: [formatData],
39+
fn: async ({ input, services }) => {
40+
return { fileUrl: '' };
41+
},
42+
});
43+
44+
const sendNotification = reportWorkflow.task('send-notification', {
45+
parents: [generateFile],
46+
fn: async ({ input, services }) => {
47+
return { sent: true };
48+
},
49+
});
50+
51+
export default reportWorkflow.build();

‎frontend/vscode-extension/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "hatchet",
33
"displayName": "Hatchet",
44
"description": "Visualize Hatchet workflow DAGs inline in your editor",
5-
"version": "0.1.2",
5+
"version": "0.2.0",
66
"publisher": "hatchet",
77
"license": "MIT",
88
"repository": {

0 commit comments

Comments
 (0)