Compile Workflow
Compiles workflow specifications from specs/workflows/*.md into executable TypeScript in generated/workflows/*.ts.
Announce at start: "I'm using the compile-workflow skill to generate TypeScript from the workflow spec."
Pre-Flight Checks
- •
Verify directories exist:
- •
specs/workflows/must exist with at least one.mdfile - •Create
generated/workflows/if it doesn't exist
- •
- •
If no workflow name provided:
- •List all specs in
specs/workflows/ - •Ask user to select which one to compile
- •List all specs in
- •
If workflow name provided:
- •Verify
specs/workflows/<name>.mdexists - •If not, list available specs and ask user to choose
- •Verify
Spec Parsing
Required Sections
- •Frontmatter:
name(required),version(defaults to 1) - •
## Inputs- at least one input parameter - •
## Tasks- at least one task (reject## Steps- tell user to rename)
Optional Sections
- •
## Outputs- if omitted, workflow returnsvoid - •Title and description after frontmatter (for humans, ignored by compiler)
Input Syntax
- param_name: type (required|optional, defaults to X) - Description
Types:
- •Simple:
string,number,boolean - •Union:
"value1" | "value2" - •Nullable:
string | null - •Object:
{ field1: type, field2?: type }(? = optional) - •Array:
string[]or{ id: number }[]
Task Formats
Standard task:
### N. Task Name Description of what this task does. **Node:** `node-name` (agent|node) **Input:** var1, var2.field, inputs.field **Output:** `var_name: type`
Decision task (no Node):
### N. Decision Name Description. **Condition:** `expression` **If true:** continue to task M **If false:** return: - field1: value - field2: value
Terminal task (ends with Return):
**Return:** - field1: value - field2: value
Node Resolution
For each task's **Node:** reference, determine what it is and where it lives.
Node Types
| Type | Location | Import Pattern |
|---|---|---|
(builtin) | Built-in nodes from 0pflow | import { webRead } from "0pflow" |
(node) | User-defined in src/nodes/ | import { nodeName } from "../../src/nodes/<name>.js" |
(agent) | agents/<name>.ts | import { agentName } from "../../agents/<name>.js" |
Note: Agent imports reference the executable file (agents/<name>.ts), not the spec file (specs/agents/<name>.md). The executable contains the runtime code that loads the spec.
Resolution Steps
- •
Parse node reference: Extract name and type from
**Node:** \name` (type)` - •
For builtin nodes:
- •Check if it's a built-in node (
web_read, etc.) - •Import from
"0pflow"
- •Check if it's a built-in node (
- •
For user-defined nodes:
- •Look for
src/nodes/<name>.ts - •If missing: ask user to create it (nodes require user implementation)
- •Look for
- •
For agents:
- •Look for
specs/agents/<name>.md - •If missing but task has enough context: create agent stub (see Stub Generation)
- •If missing and context is insufficient: ask clarifying questions
- •Look for
Stub Generation
When an agent is referenced but doesn't exist, generate a stub using the enriched task description.
Tool Types
There are three types of tools that can be used in agents:
| Type | Description | Import | Example |
|---|---|---|---|
| Provider tools | Tools from AI SDK providers (OpenAI, Anthropic) | import { createOpenAI } from "@ai-sdk/openai" | openai.tools.webSearch() |
| Built-in nodes | Nodes that ship with 0pflow | import { webRead } from "0pflow" | webRead |
| User nodes | Custom nodes implemented in src/nodes/ | import { myNode } from "../../src/nodes/my-node.js" | myNode |
Enriched Task Format (from spec-author)
Tasks for new agents include extra fields used to generate the agent executable. The spec will explicitly specify which tools to use (no mapping required):
### N. Task Name Description of what the agent does. **Tools needed:** - webRead (builtin) - openai.tools.webSearch() (provider) - myCustomNode (user node) **Guidelines:** specific guidelines for the agent (becomes part of system prompt) **Output fields:** field names and types (used for outputSchema) **Node:** `agent-name` (agent) **Input:** ... **Output:** `var: type`
Agent Stub Template
When creating a new agent, generate TWO files:
- •Spec file:
specs/agents/<name>.md- The agent prompt/config - •Executable file:
agents/<name>.ts- TypeScript executable that references the spec
Spec File (specs/agents/<name>.md)
The spec contains only the system prompt and optional model/maxSteps config. Tools are defined in code, not in the spec.
--- name: <agent-name> model: openai/gpt-4o # optional maxSteps: 10 # optional --- # <Agent Title> <First paragraph of task description> ## Task <Derived from task description and inputs> ## Guidelines <From **Guidelines:** field, or defaults:> - Prefer primary sources over aggregators - If information is unavailable, say so rather than guessing - Keep output structured and consistent ## Output Format Return a JSON object with: <From **Output fields:** or parsed from **Output:** type>
Executable File (agents/<name>.ts)
IMPORTANT:
- •Always use absolute path resolution for
specPathto ensure the agent works regardless of the current working directory - •Tools are defined as a record in
Agent.create(), not in the spec file
// agents/<name>.ts
// Agent executable for <name>
import { z } from "zod";
import { Agent, webRead } from "0pflow"; // Built-in nodes
import { createOpenAI } from "@ai-sdk/openai"; // Provider tools
// import { myNode } from "../src/nodes/my-node.js"; // User nodes
import { fileURLToPath } from "url";
import path from "path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Initialize provider for provider-specific tools (only if using provider tools)
const openai = createOpenAI({});
export const <camelCaseName> = Agent.create({
name: "<name>",
description: "<brief description for when this agent is used as a tool>",
inputSchema: z.object({
// ... from task **Input:**
}),
outputSchema: z.object({
// ... from task **Output:** type
}),
// Tools from **Tools needed:** - only include what the spec specifies
tools: {
web_read: webRead, // (builtin)
web_search: openai.tools.webSearch(), // (provider)
// my_node: myNode, // (user node)
},
specPath: path.resolve(__dirname, "../specs/agents/<name>.md"),
});
The path.resolve(__dirname, ...) pattern ensures the spec file is found relative to the executable file's location, not the current working directory.
Generating Tools from Spec
The **Tools needed:** section explicitly specifies each tool with its type. Generate imports and tools record directly:
**Tools needed:** - webRead (builtin) - openai.tools.webSearch() (provider) - enrichCompany (user node in src/nodes/enrich-company.ts)
Generates:
import { webRead } from "0pflow";
import { createOpenAI } from "@ai-sdk/openai";
import { enrichCompany } from "../../src/nodes/enrich-company.js";
const openai = createOpenAI({});
// In Agent.create():
tools: {
web_read: webRead,
web_search: openai.tools.webSearch(),
enrich_company: enrichCompany,
},
When Context Is Insufficient
If the task lacks **Tools needed:**, **Guidelines:**, or clear output type, ask:
"Task N references <agent-name> agent but the task description is missing details:
- •Tools needed: [missing/present]
- •Guidelines: [missing/present]
- •Output format: [missing/present]
Would you like me to ask clarifying questions, or should I create a minimal stub with TODOs?"
Handling Ambiguities
When task logic is unclear, ask for clarification before generating code.
Common Ambiguities
| Pattern | Problem | Ask |
|---|---|---|
| "if good fit" | Undefined criteria | "What's the exact condition? (e.g., score >= 80)" |
| "check if valid" | Undefined validation | "What makes it valid? What fields to check?" |
| Untyped output | Can't generate schema | "What type should var_name be?" |
| Missing condition | Decision has no Condition: | "What condition determines the branch?" |
Clarification Flow
- •Identify the ambiguity
- •Ask ONE specific question
- •Wait for user response
- •Update the spec file with the clarified information
- •Continue compilation
Example
Spec says:
### 3. Check Quality See if the data is good enough. **Condition:** ???
Ask: "Task 3 'Check Quality' needs a concrete condition. What makes data 'good enough'?
- •A) A specific field value (e.g.,
data.score >= 80) - •B) Presence of required fields
- •C) Other criteria (please describe)"
After user answers, update the spec:
### 3. Check Quality Verify the data meets minimum quality threshold. **Condition:** `data.score >= 80`
Code Generation
Generate TypeScript file at generated/workflows/<name>.ts.
File Structure
// generated/workflows/<name>.ts
// Auto-generated by compile-workflow skill - do not edit directly
import { z } from "zod";
import { Workflow } from "0pflow";
// ... node/tool imports ...
// Input schema
const <Name>InputSchema = z.object({
// ... from ## Inputs
});
type <Name>Input = z.infer<typeof <Name>InputSchema>;
// Output schema (if ## Outputs exists)
const <Name>OutputSchema = z.object({
// ... from ## Outputs
});
type <Name>Output = z.infer<typeof <Name>OutputSchema>;
export const <camelCaseName> = Workflow.create({
name: "<kebab-case-name>",
version: <version>,
inputSchema: <Name>InputSchema,
outputSchema: <Name>OutputSchema, // omit if no outputs
async run(ctx, inputs: <Name>Input): Promise<<Name>Output> {
// Task 1: <Task Name>
// <task description as comment>
const <output_var> = await ctx.run(<nodeRef>, { <inputs> });
// Task 2: <Decision or next task>
if (<condition>) {
// ...
}
return { <output fields> };
},
});
Type Mapping
| Spec Type | Zod Schema |
|---|---|
string | z.string() |
number | z.number() |
boolean | z.boolean() |
string | null | z.string().nullable() |
"a" | "b" | z.enum(["a", "b"]) |
{ x: string, y?: number } | z.object({ x: z.string(), y: z.number().optional() }) |
string[] | z.array(z.string()) |
Naming Conventions
- •Workflow export:
camelCase(e.g.,urlSummarizer) - •Schema names:
PascalCase+ Schema/Input/Output (e.g.,UrlSummarizerInputSchema) - •Type names:
PascalCase+ Input/Output (e.g.,UrlSummarizerInput)
Compilation Process
Follow these steps in order:
Step 1: Read and Parse Spec
- •Read
specs/workflows/<name>.md - •Parse frontmatter with gray-matter (name, version)
- •Extract sections: Inputs, Tasks, Outputs (optional)
- •If
## Stepsfound instead of## Tasks, stop and ask user to rename it
Step 2: Validate Structure
- •Verify required sections exist
- •Check all tasks have required fields (Node or Condition)
- •List any missing or ambiguous elements
Step 3: Resolve All Nodes
For each task with a **Node:** reference:
- •Determine node type (agent/function/tool)
- •Verify node exists or create stub
- •Build import statement
Step 4: Clarify Ambiguities
If any ambiguities found:
- •Ask ONE question at a time
- •Update spec with answer
- •Repeat until all ambiguities resolved
Step 5: Generate Code
- •Create
generated/workflows/directory if needed - •Generate TypeScript file
- •Write to
generated/workflows/<name>.ts
Step 6: Report Results
Tell user:
- •"Generated
generated/workflows/<name>.ts" - •If stubs created: "Created agent stub(s):
specs/agents/<name>.md" - •If function nodes missing: "Missing function node(s) that you need to implement:
src/nodes/<name>.ts"
Example Compilation
Input: specs/workflows/url-summarizer.md
Process:
- •Parse spec - found: name=url-summarizer, version=1, 1 input, 3 tasks, 4 outputs
- •Resolve nodes:
- •Task 1:
web_read(node) - built-in ✓ - •Task 2: Decision - no node needed
- •Task 3:
page-summarizer(agent) - check specs/agents/... found ✓
- •Task 1:
- •No ambiguities
- •Generate code
Output: generated/workflows/url-summarizer.ts
Compiler Principles
- •No invention - Only emit code that directly maps to spec
- •Fail closed - Missing info → ask, don't guess
- •Deterministic - Same spec → same output
- •Readable output - Generated code should be understandable
- •Update specs - When clarifying, update the spec file so it stays canonical