Skip to content

Building Workflows

This guide covers how to build production-grade automation pipelines with vibeWorkflow. You’ll learn how to orchestrate multiple agents, manage cumulative context across stages, and build resilient workflows that handle complex tasks.

Use vibeWorkflow for:

  • Production automation - CI/CD pipelines, deployments, migrations
  • Multi-stage tasks - Tasks requiring sequential agent execution
  • Context accumulation - When later stages need data from earlier stages
  • Error recovery - Workflows that retry or adapt based on results

Use vibeTest instead for:

  • Quality gates - Pass/fail validation with assertions
  • Benchmarking - Model comparison and evaluation
  • Testing - Verifying agent behavior

Every workflow has three core components:

  1. Name - Identifier for logs and reports
  2. Function - Receives WorkflowContext (wf)
  3. Options - Configuration like timeout and defaults
import { vibeWorkflow } from '@dao/vibe-check';
vibeWorkflow(
'deployment pipeline', // 1. Name
async (wf) => { // 2. Function (receives WorkflowContext)
await wf.stage('build', { prompt: '/build' });
await wf.stage('deploy', { prompt: '/deploy' });
},
{ // 3. Options
timeout: 600_000, // 10 minutes
defaults: {
workspace: '/path/to/repo',
model: 'claude-opus-4-20250514'
}
}
);

Stages are the building blocks of workflows. Each stage executes an agent and returns a RunResult.

vibeWorkflow('simple pipeline', async (wf) => {
// Execute a stage
const result = await wf.stage('stage name', {
prompt: '/command',
workspace: '/optional/override'
});
// Access stage results
console.log('Files changed:', result.files.stats().total);
console.log('Cost:', result.metrics.cost.total);
});

Choose descriptive stage names that appear in logs and reports:

vibeWorkflow('deployment', async (wf) => {
// ✅ Good: Clear, actionable names
await wf.stage('validate environment', { ... });
await wf.stage('run database migrations', { ... });
await wf.stage('deploy application', { ... });
await wf.stage('smoke test endpoints', { ... });
// ❌ Bad: Vague or uninformative
await wf.stage('step1', { ... });
await wf.stage('do stuff', { ... });
await wf.stage('stage', { ... });
});

Stages execute sequentially. Later stages can reference earlier results:

vibeWorkflow('analysis pipeline', async (wf) => {
// Stage 1: Scan for vulnerabilities
const scan = await wf.stage('security scan', {
prompt: '/scan --output report.json'
});
// Stage 2: Use scan results in next stage
const fix = await wf.stage('fix vulnerabilities', {
prompt: `/fix issues from ${scan.bundleDir}/report.json`
});
// Stage 3: Verify fixes
const verify = await wf.stage('verify fixes', {
prompt: `/verify ${fix.files.changed().map(f => f.path).join(' ')}`
});
});

Unlike tests (where each runAgent starts fresh), workflows accumulate state across all stages. Access cumulative data via wf.files, wf.tools, and wf.timeline.

Track all file changes across the workflow:

vibeWorkflow('refactoring workflow', async (wf) => {
await wf.stage('extract utils', { prompt: '/extract-utils' });
await wf.stage('update imports', { prompt: '/update-imports' });
await wf.stage('fix tests', { prompt: '/fix-tests' });
// Access cumulative file changes
const allFiles = wf.files.changed();
console.log(`Total files modified: ${allFiles.length}`);
// Get specific file across all stages
const config = wf.files.get('vite.config.ts');
if (config) {
console.log('Config was changed in:', config.stage);
}
// Filter by glob pattern
const tests = wf.files.filter('**/*.test.ts');
console.log(`Test files affected: ${tests.length}`);
// Get statistics
const stats = wf.files.stats();
console.log('Added:', stats.added);
console.log('Modified:', stats.modified);
console.log('Deleted:', stats.deleted);
});

Track all tool calls across stages:

vibeWorkflow('migration workflow', async (wf) => {
await wf.stage('analyze codebase', { prompt: '/analyze' });
await wf.stage('migrate syntax', { prompt: '/migrate' });
// Get all tool calls
const allTools = wf.tools.all();
console.log(`Total tools used: ${allTools.length}`);
// Filter by tool name
const edits = wf.tools.filter('Edit');
console.log(`Total edits: ${edits.length}`);
// Check for specific tools
const usedBash = wf.tools.used('Bash');
if (usedBash) {
console.log('Workflow executed shell commands');
}
// Get success/failure counts
console.log('Succeeded:', wf.tools.succeeded().length);
console.log('Failed:', wf.tools.failed().length);
});

Access a unified timeline of events across all stages:

vibeWorkflow('deployment pipeline', async (wf) => {
await wf.stage('build', { prompt: '/build' });
await wf.stage('deploy', { prompt: '/deploy' });
// Iterate over all events
for await (const { stage, evt } of wf.timeline.events()) {
console.log(`[${stage}] ${evt.type} at ${evt.timestamp}`);
if (evt.type === 'tool_use') {
console.log(` Tool: ${evt.toolName}`);
}
}
});

Set workflow-level defaults that apply to all stages, with per-stage overrides when needed.

vibeWorkflow('monorepo deployment', async (wf) => {
// All stages inherit workspace from defaults
await wf.stage('build app', { prompt: '/build' });
await wf.stage('run tests', { prompt: '/test' });
// Override for specific stage
await wf.stage('deploy docs', {
prompt: '/deploy',
workspace: '/path/to/docs-repo' // Different repo
});
}, {
defaults: {
workspace: '/path/to/app-repo' // Default for most stages
}
});
vibeWorkflow('cost-optimized pipeline', async (wf) => {
// Simple tasks use faster model (from defaults)
await wf.stage('format code', { prompt: '/format' });
await wf.stage('update docs', { prompt: '/update-docs' });
// Complex task uses more capable model
await wf.stage('refactor architecture', {
prompt: '/refactor --scope=architecture',
model: 'claude-opus-4-20250514' // Override for this stage
});
}, {
defaults: {
model: 'claude-sonnet-4-5-20250929' // Fast model for most stages
}
});
vibeWorkflow('long-running migration', async (wf) => {
// Each stage gets 10-minute timeout
await wf.stage('analyze', { prompt: '/analyze' });
await wf.stage('migrate', { prompt: '/migrate' });
await wf.stage('verify', { prompt: '/verify' });
}, {
timeout: 600_000 // 10 minutes for entire workflow
});

Workflows handle errors differently than tests. Instead of failing immediately, they log errors and continue.

vibeWorkflow('resilient deployment', async (wf) => {
try {
const build = await wf.stage('build', { prompt: '/build' });
// Check for errors in logs
if (build.logs.some(log => log.includes('ERROR'))) {
console.error('Build had errors, aborting deployment');
return; // Exit workflow early
}
await wf.stage('deploy', { prompt: '/deploy' });
} catch (error) {
console.error('Deployment failed:', error);
// Optionally run cleanup stage
await wf.stage('rollback', { prompt: '/rollback' });
}
});

Execute stages based on previous results:

vibeWorkflow('conditional pipeline', async (wf) => {
const lint = await wf.stage('lint', { prompt: '/lint' });
// Only fix if linting found issues
const issues = lint.files.changed();
if (issues.length > 0) {
await wf.stage('fix lint issues', {
prompt: `/fix ${issues.map(f => f.path).join(' ')}`
});
}
const test = await wf.stage('test', { prompt: '/test' });
// Deploy only if tests passed
const testsPassed = test.tools
.filter('Bash')
.every(t => t.result?.includes('PASS'));
if (testsPassed) {
await wf.stage('deploy', { prompt: '/deploy' });
} else {
console.log('Tests failed, skipping deployment');
}
});

See Loop Patterns → for detailed retry strategies.


There are multiple ways to pass data between stages:

Each stage has a bundleDir containing artifacts:

vibeWorkflow('data pipeline', async (wf) => {
const extract = await wf.stage('extract data', {
prompt: '/extract --output data.json'
});
// Reference the bundle directory in next stage
const transform = await wf.stage('transform data', {
prompt: `/transform ${extract.bundleDir}/data.json`
});
const load = await wf.stage('load data', {
prompt: `/load ${transform.bundleDir}/transformed.json`
});
});

Read file content directly from results:

vibeWorkflow('config pipeline', async (wf) => {
const generate = await wf.stage('generate config', {
prompt: '/generate-config'
});
// Get the generated file
const configFile = generate.files.get('config.json');
const configContent = await configFile?.after?.text();
if (configContent) {
const config = JSON.parse(configContent);
// Use config data in next stage
await wf.stage('apply config', {
prompt: `/apply --env=${config.environment}`
});
}
});

