Skip to content

Data Processing & ETL

Build complex data pipelines that process large datasets without timeouts.

Why Trigger.dev for ETL?

  • No execution time limits — run multi-hour transformations
  • Automatic retries — recover from API failures mid-pipeline
  • Real-time progress — stream row-by-row status to your frontend
  • Parallel processing — scale with configurable concurrency

Pattern 1: CSV Import with Real-Time Progress

ts
import { schemaTask } from "@trigger.dev/sdk";
import { z } from "zod";

// Child task: process individual rows
export const processCSVRow = schemaTask({
  id: "process-csv-row",
  schema: z.object({
    row: z.record(z.string()),
    rowNumber: z.number(),
  }),
  run: async ({ row, rowNumber }) => {
    // Process row, save to database
    await db.insert(table).values(transformRow(row));
    metadata.parent.increment("processedRows", 1);
    return { success: true, rowNumber };
  },
});

// Parent task: coordinate
export const importCSV = schemaTask({
  id: "import-csv",
  schema: z.object({ fileUrl: z.string() }),
  run: async ({ fileUrl }) => {
    metadata.set("status", "downloading");
    const csv = await fetchCSV(fileUrl);
    const rows = parseCSV(csv);

    metadata.set("status", "processing").set("totalRows", rows.length);

    const results = await processCSVRow.batchTriggerAndWait(
      rows.map((row, i) => ({ payload: { row, rowNumber: i + 1 } }))
    );

    metadata.set("status", "complete");
    return { imported: results.filter(r => r.ok).length };
  },
});

Pattern 2: Multi-Source ETL (Coordinator)

ts
export const etlPipeline = task({
  id: "etl-pipeline",
  run: async (payload: { date: string }) => {
    // Parallel extraction from multiple sources
    const [apiData, dbData, s3Data] = await Promise.all([
      extractFromAPI.triggerAndWait({ date: payload.date }),
      extractFromDatabase.triggerAndWait({ date: payload.date }),
      extractFromS3.triggerAndWait({ date: payload.date }),
    ]);

    // Transform and load
    const transformed = await transformData.triggerAndWait({
      api: apiData.output,
      db: dbData.output,
      s3: s3Data.output,
    });

    await loadToWarehouse.triggerAndWait({ data: transformed.output });
    return { processed: transformed.output.recordCount };
  },
});

Pattern 3: Batch Enrichment with Rate Limiting

ts
export const enrichRecords = task({
  id: "enrich-records",
  run: async (payload: { recordIds: string[] }) => {
    const records = await db.fetchRecords(payload.recordIds);

    // Process in batches, respecting API rate limits
    const results = await enrichRecord.batchTrigger(
      records.map(record => ({
        payload: { recordId: record.id },
      })),
      {
        // SDK auto-handles rate limit backoff
      }
    );

    return { total: records.length, triggered: results.runs.length };
  },
});

export const enrichRecord = task({
  id: "enrich-record",
  retry: {
    maxAttempts: 3,
    factor: 2,
  },
  run: async ({ recordId }: { recordId: string }) => {
    const enriched = await externalAPI.enrich(recordId);
    await db.update(records).set(enriched).where(eq(records.id, recordId));
    return { enriched: true };
  },
});

Pattern 4: Parallel Web Scraping

ts
export const scrapePages = task({
  id: "scrape-pages",
  run: async (payload: { urls: string[] }) => {
    const results = await scrapePage.batchTriggerAndWait(
      payload.urls.map(url => ({ payload: { url } }))
    );

    const scraped = results
      .filter(r => r.ok)
      .map(r => r.output);

    await db.insert(scrapedContent).values(scraped);
    return { scraped: scraped.length };
  },
});

export const scrapePage = task({
  id: "scrape-page",
  machine: "small-2x",
  run: async ({ url }: { url: string }) => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto(url);
    const content = await page.evaluate(() => document.body.innerText);
    await browser.close();
    return { url, content };
  },
});

Built from official Trigger.dev documentation