# Compile Workflow

> Update workflow implementation from its embedded description. Use this after modifying workflow or node descriptions.

- Skill: `timescale/compile-workflow` (Agent Skill)
- Install (CLI): `npx skillmds@latest add timescale/compile-workflow`
- Raw SKILL.md: https://api.skillmd.com/api/skills/timescale/compile-workflow/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: timescale (https://skillmd.com/u/timescale)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/timescale/compile-workflow

---


# Compile Workflow

Updates the `run()` method of a workflow in `src/crayon/workflows/*.ts` based on its embedded `description` field and the `description` fields in referenced nodes/agents.

**Announce at start:** "I'm using the compile-workflow skill to update the workflow implementation from its description."

---

## Pre-Flight Checks

1. **Verify workflow files exist:**
   - `src/crayon/workflows/` must exist with at least one `.ts` file
   - If no `.ts` files found, tell user to run `/crayon:create-workflow` first

2. **If no workflow name provided:**
   - List all workflows in `src/crayon/workflows/`
   - Ask user to select which one to compile

3. **If workflow name provided:**
   - Verify `src/crayon/workflows/<name>.ts` exists
   - If not, list available workflows and ask user to choose

---

## Description Parsing

### Workflow Description

Read the `description` field from the `Workflow.create()` call in `src/crayon/workflows/<name>.ts`. The description contains flow-level information:

- **Summary** — first line/paragraph
- **`## Tasks`** — ordered list of tasks with:
  - `**Node:**` references (name + type)
  - `**Condition:**` / `**If true:**` / `**If false:**` for decisions
  - `**Loop:**` for iteration
  - `**Return:**` for terminal tasks

### Node/Agent Descriptions

For each task's `**Node:**` reference, read the `description` field from the node/agent file to get:

- **What the node does** — first paragraph
- `**Input Description:**` — plain language inputs
- `**Output Description:**` — plain language outputs

### Task Formats

**Standard task:**
```markdown
### N. Task Name
**Node:** `node-name` (agent|node)
```

Node file contains:
```markdown
<Description>

**Input Description:** what it needs

**Output Description:** what it produces
```

**Decision task** (no Node):
```markdown
### N. Decision Name
**Condition:** `expression`
**If true:** continue to task M
**If false:** return:
  - field1: value
  - field2: value
```

**Terminal task** (ends with Return):
```markdown
**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 crayon | `import { webRead } from "runcrayon"` |
| `(node)` | User-defined in `src/crayon/nodes/` | `import { nodeName } from "../nodes/<name>"` |
| `(agent)` | `src/crayon/agents/<name>.ts` | `import { agentName } from "../agents/<name>"` |

**Note:** Agent imports reference the executable file (`src/crayon/agents/<name>.ts`), not the spec file (`src/crayon/agents/<name>.md`). The executable contains the runtime code that loads the spec.

**IMPORTANT:** Never use `.js` extensions in import paths. Use extensionless imports (e.g., `"../nodes/check-website"` not `"../nodes/check-website.js"`). Turbopack cannot resolve `.js` → `.ts` in production builds.

### Resolution Steps

1. **Parse node reference:** Extract name and type from `**Node:** \`name\` (type)` in the workflow description

2. **For builtin nodes:**
   - Check if it's a built-in node (`web_read`, etc.)
   - Import from `"runcrayon"`

3. **For user-defined nodes:**
   - Look for `src/crayon/nodes/<name>.ts`
   - Read its `description` field and `inputSchema`/`outputSchema` for type info
   - If missing: create it using the stub templates from `/crayon:create-workflow`

4. **For agents:**
   - Look for `src/crayon/agents/<name>.ts`
   - Read its `description` field and `inputSchema`/`outputSchema` for type info
   - If missing: create it using the stub templates from `/crayon:create-workflow`

### Updating Agent Tools from Description

When an agent's description contains a `**Tools needed:**` section (added by `/crayon:refine-node`), update the agent's `tools` record and imports to match. Use the tool type to determine the import pattern:

| Tool Type | Import Pattern | tools record entry |
|-----------|----------------|--------------------|
| `(builtin)` | `import { webRead } from "runcrayon"` | `web_read: webRead` |
| `(provider)` | `import { createOpenAI } from "@ai-sdk/openai"; const openai = createOpenAI();` | `web_search: openai.tools.webSearch()` |
| `(user node in src/crayon/nodes/<file>.ts)` | `import { name } from "../nodes/<file>"` | `enrich_company: enrichCompany` |

---

## Handling Ambiguities

When task logic is unclear, make your best guess and generate the code. The user can correct after. Don't block on questions.

### Common Ambiguities and Defaults

| Pattern | Problem | Default |
|---------|---------|---------|
| "if good fit" | Undefined criteria | Use `score >= 80` and add a comment noting the threshold is a guess |
| "check if valid" | Undefined validation | Check for presence of required fields |
| Untyped output | Can't generate schema | Infer from node description and context |
| Missing condition | Decision has no **Condition:** | Infer from surrounding task context, add a TODO comment if truly unknowable |

