Tasks
Tasks are reusable execution units. A task can wrap a shell command or custom logic, declare required environments, validate parameters, and run through the same runner that commands use.
When to Use Tasks
Use tasks when you need:
- Reusability: the same operation in multiple commands.
- Environment resolution: secrets or config loaded before execution.
- Validated parameters: typed input for the operation.
- Typed context checks: commands can only run tasks whose requirements they satisfy.
Exec vs Run Tasks
Exec Tasks
For simple shell commands:
const buildTask = defineTask({
label: 'Build',
exec: 'npm run build',
});
// With dynamic command
const migrateTask = defineTask({
label: 'Migrate',
params: z.object({ dryRun: z.boolean().default(false) }),
exec: (ctx) => `prisma migrate ${ctx.params.dryRun ? '--dry-run' : ''}`,
});Run Tasks
For complex logic:
const setupTask = defineTask({
label: 'Setup',
run: async (r, ctx) => {
ctx.reporter.info('Setting up...');
await r.exec('npm install');
await r.exec('npm run db:setup');
ctx.reporter.success('Ready!');
},
});Environment Patterns
Single Environment
const dbEnv = defineEnv({
resolver: secretsResolver,
vars: ['DATABASE_URL'],
});
const migrateTask = defineTask({
label: 'Migrate',
env: dbEnv,
exec: 'prisma migrate deploy', // DATABASE_URL available
});Multiple Environments
const deployTask = defineTask({
label: 'Deploy',
env: [dbEnv, apiEnv, awsEnv], // All merged
exec: (ctx) => {
// ctx.envs.DATABASE_URL
// ctx.envs.API_KEY
// ctx.envs.AWS_ACCESS_KEY
return 'deploy';
},
});Environment Caching
Resolved environments are cached for the runner session:
run: async (r) => {
await r.run(task1); // Resolves DATABASE_URL
await r.run(task2); // Uses cached DATABASE_URL
await r.exec('psql $DATABASE_URL'); // Also available
},Parameter Patterns
Basic Parameters
const deployTask = defineTask({
label: 'Deploy',
params: z.object({
env: z.enum(['staging', 'prod']),
version: z.string(),
}),
exec: (ctx) => `deploy --env ${ctx.params.env} --version ${ctx.params.version}`,
});
// Usage
await r.run(deployTask, { env: 'staging', version: '1.2.3' });Optional Parameters
const buildTask = defineTask({
label: 'Build',
params: z.object({
mode: z.enum(['dev', 'prod']).default('prod'),
sourcemaps: z.boolean().optional(),
}),
exec: (ctx) => {
let cmd = `build --mode ${ctx.params.mode}`;
if (ctx.params.sourcemaps) {
cmd += ' --sourcemaps';
}
return cmd;
},
});
// All valid
await r.run(buildTask);
await r.run(buildTask, { mode: 'dev' });
await r.run(buildTask, { mode: 'prod', sourcemaps: true });Complex Parameters
const deployTask = defineTask({
label: 'Deploy',
params: z.object({
services: z.array(z.string()),
config: z.object({
replicas: z.number(),
memory: z.string(),
}),
}),
run: async (r, ctx) => {
for (const service of ctx.params.services) {
await r.exec(`deploy ${service} --replicas ${ctx.params.config.replicas}`);
}
},
});Return Values
Tasks can return typed values:
const getVersionTask = defineTask({
label: 'Get version',
run: async (r, ctx) => {
const pkg = await Bun.file('package.json').json();
return pkg.version as string;
},
});
// Usage
const version = await r.run(getVersionTask);
// version is typed as stringWriting Environment Variables
For tasks that need to persist secrets:
const secretsEnv = defineEnv({
resolver: secretsResolver, // Must implement write()
vars: ['API_KEY'],
});
const rotateKeyTask = defineTask({
label: 'Rotate API key',
envWriter: secretsEnv,
run: async (r, ctx) => {
const newKey = await generateApiKey();
await ctx.writeEnvs({ API_KEY: newKey });
ctx.reporter.success('API key rotated');
},
});Task Context
The full TaskContext:
type TaskContext = {
context: TContext; // From parent command
cwd: string; // Project root
envs: TEnvs; // Resolved env vars
params: TParams; // Validated parameters
extraArgs: string[]; // Remaining CLI args
reporter: TaskReporter; // Event emission
writeEnvs: TWriteEnvs; // Write function (if envWriter)
};Accessing Command Context
const deployTask = defineTask({
label: 'Deploy',
env: envWithEnvContext, // Requires { env: string }
run: async (r, ctx) => {
// ctx.context.env comes from the parent command
ctx.reporter.info(`Deploying to ${ctx.context.env}`);
},
});Tabs with Tasks
Tasks also work with r.tabs():
run: async (r) => {
await r.tabs([
r.run(devServerTask),
r.run(watcherTask),
r.exec('stripe listen'),
]);
},The shortLabel property controls tab names:
const devServerTask = defineTask({
label: 'Development Server',
shortLabel: 'dev', // Tab shows "dev"
exec: 'npm run dev',
});Composition Patterns
Task Sequences
const deployTask = defineTask({
label: 'Deploy',
run: async (r, ctx) => {
await r.run(buildTask);
await r.run(testTask);
await r.run(pushTask, { env: ctx.params.env });
},
});Conditional Tasks
const releaseTask = defineTask({
label: 'Release',
params: z.object({
skipTests: z.boolean().default(false),
}),
run: async (r, ctx) => {
await r.run(buildTask);
if (!ctx.params.skipTests) {
await r.run(testTask);
}
await r.run(publishTask);
},
});Parallel Tasks
const setupTask = defineTask({
label: 'Setup',
run: async (r, ctx) => {
await r.parallel([r.run(installDepsTask), r.run(setupDbTask), r.run(generateTypesTask)]);
},
});Best Practices
1. Single Responsibility
Each task should do one thing:
// Good
const buildTask = defineTask({ exec: 'npm run build' });
const testTask = defineTask({ exec: 'npm run test' });
// Bad - too many concerns
const everythingTask = defineTask({
run: async (r) => {
await r.exec('npm install');
await r.exec('npm run build');
await r.exec('npm run test');
await r.exec('npm run deploy');
},
});2. Descriptive Labels
// Good
const migrateTask = defineTask({
label: 'Run database migrations',
shortLabel: 'migrate',
});
// Bad
const task1 = defineTask({
label: 'Do stuff',
});3. Type Your Parameters
// Good - explicit schema
params: z.object({
env: z.enum(['dev', 'staging', 'prod']),
version: z.string().regex(/^\d+\.\d+\.\d+$/),
}),
// Avoid - loose types
params: z.record(z.unknown()),4. Handle Errors Gracefully
run: async (r, ctx) => {
try {
await r.exec('risky-operation');
} catch (error) {
ctx.reporter.error('Operation failed');
ctx.reporter.info('Attempting recovery...');
await r.exec('recovery-operation');
}
},