Use workflow-level context to track state:

vibeWorkflow('incremental builder', async (wf) => {
await wf.stage('build step 1', { prompt: '/build-ui' });
await wf.stage('build step 2', { prompt: '/build-api' });
await wf.stage('build step 3', { prompt: '/build-infra' });
// Access all changed files across all stages
const allChanges = wf.files.changed();
// Package everything
await wf.stage('package', {
prompt: `/package ${allChanges.map(f => f.path).join(' ')}`
});
});

While stages execute sequentially, you can structure independent operations:

vibeWorkflow('multi-target deployment', async (wf) => {
// Build once
const build = await wf.stage('build', { prompt: '/build' });
// Deploy to each environment (sequential but independent)
const staging = await wf.stage('deploy staging', {
prompt: `/deploy --target=staging --artifact=${build.bundleDir}/dist`
});
const production = await wf.stage('deploy production', {
prompt: `/deploy --target=production --artifact=${build.bundleDir}/dist`
});
console.log('Deployed to:', staging.workspace, production.workspace);
});

Generate stage names programmatically:

vibeWorkflow('multi-service deployment', async (wf) => {
const services = ['auth', 'api', 'worker', 'frontend'];
for (const service of services) {
await wf.stage(`deploy ${service}`, {
prompt: `/deploy ${service}`,
workspace: `/path/to/${service}`
});
}
// Access by service name
const authDeployment = wf.files.filter('auth/**/*');
console.log(`Auth files changed: ${authDeployment.length}`);
});

Add validation between stages:

vibeWorkflow('safe deployment', async (wf) => {
const build = await wf.stage('build', { prompt: '/build' });
// Validate build artifacts
const distFiles = build.files.filter('dist/**/*');
if (distFiles.length === 0) {
throw new Error('Build produced no output files');
}
const test = await wf.stage('test', { prompt: '/test' });
// Validate test results
const testPassed = test.tools
.filter('Bash')
.every(t => !t.result?.includes('FAIL'));
if (!testPassed) {
throw new Error('Tests failed, aborting deployment');
}
await wf.stage('deploy', { prompt: '/deploy' });
});

Stage names appear in logs, reports, and timelines. Make them informative:

// ✅ Good
await wf.stage('validate database connection', { ... });
await wf.stage('run database migrations', { ... });
await wf.stage('seed test data', { ... });
// ❌ Bad
await wf.stage('step1', { ... });
await wf.stage('db', { ... });
await wf.stage('do thing', { ... });

Configure workspace and model at the workflow level:

vibeWorkflow('pipeline', async (wf) => {
// Stages inherit sensible defaults
await wf.stage('build', { prompt: '/build' });
await wf.stage('test', { prompt: '/test' });
}, {
defaults: {
workspace: process.cwd(),
model: 'claude-sonnet-4-5-20250929'
}
});

Don’t let one stage failure crash the entire workflow:

vibeWorkflow('robust pipeline', async (wf) => {
let buildSucceeded = false;
try {
await wf.stage('build', { prompt: '/build' });
buildSucceeded = true;
} catch (error) {
console.error('Build failed:', error);
}
if (buildSucceeded) {
await wf.stage('deploy', { prompt: '/deploy' });
} else {
await wf.stage('notify failure', { prompt: '/notify build-failed' });
}
});

Use workflow context to report results:

vibeWorkflow('metrics pipeline', async (wf) => {
const start = Date.now();
await wf.stage('process data', { prompt: '/process' });
const duration = Date.now() - start;
const stats = wf.files.stats();
const totalCost = wf.timeline.events()
.reduce((sum, { evt }) => sum + (evt.cost?.total || 0), 0);
console.log(`Pipeline completed in ${duration}ms`);
console.log(`Files changed: ${stats.total}`);
console.log(`Total cost: $${totalCost.toFixed(4)}`);
});

Use bundleDir to pass artifacts between stages:

vibeWorkflow('artifact pipeline', async (wf) => {
const build = await wf.stage('build', { prompt: '/build' });
// ✅ Good: Reference bundle directory
await wf.stage('package', {
prompt: `/package ${build.bundleDir}/dist`
});
// ❌ Bad: Hardcoded paths
await wf.stage('package', {
prompt: '/package .vibe-bundles/some-id/dist'
});
});

Now that you understand workflow fundamentals, explore:

Or dive into the API reference: