Skip to content

Validation Steps

Validation steps catch invalid input early, before expensive operations run. Structure them to fail fast with no retries.

pgflow retries all exceptions based on maxAttempts. Without explicit validation, input errors waste retry attempts:

// Without validation - wastes retry attempts on invalid input
new Flow<{ email: string }>({ slug: 'sendEmail', maxAttempts: 5 })
.step(
{ slug: 'send' },
async (input) => {
if (!input.run.email.includes('@')) {
throw new Error('Invalid email'); // Retries 5 times!
}
return await sendEmail(input.run.email);
}
)

With explicit validation, failures stop immediately:

// With validation - fails immediately on invalid input
new Flow<{ email: string }>({ slug: 'sendEmail' })
.step(
{ slug: 'validInput', maxAttempts: 1 },
(input) => {
if (!input.run.email.includes('@')) {
throw new Error('Invalid email');
}
return input.run;
}
)
.step(
{ slug: 'send', dependsOn: ['validInput'], maxAttempts: 5 },
async (input) => await sendEmail(input.validInput.email)
)

Validation steps should be fast, synchronous functions that check input format and structure. Avoid async operations like database queries or API calls - those belong in separate steps with appropriate retry configuration.

// Good: Fast, synchronous validation
.step(
{ slug: 'validOrder', maxAttempts: 1 },
(input) => {
const { amount, items } = input.run;
if (amount <= 0) throw new Error('amount must be positive');
if (!items?.length) throw new Error('items cannot be empty');
return input.run;
}
)
// Bad: Async checks in validation
.step(
{ slug: 'validCustomer', maxAttempts: 1 },
async (input) => {
// Database lookups belong in separate steps with retries
const exists = await checkCustomerExists(input.run.customerId);
if (!exists) throw new Error('Customer not found');
return input.run;
}
)