Skip to content

Your First Workflow

In this tutorial, you’ll build a multi-stage automation workflow that chains agents together to accomplish complex tasks. Unlike tests (which focus on validation), workflows are optimized for production automation with features like stage management, loops, and cumulative context.

A complete automation pipeline that:

  1. Analyzes a codebase for issues
  2. Fixes the issues found
  3. Tests the fixes
  4. Retries if tests fail (with a loop)
  5. Reports results

We’ll use vibeWorkflow instead of vibeTest to optimize for automation.


Before we start, understand the difference:

FeaturevibeTestvibeWorkflow
PurposeTesting & evaluationProduction automation
Assertions✅ Uses expect()❌ No assertions (logs instead)
Failure ModeFails test on errorContinues, reports errors
ContextVibeTestContext (with matchers)WorkflowContext (with stages)
Use CaseBenchmarking, quality gatesCI/CD, deployments, migrations

Let’s start with a simple 3-stage workflow:

  1. Create the workflow file

    Create workflows/refactor-pipeline.ts:

    import { vibeWorkflow } from '@dao/vibe-check';
    vibeWorkflow('refactor pipeline', async (wf) => {
    // Stage 1: Analyze code
    const analyze = await wf.stage('analyze', {
    prompt: '/analyze src/ --find-issues'
    });
    // Stage 2: Fix issues
    const fix = await wf.stage('fix', {
    prompt: `/fix issues from ${analyze.bundleDir}/summary.json`
    });
    // Stage 3: Run tests
    const test = await wf.stage('test', {
    prompt: '/test'
    });
    // Log results
    console.log('Pipeline complete!');
    console.log('Issues found:', analyze.files.stats().total);
    console.log('Files fixed:', fix.files.stats().modified);
    console.log('Tests passed:', test.tools.succeeded().length);
    });
  2. Run the workflow

    Terminal window
    vitest workflows/refactor-pipeline.ts
  3. See the output

    ✓ workflows/refactor-pipeline.ts (1)
    ✓ refactor pipeline (45.2s)
    - Stage: analyze (12.1s)
    - Stage: fix (28.3s)
    - Stage: test (4.8s)
    Pipeline complete!
    Issues found: 8
    Files fixed: 5
    Tests passed: 12

The WorkflowContext (passed as wf) provides:

Execute one stage of the workflow:

const result = await wf.stage('stage-name', {
prompt: 'What to do',
model: 'claude-3-5-sonnet-latest', // Optional
workspace: '/path/to/workspace', // Optional
// ... all RunAgentOptions
});

Returns an AgentExecution (awaitable) with the RunResult.

Access data across all stages:

vibeWorkflow('multi-stage', async (wf) => {
await wf.stage('stage1', { prompt: 'Create files' });
await wf.stage('stage2', { prompt: 'Modify files' });
// Access all files changed across both stages
const allFiles = wf.files.allChanged();
console.log('Total files changed:', allFiles.length);
// Get files from specific stage
const stage1Files = wf.files.byStage('stage1');
const stage2Files = wf.files.byStage('stage2');
// Access all tool calls with stage context
const allTools = wf.tools.all();
for (const { stage, call } of allTools) {
console.log(`[${stage}] Used ${call.name}`);
}
});

Access unified timeline across stages:

// Iterate over all events with stage context
for await (const { stage, evt } of wf.timeline.events()) {
console.log(`[${stage}] ${evt.type} at ${evt.timestamp}`);
}

Stages can communicate via:

vibeWorkflow('data passing', async (wf) => {
// Stage 1: Analyze and write report
const analyze = await wf.stage('analyze', {
prompt: 'Analyze src/ and write report to analysis.json'
});
// Stage 2: Read report and fix
const fix = await wf.stage('fix', {
prompt: 'Read analysis.json and fix all issues'
});
});
vibeWorkflow('using bundles', async (wf) => {
const analyze = await wf.stage('analyze', {
prompt: 'Analyze code'
});
// Pass bundle path to next stage
const fix = await wf.stage('fix', {
prompt: `Fix issues. Analysis results: ${analyze.bundleDir}`
});
});
vibeWorkflow('with context', async (wf) => {
const analyze = await wf.stage('analyze', {
prompt: 'Analyze code'
});
// Pass previous result as context
const fix = await wf.stage('fix', {
prompt: 'Fix issues found',
context: analyze // Passes full RunResult
});
});

The until() helper enables retry logic and iterative workflows:

