From crayon
Update workflow implementation from its embedded description. Use this after modifying workflow or node descriptions.
npx claudepluginhub timescale/crayon --plugin crayonThis skill uses the workspace's default tool permissions.
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.
Verifies tests pass on completed feature branch, presents options to merge locally, create GitHub PR, keep as-is or discard; executes choice and cleans up worktree.
Guides root cause investigation for bugs, test failures, unexpected behavior, performance issues, and build failures before proposing fixes.
Writes implementation plans from specs for multi-step tasks, mapping files and breaking into TDD bite-sized steps before coding.
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."
Verify workflow files exist:
src/crayon/workflows/ must exist with at least one .ts file.ts files found, tell user to run /crayon:create-workflow firstIf no workflow name provided:
src/crayon/workflows/If workflow name provided:
src/crayon/workflows/<name>.ts existsRead the description field from the Workflow.create() call in src/crayon/workflows/<name>.ts. The description contains flow-level information:
## Tasks — ordered list of tasks with:
**Node:** references (name + type)**Condition:** / **If true:** / **If false:** for decisions**Loop:** for iteration**Return:** for terminal tasksFor each task's **Node:** reference, read the description field from the node/agent file to get:
**Input Description:** — plain language inputs**Output Description:** — plain language outputsStandard 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
For each task's **Node:** reference, determine what it is and where it lives.
| 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.
Parse node reference: Extract name and type from **Node:** \name` (type)` in the workflow description
For builtin nodes:
web_read, etc.)"runcrayon"For user-defined nodes:
src/crayon/nodes/<name>.tsdescription field and inputSchema/outputSchema for type info/crayon:create-workflowFor agents:
src/crayon/agents/<name>.tsdescription field and inputSchema/outputSchema for type info/crayon:create-workflowWhen 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 |
When task logic is unclear, make your best guess and generate the code. The user can correct after. Don't block on questions.
| 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.
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.
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> };
},
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:
inputs.recipientEmail)enrichResult.email)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.
// 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 });
camelCase (e.g., urlSummarizer)PascalCase + Schema/Input/Output (e.g., UrlSummarizerInputSchema)PascalCase + Input/Output (e.g., UrlSummarizerInput)A workflow enriches Gmail leads and sends results to Slack. Here's how the compiler turns descriptions into the run() method.
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)
inputSchema: z.object({}), outputSchema: z.object({ leads: z.array(LeadSchema) })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(), ... })inputSchema: z.object({ enrichedLeads: z.array(EnrichedLeadSchema) }), outputSchema: z.object({ success: z.boolean(), channel: z.string() })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:
leadsResult.leads fields into enrichLead's input schema, and enrichedLeads array into sendSlackDm's input**Loop:** for each lead in leads into a for...of loop over leadsResult.leadslead.name → name, lead.company → company)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:
src/crayon/workflows/<name>.ts"runWorkflow / runNode MCP tools or run CLI commands yourself.