From crayon
Refine node definitions - determines HOW each node is implemented (SDKs, libraries, input/output structures, tools, guidelines).
npx claudepluginhub timescale/crayon --plugin crayonThis skill uses the workspace's default tool permissions.
Refine node definitions in existing node and agent files. While create-workflow determines **WHAT** each node does, this phase determines **HOW**:
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.
Refine node definitions in existing node and agent files. While create-workflow determines WHAT each node does, this phase determines HOW:
/crayon:refine-node <workflow-name>
/crayon:refine-node <workflow-name> <node-name>
Read src/crayon/workflows/<workflow-name>.ts and parse its description field to find all task nodes. Then read each referenced node/agent file and check its description field.
A node needs refinement if its inputSchema / outputSchema are still empty z.object({}).
Before drafting, gather the information needed:
For nodes that interact with external systems (Salesforce, HubSpot, Slack, etc.):
/crayon:integrations to determine which SDK/library/API to usesalesforce.md)unlisted.md and research the best optionget_connection with the integration ID, workflow name, and node name. If it succeeds, the connection is already assigned — proceed with implementation using the returned config.list_connections for that integration to see all available connections.
assign_connection to map the chosen connection to the workflow/node. Finally call get_connection again to retrieve provider-specific config (e.g., Salesforce instance_url).list_connections returned a credentials_page_url, tell the user to visit that URL to add a connection. Otherwise tell them to open the Dev UI Credentials page. Then say "continue" when ready.For agent nodes, check AI SDK provider docs for available tools:
WebFetch to read these pages for provider tool optionsintegrations: ["openai"] so the framework fetches the API key at runtime via ctx.getConnection(). Do NOT rely on env vars like OPENAI_API_KEY. The agent executor automatically detects the model provider in the integrations list and calls ctx.getConnection() to get the key.For simple compute nodes: determine if any libraries are needed
Draft the complete refined definition for every node that needs it, then update all files at once.
For each node, determine:
Use your judgment to propose reasonable schemas and tool selections based on the descriptions. The user can correct anything after.
When refining a node that performs side effects (sends emails/messages, updates database records, calls write APIs, modifies external state):
Targets must be intentional — The recipient, channel, record ID, or destination must be either an explicit input field OR a deliberate constant with clear rationale. Never randomly choose or fabricate a target. If the target isn't obvious from the node description, ask the user — don't guess.
Output schema MUST include action details — Every side-effect node must return what it did (or would have done), regardless of whether the action was actually performed:
messageSent: z.string().describe("The message that was sent")recipientEmail: z.string().describe("The email address the message was sent to")fieldsUpdated: z.record(z.string(), z.unknown()).describe("The fields that were updated and their new values")testMode: z.boolean().describe("Whether the node ran in test mode (action was skipped)")Guidelines for agents with side-effect tools — If an agent uses tools that have side effects, add explicit guidelines:
Side-effect nodes must support test mode via ctx.testMode. When ctx.testMode is true, the node should:
testMode: ctx.testMode in the outputExample pattern for side-effect nodes:
execute: async (ctx, inputs) => {
const message = `Summary for ${inputs.recipientName}: ...`;
if (!ctx.testMode) {
// Actually send the message
await slackClient.postMessage({ channel: inputs.slackChannel, text: message });
}
return {
messageSent: message,
channelName: inputs.slackChannel,
recipientName: inputs.recipientName,
testMode: ctx.testMode,
};
},
The key insight: the output schema is the SAME in both modes. The node always returns what it did (or would have done). Test mode only skips the external call.
| Category | Description | Examples |
|---|---|---|
| Built-in nodes | Ships with crayon | webRead |
| Provider tools | From AI SDK providers (see import below) | openai.tools.webSearch(), openai.tools.codeInterpreter() |
| User nodes | Custom nodes in src/crayon/nodes/ | enrichCompany, sendSlackMessage |
Common mappings:
| Need | Tool | Category |
|---|---|---|
| Fetch web pages | webRead | builtin |
| Search the web | openai.tools.webSearch() | provider |
| Run Python code | openai.tools.codeInterpreter() | provider |
| Domain-specific (CRM, email) | User must implement | user node |
Provider tool imports: Provider tools require creating a provider instance from @ai-sdk/openai (NOT from crayon or openai):
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI();
// Then use: openai.tools.webSearch(), openai.tools.codeInterpreter(), etc.
For each node/agent file (src/crayon/nodes/<name>.ts or src/crayon/agents/<name>.ts), update:
description field — add typed schemas and (for agents) tools/guidelines:Refined node description:
<Expanded description>
**Implementation:** <SDK, library, or approach>
**Input Description:** <original from create-workflow>
**Output Description:** <original from create-workflow>
Refined agent description:
<Expanded description>
**Implementation:** <SDK, library, or approach>
**Tools needed:**
- webRead (builtin)
- openai.tools.webSearch() (provider)
- myCustomNode (user node in src/crayon/nodes/my-custom-node.ts)
**Guidelines:** <specific guidelines>
**Input Description:** <original from create-workflow>
**Output Description:** <original from create-workflow>
The inputSchema and outputSchema — replace empty z.object({}) with proper Zod types
For agents: the integrations array — add the model provider (e.g. "openai", "anthropic") and any external services the agent needs. This is how the framework knows to fetch API keys at runtime via ctx.getConnection().
For agents: the tools record — add tool imports and entries based on **Tools needed:**
For agents: the spec file (src/crayon/agents/<name>.md) — update guidelines, output format sections, and optionally add model and maxSteps to the YAML frontmatter:
model: openai/gpt-4o — override the default model (use when a specific model is needed, e.g. cheaper model for simple tasks, stronger model for complex reasoning)maxSteps: 10 — max tool-call iterations (increase for agents that need many sequential tool calls, decrease for simple single-shot agents)After writing all refinements:
create_version MCP tool with a message like:
Refine nodes: <workflow-name>
<Describe what changed — like a good git commit message body.>
/crayon:compile-workflow to regenerate the workflow's run() method with proper typesA workflow has an enrich-lead agent with empty schemas from create-workflow:
Before (src/crayon/agents/enrich-lead.ts):
export const enrichLead = Agent.create({
name: "enrich-lead",
integrations: ["openai"],
description: `
Searches the web to find additional professional info about a lead.
**Input Description:** A lead's name, email, and any known company or title from Salesforce.
**Output Description:** Enriched lead info including LinkedIn URL, company, job title, and summary.
`,
inputSchema: z.object({}),
outputSchema: z.object({}),
tools: {},
specPath: path.resolve(__dirname, "./enrich-lead.md"),
});
After refinement — schemas filled, description enriched, tools added:
import { createOpenAI } from "@ai-sdk/openai";
const openai = createOpenAI();
export const enrichLead = Agent.create({
name: "enrich-lead",
integrations: ["openai"],
description: `
Given a lead's name, email, and any existing Salesforce fields, searches the web to find
additional information: LinkedIn profile URL, current company, job title, and other
relevant professional details.
**Implementation:** OpenAI GPT-4o with web search tool
**Tools needed:**
- openai.tools.webSearch() (provider)
**Guidelines:** Search by name + email domain or company. Prioritize LinkedIn as primary source. Do not fabricate URLs.
**Input Description:** A lead's name, email, and any known company or title from Salesforce.
**Output Description:** Enriched lead info including LinkedIn URL, company, job title, and summary.
`,
inputSchema: z.object({
name: z.string().describe("Lead's full name"),
email: z.string().describe("Lead's email address"),
company: z.string().nullable().describe("Company from Salesforce, if known"),
title: z.string().nullable().describe("Title from Salesforce, if known"),
}),
outputSchema: z.object({
name: z.string(),
email: z.string(),
linkedinUrl: z.string().nullable().describe("LinkedIn profile URL if found"),
company: z.string().nullable().describe("Current company if found"),
jobTitle: z.string().nullable().describe("Current job title if found"),
summary: z.string().describe("Brief summary of findings"),
}),
tools: {
webSearch: openai.tools.webSearch({}),
},
specPath: path.resolve(__dirname, "./enrich-lead.md"),
});
What changed:
webSearch added based on Tools neededany or untyped fields