Naming workflow steps effectively
Step naming is an important design decision that affects workflow readability and maintainability. After analyzing multiple pgflow projects, these patterns have proven effective.
Recommended approach: Hybrid naming
Section titled “Recommended approach: Hybrid naming”Ask yourself: “What matters more - the returned data or that the action happened?”
- Use nouns when downstream steps process the returned data meaningfully
- Use verb-noun when the side effect is the primary purpose, even if data is returned
Result-focused naming (nouns)
Section titled “Result-focused naming (nouns)”// These steps produce data that other steps consume.step({ slug: "website" }, ...) // Returns website content.step({ slug: "summary", dependsOn: ["website"] }, ...) // Returns summary text.map({ slug: "userProfiles", array: "userIds" }, ...) // Returns user profile objectsAction-focused naming (verb-noun)
Section titled “Action-focused naming (verb-noun)”// These steps are primarily about their side effects.step({ slug: "saveToDb", dependsOn: ["summary"] }, ...) // Side effect: database write.map({ slug: "sendEmails", array: "users" }, ...) // Side effect: emails sent.map({ slug: "resizeImages", array: "images" }, ...) // Action: transformationWhy this works well
Section titled “Why this works well”-
Result-focused steps with noun names create intuitive property access:
// Natural to access data({ website, userProfiles }) => {return analyzeData(website.content, userProfiles);} -
Action-focused steps with verb names clearly communicate their purpose:
// Clear that this is about the action, not the return value.step({ slug: "notifyAdmins" }, async () => {await sendSlackMessage("Process complete");return { notified: true }; // Simple confirmation}) -
The distinction helps readers quickly understand whether a step exists to:
- Produce data for the workflow (noun)
- Perform an action with side effects (verb-noun)
The nuance: Many steps do both
Section titled “The nuance: Many steps do both”Most real-world steps both perform actions AND return data. The naming should reflect the primary purpose:
// Primary purpose: Send notifications (side effect)// The success/failure data is just metadata about the action.map({ slug: "sendNotifications" }, async (user) => { const sent = await sendEmail(user); return { userId: user.id, sent, timestamp: Date.now() };}).step({ slug: "updateDatabase", dependsOn: ["sendNotifications"] }, (input) => { // Even though we use the results, the primary goal was sending const successful = input.sendNotifications.filter(r => r.sent); await markUsersNotified(successful);});
// Primary purpose: Get user profiles (data)// The API call is just how we get the data.map({ slug: "userProfiles" }, async (userId) => { return await fetchUserProfile(userId); // Side effect: API call}).step({ slug: "analyze", dependsOn: ["userProfiles"] }, (input) => { // We care about the profile data, not that an API was called return analyzeProfiles(input.userProfiles);});Use camelCase for step slugs
Section titled “Use camelCase for step slugs”.step({ slug: "websiteContent" }, ...) // Correct.step({ slug: "website_content" }, ...) // AvoidStep slugs are used as identifiers in TypeScript and must match exactly when referenced in dependency arrays. Following JavaScript conventions with camelCase helps maintain consistency.
While this guide recommends the hybrid pattern, the most important thing is consistency within your project. Document the chosen convention and apply it throughout the codebase.