After generating code, tell the user what you assumed so they can correct anything.

---

## Code Generation

Rewrite the `run()` method in the existing `src/crayon/workflows/<name>.ts` file. Also update imports and schemas as needed. Preserve the `description` field as-is.

### Generated run() Structure

```typescript
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> };
},
```

### Side-Effect Node Data Flow

When wiring `ctx.run()` calls for side-effect nodes (nodes whose description includes `**Side Effect:**`):

1. **Every target input MUST trace to an explicit source** — Side-effect node inputs like `recipientEmail`, `slackChannel`, `recordId` must come from:
   - The workflow's own input fields (e.g., `inputs.recipientEmail`)
   - An upstream node's output field (e.g., `enrichResult.email`)
   - A deliberate constant with clear rationale

   **NEVER fabricate or randomly choose** a target value. If the data flow for a target field is unclear, add a `// TODO: wire this to an explicit source` comment and flag it to the user.

2. **Side-effect outputs should be captured** — Always capture the return value of side-effect nodes, even if it's not used by downstream nodes. This ensures the action details (what was sent, to whom, what was updated) appear in the workflow trace.

   ```typescript
   // GOOD: capture the result
   const slackResult = await ctx.run(sendSlackDm, { channel: inputs.slackChannel, message: summary });

   // BAD: discard the result
   await ctx.run(sendSlackDm, { channel: inputs.slackChannel, message: summary });
   ```

### 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`)

---

## Worked Example

A workflow enriches Gmail leads and sends results to Slack. Here's how the compiler turns descriptions into the `run()` method.

### Input: Workflow description

```markdown
Enrich the 10 most recent Gmail leads from Salesforce with web research and DM results on Slack.

## Tasks

### 1. Fetch Gmail Leads
**Node:** `fetch-gmail-leads` (node)

### 2. Enrich Lead
**Node:** `enrich-lead` (agent)
**Loop:** for each lead in leads

### 3. Send Slack DM
**Node:** `send-slack-dm` (node)
```

### Input: Node schemas (from refined node files)

- **fetch-gmail-leads** — `inputSchema: z.object({})`, `outputSchema: z.object({ leads: z.array(LeadSchema) })`
- **enrich-lead** — `inputSchema: z.object({ name: z.string(), email: z.string(), company: z.string().nullable(), title: z.string().nullable() })`, `outputSchema: z.object({ name: z.string(), email: z.string(), linkedinUrl: z.string().nullable(), ... })`
- **send-slack-dm** — `inputSchema: z.object({ enrichedLeads: z.array(EnrichedLeadSchema) })`, `outputSchema: z.object({ success: z.boolean(), channel: z.string() })`

### Output: Generated run() method

```typescript
async run(ctx, inputs: LeadEnrichmentInput): Promise<LeadEnrichmentOutput> {
  // Task 1: Fetch the 10 most recent Gmail leads from Salesforce
  const leadsResult = await ctx.run(fetchGmailLeads, {});

  // Task 2: Enrich each lead with web research
  const enrichedLeads = [];
  for (const lead of leadsResult.leads) {
    const enriched = await ctx.run(enrichLead, {
      name: lead.name ?? "",
      email: lead.email ?? "",
      company: lead.company,
      title: lead.title,
    });
    enrichedLeads.push(enriched);
  }

  // Task 3: Send enriched lead summary as Slack DM
  const slackResult = await ctx.run(sendSlackDm, { enrichedLeads });

  return { success: slackResult.success, channel: slackResult.channel };
},
```

Key things the compiler did:
1. **Data flow** — wired `leadsResult.leads` fields into `enrichLead`'s input schema, and `enrichedLeads` array into `sendSlackDm`'s input
2. **Loop** — translated `**Loop:** for each lead in leads` into a `for...of` loop over `leadsResult.leads`
3. **Schema alignment** — matched field names from output schemas to input schemas (e.g., `lead.name` → `name`, `lead.company` → `company`)

### After Compilation

1. **Save a version** — Call the `create_version` MCP tool with a message like:
   ```
   Finalize workflow: <workflow-name>

   <Describe what changed — like a good git commit message body.>
   ```

2. Tell the user:
   - "Updated `src/crayon/workflows/<name>.ts`"
   - If any missing nodes/agents were created: list the new files
   - If agent tools were updated from descriptions: list which agents were updated
   - "To test it, just say **run it** and I'll trigger it for you." — do NOT suggest using the CLI or opening the Dev UI manually

---

## Compiler Principles

1. **Draft first, ask later** — make your best guess and let the user correct, rather than blocking on questions
2. **No invention** — only emit code that maps to descriptions; guesses should be flagged with comments
3. **Deterministic** — same descriptions → same output
4. **Readable output** — generated code should be understandable
5. **Update descriptions** — when clarifying, update the description field so it stays canonical
6. **Run it yourself** — when the user wants to test a workflow or node, use the `runWorkflow` / `runNode` MCP tools or run CLI commands yourself.
7. **Side-effect targets are never fabricated** — they must come from an explicit source or be a deliberate constant.

