Skip to content

Create Reusable Tasks

This guide explains best practices for creating reusable task functions that can be composed into pgflow workflows.

Tasks in pgflow should follow functional programming principles:

  1. Single Responsibility: Each task should do one thing well
  2. Pure Functions: Tasks should have minimal side effects
  3. Clear Interface: Well-defined inputs and outputs
  4. Reusability: Tasks should be designed for use across multiple flows

A well-designed task function should:

  • Accept specific parameters rather than the entire flow input object
  • Return a well-defined output structure
  • Be testable in isolation
  • Have no knowledge about other tasks or the overall flow
// Good: Task accepts direct content parameter
async function summarizeContent(content: string) {
// Process the content directly
return { summary: "Processed summary..." };
}
// In your flow:
.step(
{ slug: 'summary', dependsOn: ['website'] },
async (input) => await summarizeContent(input.website.content)
)
// Bad: Task expects specific step structure
async function summarizeWebsite(input: { website: { content: string } }) {
// Tightly coupled to previous step name and structure
return { summary: "Processed summary..." };
}
// In your flow:
.step(
{ slug: 'summary', dependsOn: ['website'] },
async (input) => await summarizeWebsite(input) // Passing entire input object
)

When creating reusable tasks:

  1. Accept Simple Parameters:

    • Take in only what the task needs (strings, numbers, objects)
    • Don’t expect specific step names or flow structures
  2. Return JSON-Compatible Data:

    • All returned data must be serializable to JSON
    • Use primitive types: strings, numbers, booleans, null, plain objects, arrays
    • Convert dates to ISO strings: new Date().toISOString()
    • Avoid class instances, functions, symbols, undefined, and circular references
  3. Clear Documentation:

    • Document input parameters and their types
    • Document the shape of returned data
    • Include examples when helpful
  4. Error Handling:

    • Gracefully handle expected errors
    • Provide meaningful error messages
    • Consider retry strategies for transient issues

A common pattern is to organize tasks in a dedicated directory:

  • Directorysupabase
    • Directoryfunctions
      • Directory_tasks
        • scrapeWebsite.ts
        • summarizeWithAI.ts
        • extractTags.ts
        • saveWebsite.ts
      • Directory_flows
        • analyze_website.ts

This makes tasks easy to discover, import, and reuse across different flows.

Example: Reusable Task for Website Scraping

Section titled “Example: Reusable Task for Website Scraping”
supabase/functions/_tasks/scrapeWebsite.ts
/**
* Fetches website content from a URL
*/
export default async function scrapeWebsite(url: string) {
console.log(`Fetching content from: ${url}`);
// Implementation details...
const response = await fetch(url);
const html = await response.text();
return {
content: html,
metadata: {
url,
fetched_at: new Date().toISOString()
}
};
}

Import and use tasks in flow definitions:

supabase/functions/_flows/analyze_website.ts
import { Flow } from 'npm:@pgflow/dsl';
import scrapeWebsite from '../_tasks/scrapeWebsite.ts';
import summarizeWithAI from '../_tasks/summarizeWithAI.ts';
type Input = {
url: string;
};
export default new Flow<Input>({
slug: 'analyze_website',
})
.step(
{ slug: 'website' },
async (input) => await scrapeWebsite(input.run.url),
)
.step(
{ slug: 'summary', dependsOn: ['website'] },
async (input) => await summarizeWithAI(input.website.content),
);

Creating reusable tasks provides several advantages:

  1. Modularity: Build your flows from composable building blocks
  2. Testability: Test tasks in isolation without running entire flows
  3. Maintainability: Update task logic in one place, affecting all flows that use it
  4. Collaboration: Different team members can focus on specific tasks
  5. Reusability: Use the same task across multiple flows