agentsclimarketplace

Generate workflows

Skill Zpankz/create-workflow/skills/generate-workflows

Generate .workflow.js scripts that compose agents into automated pipelines using the Claude Code Workflow DSL. Produces both .js scripts and equivalent skills by default. Covers full DSL surface (agent, pipeline, parallel, phase, log, workflow, budget, args), sandbox constraints, anti-patterns, and production-ready generation guidance. Invoked by scaffold or standalone via /generate-workflows.From its SKILL.md

Install
npx -y skills add Zpankz/create-workflow --skill generate-workflows

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

22.7 KB, as published. Nobody here has run it

Generate Workflows

Produce .claude/workflows/*.workflow.js scripts — the orchestration layer that composes agents into automated pipelines using deterministic JavaScript control flow.

Role in the System

Workflows are the nervous system — they coordinate agents, invoke skills, respect hooks, and follow rules. Every other primitive exists to serve the workflow.

Workflow orchestrates:
  → Agents (who does each step)
    → Skills (what each agent knows)
  → Hooks (automatic quality gates)
    → Rules (what hooks enforce)
  → CLAUDE.md (shared context for all participants)

Cross-Tool Scope

.workflow.js scripts are Claude Code only — gated behind CLAUDE_CODE_WORKFLOWS=1. For non-Claude-Code targets (Codex, Hermes, Pi, Antigravity), generate skill-only output. The skill fallback works on all tools.

Input

Read .claude/scaffold-decisions.md if it exists — primary source for resolved grill decisions. Extract the end-to-end workflow from decision records.


Workflow Design Process

1. Confirm the Gate

Check if CLAUDE_CODE_WORKFLOWS=1 is set. If not, the .js file is still worth writing — but tell the user they must enable it:

export CLAUDE_CODE_WORKFLOWS=1 && claude

Or persistently in .claude/settings.local.json:

{ "env": { "CLAUDE_CODE_WORKFLOWS": "1" } }

2. Decide Whether a Workflow Is the Right Tool

The jobRight tool
One subagent, one taskPlain Agent tool — no workflow
Reusable procedure where Claude picks stepsA Skill
Many subagents in a fixed shape, deterministic, resumableA Workflow

A workflow earns its cost when ALL of these hold: work is parallel or multi-stage; orchestration should be deterministic and resumable; isolating each step in fresh context is an advantage.

3. Find the Shape

Answer these before writing code — they pick the topology:

  1. Unit of work — what one subagent does once (review one file, research one question)
  2. Count — known list → map. Unknown count → loop.
  3. Topology — independent units → fan-out. Ordered stages → pipeline. Target count or budget cap → loop.
  4. Barrier needed? — Does a later step need ALL earlier results at once (dedup, merge, count-based exit)? Yes → parallel(). No → pipeline().
  5. Structured handoff? — Does a step need typed data back? → add schema.

4. The Decision That Matters Most: pipeline vs parallel

pipeline is the default for multi-stage work. Each item flows through every stage on its own — no barrier between stages. Item A can be in stage 3 while item B is in stage 1.

parallel is a barrier. It waits for every task before returning. Use ONLY when a step genuinely needs the entire previous result set:

  • Dedup or merge across the full set
  • Early-exit on a total count ("0 findings → skip verification")
  • A prompt that compares one item against all others

Not justified by: "I need to flatten/filter first" (do that inside a pipeline stage) or "it is cleaner" (a pipeline models separate stages fine).


The DSL — Complete Reference

Script Structure

Every workflow has exactly two parts:

Part 1 — meta block (must be the first statement)

export const meta = {
  name: "deploy-pipeline",
  description: "Build, test, stage, smoke-test, and promote",
  whenToUse: "Before deploying to production",
  phases: [
    { title: "build", detail: "Compile artifacts" },
    { title: "test", detail: "Run unit and integration tests" },
    { title: "verify", detail: "Smoke test staging", model: "haiku" }
  ]
};

meta MUST be a pure literal — no variables, function calls, spreads, or template strings inside it. Build dynamic values in the body, never in meta.

meta.phases[].model is display-only — it appears in the permission dialog but does NOT set the model at runtime. Set model on each agent() call separately.

Part 2 — Body (async JavaScript)

Everything after meta is the body. Top-level await works. These globals are injected — do not import anything:

GlobalPurpose
agent(prompt, opts?)Spawn one fresh-context subagent
pipeline(items, s1, s2, …)Stream items through stages, no barrier
parallel(thunks)Run an array of thunks concurrently (a barrier)
phase(title)Group agents under a heading in /workflows
log(msg)Emit a narrator line above the progress tree
consoleStandard console, routed into the workflow log
budget{ total, spent(), remaining() } — for budget-aware loops
argsWhatever was passed — passed through unchanged. undefined if nothing
workflow(name, args?)Run another workflow inline (one level of nesting only)

The body's return value becomes the tool result handed back to Claude.

agent(prompt, opts?)

The core primitive. Returns the agent's final text as a string, or a validated object if schema is set. Returns null if the user skips it.

Valid options:

OptionTypePurpose
model'haiku' | 'sonnet' | 'opus' | 'inherit' | modelIdModel selection. Omit → inherits session model
schemaJSON Schema objectForce structured output. AJV-validated, agent retries on mismatch
labelstringDisplay label for the agent call
phasestringAssociate with a named phase
isolation'worktree'Run in fresh git worktree (~200-500ms overhead)
agentTypestringAgent definition to use (e.g., custom .claude/agents/*.md)
stallMsnumberOverride stall timeout (default 180000ms = 3min)

There is NO tools option. Agents have access to all tools in the session — you cannot restrict per-agent.

pipeline(items, stage1, stage2, …)

Stream items through staged functions with NO barrier between stages:

const results = await pipeline(
  files,
  (file) => agent(`Review ${file} for bugs`, { schema: FINDINGS }),
  (review) => parallel(
    (review?.findings ?? []).map(f => () =>
      agent(`Verify finding: ${f.title}`, { schema: VERDICT })
    )
  )
);

Each item flows independently — item A can finish stage 2 while item B is still in stage 1.

parallel(thunks)

Run an array of thunks concurrently and wait for all. Takes a single array argument, NOT variadic.

// CORRECT — single array argument
const results = await parallel([
  () => agent("Security review"),
  () => agent("Performance review"),
  () => agent("Style review")
]);

// WRONG — variadic form throws
const results = await parallel(
  () => agent("Security review"),
  () => agent("Performance review")
);

Thunks, not promises. Must be [() => agent(…)], never [agent(…)]. Bare calls start immediately and defeat the concurrency limiter.

Always .filter(Boolean). A thunk that throws or is skipped resolves to null. Result arrays have holes by design:

const results = await parallel(items.map(i => () =>
  agent(`Process ${i}`, { schema: RESULT })
));
const valid = results.filter(Boolean);

args Normalization

args is passed through unchanged — an object stays an object, a string stays a string, nothing gives undefined. Always normalize before reading fields:

const input = typeof args === 'string'
  ? (() => { try { return JSON.parse(args) } catch { return args } })()
  : args;
const threshold = input?.minUsers ?? 20;

Do NOT call JSON.parse(args) unconditionally — if args is already an object, that throws.

Budget-Aware Loops

while (budget.total && budget.remaining() > 50_000) {
  const batch = await agent(`Find more issues`, { schema: FINDINGS });
  if (!batch?.findings?.length) break;
  allFindings.push(...batch.findings);
}

Critical: Without the budget.total guard, remaining() is Infinity → the loop sprints into the 1000-agent lifetime cap and throws. Always guard with budget.total &&.


Sandbox Constraints

ConstraintDetails
Script size524,288 bytes max
No filesystemNo require, import, fs, path, process
No nondeterminismDate.now(), argless new Date(), Math.random() throw
Lifetime cap1000 total agent calls per workflow run
Concurrent agentsmin(16, max(2, cores-2)) — excess calls queue
Stall timeout3 minutes default per agent (override with stallMs)
VM timeout30 seconds synchronous execution
Nestingworkflow() calls limited to one level

Why nondeterminism is banned: Resume caching. resumeFromRunId replays completed agent() calls when prompt+opts are unchanged. Timestamps would break cache keys.

Pass timestamps via args and stamp results after the workflow returns. Vary "randomness" by loop index.

All file read/write/Bash work belongs inside an agent() call — agents have tools, the orchestrator does not.


Model Selection Strategy

Work typeModelRationale
Cheap, high-volume, mechanical (classification, summarization)'haiku'3x cost savings, 90% capability
Judgment-heavy analysis, code review'sonnet' or inheritedBest coding balance
Architecture decisions, complex reasoning'opus'Deepest reasoning

Set model in BOTH the meta.phases[] entry (honest permission dialog) and the agent() call (actual runtime effect).

No validation on model strings. A typo ('hauku') is silently passed through and the agent fails later. Spell exactly.


Resume and Caching

Same script + same args = 100% cache hit on resume. To iterate:

  1. Edit the saved workflow file
  2. Re-invoke with Workflow({ scriptPath, resumeFromRunId })
  3. Every agent() call before your edit replays instantly from cache
  4. Only the changed call and everything after it re-runs

Never re-paste the whole script after the first run — edit the file.


Sandbox Validation

After generating a .js file, scan for violations before writing:

# Check for filesystem/Node.js API violations
grep -nE 'require\(|import .* from .node:|fs\.|path\.' "$FILE"

# Check for nondeterminism violations
grep -nE 'Date\.now\(\)|new Date\(\)|Math\.random\(\)' "$FILE"

# Check file size
wc -c < "$FILE"  # must be < 524288

If the plugin's linter is available, run it from the plugin root:

node scripts/validate-workflow.mjs "$FILE"

Fix all violations before writing the file.


Common Workflow Patterns

Fan-Out Research

Independent research from multiple angles, then synthesize.

export const meta = {
  name: "research",
  description: "Fan-out research, verify claims, synthesize cited report",
  phases: [
    { title: "Gather" },
    { title: "Verify" },
    { title: "Synthesize" }
  ]
};

const QUESTIONS = typeof args === 'string'
  ? (() => { try { return JSON.parse(args) } catch { return { questions: [args] } } })()
  : args;
const questions = QUESTIONS?.questions ?? ["What are the main findings?"];

phase("Gather");
const research = await parallel(
  questions.map(q => () =>
    agent(`Research: ${q}`, { label: `research:${q.slice(0, 30)}`, phase: "Gather", schema: {
      type: "object",
      required: ["findings"],
      properties: { findings: { type: "array", items: { type: "string" } } }
    }})
  )
);

phase("Verify");
const verified = await parallel(
  research.filter(Boolean).flatMap(r =>
    (r.findings ?? []).map(f => () =>
      agent(`Verify this claim. Try to refute it: ${f}`, { model: "haiku", phase: "Verify", schema: {
        type: "object",
        required: ["isReal"],
        properties: { isReal: { type: "boolean" }, evidence: { type: "string" } }
      }})
    )
  )
);

phase("Synthesize");
const report = await agent(
  `Synthesize a cited report from verified findings:\n${JSON.stringify(verified.filter(Boolean))}`,
  { model: "opus", phase: "Synthesize" }
);

return report;

Multi-Stage Pipeline (Review → Verify)

Each item flows through stages independently — no wasted wall-clock.

export const meta = {
  name: "review-verify",
  description: "Review across dimensions, adversarially verify each finding",
  phases: [{ title: "Review" }, { title: "Verify" }]
};

const FINDINGS = { type: "object", required: ["findings"], properties: {
  findings: { type: "array", items: { type: "object", required: ["title", "file"],
    properties: { title: { type: "string" }, file: { type: "string" } } } } } };

const VERDICT = { type: "object", required: ["isReal"], properties: {
  isReal: { type: "boolean" }, reason: { type: "string" } } };

const DIMENSIONS = [
  { key: "bugs", prompt: "Find logic bugs in changed files." },
  { key: "perf", prompt: "Find performance regressions." },
  { key: "tests", prompt: "Find missing test coverage." }
];

const results = await pipeline(
  DIMENSIONS,
  d => agent(d.prompt, { label: `review:${d.key}`, phase: "Review", schema: FINDINGS }),
  review => parallel(
    (review?.findings ?? []).map(f => () =>
      agent(`Adversarially verify: ${f.title} (${f.file})`,
        { label: `verify:${f.file}`, phase: "Verify", model: "haiku", schema: VERDICT })
        .then(v => ({ ...f, verdict: v }))
    )
  )
);

const confirmed = results.flat().filter(Boolean).filter(f => f.verdict?.isReal);
return { confirmedCount: confirmed.length, confirmed };

Dev Pipeline (Build → Quality → Review)

Sequential phases with parallel quality gates.

export const meta = {
  name: "dev-pipeline",
  description: "Implement, run quality gates in parallel, review, verify",
  phases: [
    { title: "implement" },
    { title: "quality", model: "haiku" },
    { title: "review" },
    { title: "verify" }
  ]
};

const input = typeof args === 'string'
  ? (() => { try { return JSON.parse(args) } catch { return args } })()
  : args;
const task = input?.task ?? input ?? "Implement the requested change";

phase("implement");
log("Implementing changes...");
const impl = await agent(`Read CLAUDE.md for architecture. Task: ${task}`, {
  model: "opus", label: "implementer", phase: "implement"
});

phase("quality");
log("Running quality gates...");
const [lint, types, tests] = await parallel([
  () => agent("Run linter, fix issues, report remaining", {
    model: "haiku", label: "linter", phase: "quality"
  }),
  () => agent("Run type checker, report errors", {
    model: "sonnet", label: "type-check", phase: "quality"
  }),
  () => agent("Run test suite, report failures", {
    model: "sonnet", label: "test-runner", phase: "quality"
  })
]);

phase("review");
const review = await agent(
  `Review changes. Quality results:\nLint: ${lint ?? "skipped"}\nTypes: ${types ?? "skipped"}\nTests: ${tests ?? "skipped"}`,
  { model: "opus", label: "reviewer", phase: "review", schema: {
    type: "object",
    required: ["approved", "blocking"],
    properties: {
      approved: { type: "boolean" },
      blocking: { type: "number" },
      findings: { type: "array", items: { type: "string" } }
    }
  }}
);

if (review && !review.approved && review.blocking > 0) {
  log(`Fixing ${review.blocking} blocking issues...`);
  await agent(`Fix these issues:\n${JSON.stringify(review.findings)}`, {
    model: "opus", label: "fix-blockers", phase: "review"
  });
}

phase("verify");
const result = await agent("Run full quality suite. Report pass/fail with evidence.", {
  model: "sonnet", label: "verifier", phase: "verify", schema: {
    type: "object",
    required: ["passed"],
    properties: { passed: { type: "boolean" }, summary: { type: "string" } }
  }
});

log(`Pipeline ${result?.passed ? "PASSED" : "FAILED"}`);
return result;

Budget-Aware Bug Hunt

Loop until budget exhausted or target count reached.

export const meta = {
  name: "bughunt",
  description: "Find bugs until budget runs out or target count reached",
  phases: [{ title: "Hunt" }, { title: "Verify" }]
};

const input = typeof args === 'string'
  ? (() => { try { return JSON.parse(args) } catch { return args } })()
  : args;
const target = input?.target ?? 10;

const allBugs = [];
let round = 0;

phase("Hunt");
while (budget.total && budget.remaining() > 50_000 && allBugs.length < target) {
  round++;
  log(`Round ${round}, ${allBugs.length} bugs so far...`);
  const batch = await agent(
    `Find bugs not in this list:\n${JSON.stringify(allBugs.map(b => b.title))}`,
    { label: `hunt:r${round}`, phase: "Hunt", schema: {
      type: "object", required: ["bugs"],
      properties: { bugs: { type: "array", items: {
        type: "object", required: ["title", "file"],
        properties: { title: { type: "string" }, file: { type: "string" } }
      }}}
    }}
  );
  if (!batch?.bugs?.length) break;
  allBugs.push(...batch.bugs);
}

phase("Verify");
const verified = await parallel(
  allBugs.map(b => () =>
    agent(`Verify bug: ${b.title} in ${b.file}. Is it real?`, {
      model: "haiku", label: `verify:${b.title.slice(0, 20)}`, phase: "Verify", schema: {
        type: "object", required: ["isReal"],
        properties: { isReal: { type: "boolean" }, reason: { type: "string" } }
      }
    }).then(v => ({ ...b, verdict: v }))
  )
);

const confirmed = verified.filter(Boolean).filter(b => b.verdict?.isReal);
return { total: allBugs.length, confirmed: confirmed.length, bugs: confirmed };

Using Custom Agent Types

Reference .claude/agents/*.md definitions:

const review = await agent("Review the auth module for vulnerabilities", {
  agentType: "security-reviewer",
  label: "security-audit",
  phase: "review"
});

The definition's model, instructions, and persona are honored. The agent still has full tool access.


Anti-Patterns — What NOT to Generate

Anti-patternProblemFix
parallel(() => agent(...), () => agent(...))Variadic form — throwsparallel([() => agent(...), () => agent(...)])
[agent(...), agent(...)] inside parallelBare calls, not thunks[() => agent(...), () => agent(...)]
Missing null handling after parallel/pipelineNull slots from skipped/failed agents.filter(Boolean) on arrays, or ?? fallback on destructured vars
const [a, b] = await parallel([...]).then(r => r.filter(Boolean))Filtering shifts positions — b gets a's value if a is nullDestructure without filter, handle nulls: a ?? "skipped"
Dynamic values in meta blockParser rejects non-literal metaKeep meta pure literal, compute in body
agent(prompt, { tools: [...] })tools is not a valid optionRemove — agents inherit session tools
Math.random() / Date.now()Sandbox throws on nondeterminismPass timestamps via args, vary by index
require('fs') / importNo Node APIs in sandboxAll file work inside agent() calls
Unguarded budget loopRuns to 1000-agent capGuard: budget.total && budget.remaining() > 50_000
Barrier when not neededparallel() wastes fast items' idle timeUse pipeline() unless you need the full set
Model set only on meta.phases[]Display-only, doesn't set runtime modelSet model on each agent() call
JSON.parse(args) unconditionallyThrows if args is already an objectNormalize: parse only when typeof args === 'string'

Dual-Track Strategy

Default: generate both. Every workflow gets a .workflow.js script AND an equivalent skill.

  1. Workflow script (.claude/workflows/<name>.workflow.js) — primary orchestration artifact
  2. Skill (.claude/skills/<name>/SKILL.md) — equivalent logic using Agent tool calls

Use --js-only to skip skill generation, or --skill-only to skip .js generation.

Cross-reference both artifacts to prevent drift:

  • In the skill: <!-- Workflow equivalent: .claude/workflows/<name>.workflow.js -->
  • In the .js: // Skill equivalent: .claude/skills/<name>/SKILL.md
  • Both: // WARNING: If you modify this file, update its counterpart to stay in sync

The skill mirrors workflow logic using Agent tool calls instead of DSL agent() calls.

Cross-Tool Target Matrix

Target tool.workflow.jsSkillNotes
Claude CodeYesYesFull dual-track
Antigravity/GeminiNoYes.workflow.js is Claude Code only
CodexNoYesSkill uses Agent tool
HermesNoYesSkill adapted to Hermes patterns
PiNoYesSkill adapted to Pi patterns

Caps and Limits

LimitValueBehavior
Lifetime agents1000 per workflow runThrows on exceed
Concurrent agentsmin(16, max(2, cores-2))Excess calls queue, all finish
Script size524,288 bytesRejected at parse time
Stall timeout180,000ms (3min) defaultOverride with stallMs per agent
VM timeout30,000ms synchronousThrows if orchestrator JS blocks
Nesting depth1 levelworkflow() from child throws
Worktree overhead~200-500ms + disk per agentUse only when parallel agents mutate files

Generation Rules

  • Use schema for any result a later line reads a field off — keep schemas small and required-tight
  • Add phase() markers for any workflow with 3+ stages
  • Use log() for user-visible progress, not debugging
  • Keep scripts focused — one workflow per concern
  • Name workflows by what they accomplish, not how they work
  • Stringify data into next prompt for inter-stage handoff (JSON.stringify) — orchestrator shares no memory with subagents
  • Always normalize args before reading fields
  • Always handle nulls from parallel() and pipeline() — .filter(Boolean) on arrays, ?? fallback on destructured vars (never both)
  • For Haiku phases, set model in both meta.phases[] AND agent() calls

Upstream Dependencies

When invoked standalone, check for upstream primitives:

  • Agents (.claude/agents/): If empty, warn: "No agents found. Workflows can use agentType to reference agent definitions."
  • Skills (.claude/skills/): If empty, warn similarly.
  • CLAUDE.md: If missing, warn: "No CLAUDE.md found. Agent prompts should reference it for shared context."

Informational, not blocking.

Standalone Invocation

/generate-workflows

$ARGUMENTS:

  • --name=deploy,review — Generate only specified workflows
  • --js-only — Generate only .workflow.js scripts (skip skill fallbacks)
  • --skill-only — Generate only skill equivalents (skip .js scripts)
  • --quick-grill — Abbreviated interrogation (3-5 questions)
  • --template=research|review|deploy|investigate|bughunt — Start from a template

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.