| name | compile-workflow |
| description | Update workflow implementation from its embedded description. Use this after modifying workflow or node descriptions. |
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
-
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
-
If no workflow name provided:
- List all workflows in
src/crayon/workflows/
- Ask user to select which one to compile
-
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:
### N. Task Name
**Node:** `node-name` (agent|node)
Node file contains:
<Description>
**Input Description:** what it needs
**Output Description:** what it produces
Decision task (no Node):
### N. Decision Name
**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 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
-
Parse node reference: Extract name and type from **Node:** \name` (type)` in the workflow description
-
For builtin nodes:
- Check if it's a built-in node (
web_read, etc.)
- Import from
"runcrayon"
-
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
-
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
async run(ctx, inputs: <Name>Input): Promise<<Name>Output> {
const <output_var> = await ctx.run(<nodeRef>, { <inputs> });
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:**):
-
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.
-
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.
const slackResult = await ctx.run(sendSlackDm, { channel: inputs.slackChannel, message: summary });
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
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
async run(ctx, inputs: LeadEnrichmentInput): Promise<LeadEnrichmentOutput> {
const leadsResult = await ctx.run(fetchGmailLeads, {});
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);
}
const slackResult = await ctx.run(sendSlackDm, { enrichedLeads });
return { success: slackResult.success, channel: slackResult.channel };
},
Key things the compiler did:
- Data flow — wired
leadsResult.leads fields into enrichLead's input schema, and enrichedLeads array into sendSlackDm's input
- Loop — translated
**Loop:** for each lead in leads into a for...of loop over leadsResult.leads
- Schema alignment — matched field names from output schemas to input schemas (e.g.,
lead.name → name, lead.company → company)
After Compilation
-
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.>
-
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
- Draft first, ask later — make your best guess and let the user correct, rather than blocking on questions
- No invention — only emit code that maps to descriptions; guesses should be flagged with comments
- Deterministic — same descriptions → same output
- Readable output — generated code should be understandable
- Update descriptions — when clarifying, update the description field so it stays canonical
- Run it yourself — when the user wants to test a workflow or node, use the
runWorkflow / runNode MCP tools or run CLI commands yourself.
- Side-effect targets are never fabricated — they must come from an explicit source or be a deliberate constant.