vibeWorkflow('retry until success', async (wf) => {
// Run until tests pass (max 3 attempts)
const results = await wf.until(
(latest) => latest.tools.succeeded().length > 0,
() => wf.stage('test', { prompt: '/test' }),
{ maxIterations: 3 }
);
console.log(`Tests passed after ${results.length} attempts`);
});
vibeWorkflow('fix until clean', async (wf) => {
let iteration = 0;
const results = await wf.until(
(latest) => {
// Stop when no files changed (converged)
const filesChanged = latest.files.stats().total;
console.log(`Iteration ${iteration++}: ${filesChanged} files changed`);
return filesChanged === 0;
},
() => wf.stage('fix-iteration', {
prompt: '/fix --auto'
}),
{ maxIterations: 5 }
);
console.log(`Converged after ${results.length} iterations`);
});
vibeWorkflow('fix with budget', async (wf) => {
let totalCost = 0;
const results = await wf.until(
(latest) => {
totalCost += latest.metrics.totalCostUsd ?? 0;
// Stop if all issues fixed OR budget exceeded
const issuesRemaining = latest.tools.failed().length;
return issuesRemaining === 0 || totalCost > 10.0;
},
() => wf.stage('fix', { prompt: '/fix' }),
{ maxIterations: 10 }
);
console.log(`Total cost: $${totalCost.toFixed(2)}`);
});

Here’s a production-ready workflow with error handling, retries, and reporting:

import { vibeWorkflow } from '@dao/vibe-check';
import { writeFileSync } from 'fs';
vibeWorkflow('ci/cd pipeline', async (wf) => {
console.log('🚀 Starting CI/CD pipeline...\n');
// Stage 1: Lint and format
console.log('📝 Stage 1: Linting...');
const lint = await wf.stage('lint', {
prompt: 'Run linter and fix all issues automatically'
});
if (lint.tools.failed().length > 0) {
console.error('❌ Linting failed');
return; // Exit early
}
console.log('✅ Linting passed\n');
// Stage 2: Type checking
console.log('🔍 Stage 2: Type checking...');
const typecheck = await wf.stage('typecheck', {
prompt: 'Run TypeScript type checker and fix all errors'
});
if (typecheck.tools.failed().length > 0) {
console.error('❌ Type check failed');
return;
}
console.log('✅ Type check passed\n');
// Stage 3: Run tests with retries
console.log('🧪 Stage 3: Testing...');
const testResults = await wf.until(
(latest) => {
// Check if tests passed
const bashCalls = latest.tools.all().filter(t => t.name === 'Bash');
const testRun = bashCalls.find(t =>
t.input.command?.includes('vitest')
);
return testRun?.ok ?? false;
},
() => wf.stage('test-retry', {
prompt: '/test --fix-failures'
}),
{ maxIterations: 3 }
);
if (testResults[testResults.length - 1].tools.succeeded().length === 0) {
console.error('❌ Tests failed after 3 attempts');
return;
}
console.log(`✅ Tests passed (${testResults.length} attempts)\n`);
// Stage 4: Build
console.log('📦 Stage 4: Building...');
const build = await wf.stage('build', {
prompt: '/build --production'
});
if (build.tools.failed().length > 0) {
console.error('❌ Build failed');
return;
}
console.log('✅ Build succeeded\n');
// Generate report
const report = {
pipeline: 'ci/cd',
stages: {
lint: {
filesChanged: lint.files.stats().total,
cost: lint.metrics.totalCostUsd
},
typecheck: {
filesChanged: typecheck.files.stats().total,
cost: typecheck.metrics.totalCostUsd
},
test: {
attempts: testResults.length,
cost: testResults.reduce((sum, r) =>
sum + (r.metrics.totalCostUsd ?? 0), 0
)
},
build: {
duration: build.metrics.durationMs,
cost: build.metrics.totalCostUsd
}
},
totalCost: wf.files.allChanged().reduce((sum, f) => {
// Calculate from all stages (simplified)
return sum;
}, 0)
};
writeFileSync('pipeline-report.json', JSON.stringify(report, null, 2));
console.log('📊 Report saved to pipeline-report.json');
console.log('\n✅ Pipeline complete!');
});

Set defaults for all stages to avoid repetition:

vibeWorkflow('deployment', async (wf) => {
// All stages inherit these defaults
await wf.stage('deploy-backend', {
prompt: '/deploy backend'
// Uses workspace and model from defaults
});
await wf.stage('deploy-frontend', {
prompt: '/deploy frontend'
// Uses workspace and model from defaults
});
// Override defaults for specific stage
await wf.stage('deploy-docs', {
prompt: '/deploy',
workspace: '/path/to/docs-repo', // Override workspace
model: 'claude-3-5-haiku-latest' // Override model
});
}, {
// Workflow options with defaults
timeout: 600000, // 10 minutes
defaults: {
workspace: '/path/to/main-repo',
model: 'claude-3-5-sonnet-latest'
}
});

Some workflows span multiple repositories:

vibeWorkflow('full-stack deployment', async (wf) => {
// Deploy backend (main repo)
await wf.stage('deploy-backend', {
workspace: '/repos/backend',
prompt: '/deploy --production'
});
// Deploy frontend (different repo)
await wf.stage('deploy-frontend', {
workspace: '/repos/frontend',
prompt: '/deploy --production'
});
// Update docs (third repo)
await wf.stage('update-docs', {
workspace: '/repos/docs',
prompt: '/update-version'
});
});

Unlike tests, workflows should handle errors gracefully:

vibeWorkflow('resilient pipeline', async (wf) => {
const analyze = await wf.stage('analyze', {
prompt: '/analyze'
});
// Check for failures
if (analyze.tools.failed().length > 0) {
console.error('Analysis failed:', analyze.tools.failed());
// Try recovery stage
const recover = await wf.stage('recover', {
prompt: '/recover-from-failure'
});
if (recover.tools.failed().length > 0) {
console.error('Recovery failed. Aborting pipeline.');
return; // Exit workflow
}
}
// Continue with next stage
await wf.stage('deploy', {
prompt: '/deploy'
});
});

Like vibeTest, workflows support modifiers:

vibeWorkflow.skip('not ready', async (wf) => {
// Workflow is skipped
});
vibeWorkflow.only('focus on this', async (wf) => {
// Only this workflow runs
});
vibeWorkflow.todo('implement later', async (wf) => {
// Marked as TODO
});

vibeWorkflow('database migration', async (wf) => {
// Backup database
await wf.stage('backup', {
prompt: '/backup database --timestamp'
});
// Run migration
const migrate = await wf.stage('migrate', {
prompt: '/migrate database --production'
});
// Verify migration
const verify = await wf.stage('verify', {
prompt: '/verify migration succeeded'
});
if (verify.tools.failed().length > 0) {
// Rollback if verification failed
await wf.stage('rollback', {
prompt: '/rollback migration'
});
}
});
vibeWorkflow('monorepo refactor', async (wf) => {
const packages = ['@app/core', '@app/ui', '@app/api'];
for (const pkg of packages) {
await wf.stage(`refactor-${pkg}`, {
workspace: `/monorepo/packages/${pkg}`,
prompt: '/refactor --modernize'
});
}
// Update dependencies after all refactors
await wf.stage('update-deps', {
workspace: '/monorepo',
prompt: '/update dependencies --all-packages'
});
});
vibeWorkflow('generate docs', async (wf) => {
// Generate API docs from code
await wf.stage('api-docs', {
prompt: '/generate-docs --api'
});
// Generate guides from examples
await wf.stage('guides', {
prompt: '/generate-docs --guides'
});
// Build and deploy
await wf.stage('deploy-docs', {
workspace: '/repos/docs-site',
prompt: '/build-and-deploy'
});
});

vibeWorkflow('pipeline', async (wf) => {
console.log('Starting stage 1...');
const result = await wf.stage('stage1', { prompt: '...' });
console.log(`Stage 1 complete: ${result.files.stats().total} files changed`);
});
if (result.tools.failed().length > 0) {
console.error('Stage failed, attempting recovery...');
await wf.stage('recover', { prompt: '/recover' });
}
await wf.stage('analyze-typescript-errors', { prompt: '...' });
await wf.stage('fix-typescript-errors', { prompt: '...' });
await wf.stage('verify-typescript-errors-fixed', { prompt: '...' });
// ❌ Bad: Workflows don't use assertions
vibeWorkflow('bad', async (wf) => {
const result = await wf.stage('test', { prompt: '...' });
expect(result).toCompleteAllTodos(); // Wrong!
});
// ✅ Good: Log and handle errors
vibeWorkflow('good', async (wf) => {
const result = await wf.stage('test', { prompt: '...' });
if (result.todos.some(t => t.status !== 'completed')) {
console.error('Some TODOs incomplete');
}
});
// ❌ Bad: Missing await
const result = wf.stage('test', { prompt: '...' });
// ✅ Good: Always await
const result = await wf.stage('test', { prompt: '...' });

You’ve learned how to build automation workflows! Now explore: