Migration Guide: Trigger.dev to Dagster ERP Pipeline
Overview
Section titled βOverviewβThis guide documents the migration of ERP data synchronization pipelines from Trigger.dev to Dagster. This migration provides better observability, scheduling, and multi-tenant support for ERP data pipelines.
What Changed
Section titled βWhat ChangedβBefore (Trigger.dev)
Section titled βBefore (Trigger.dev)β- Location:
apps/trigger/src/python/etl/ - Execution: Trigger.dev Python extension
- Scheduling: Manual triggers or TypeScript task wrappers
- Observability: Limited to Trigger.dev dashboard
- Multi-tenancy: Manual connection ID passing
- Orchestration: Basic sequential execution
After (Dagster)
Section titled βAfter (Dagster)β- Location:
apps/dagster/erp_pipeline/ - Execution: Dagster webserver with native Python support
- Scheduling: Built-in cron schedules and sensors
- Observability: Full Dagster UI with asset lineage, run history, and logs
- Multi-tenancy: Automatic discovery via sensors, dynamic partitions
- Orchestration: Declarative asset-based pipeline with dependencies
Key Improvements
Section titled βKey Improvementsβ1. Multi-Tenant Architecture
Section titled β1. Multi-Tenant ArchitectureβBefore: Manual connection ID management
# Old: Manual connection passingdef sync_products(connection_id: str): # ... sync logicAfter: Automatic discovery and partitioning
# New: Automatic partition discovery@sensor(name="erp_connection_discovery_sensor")def discover_connections(context): # Automatically discovers new tenants from database # Creates partitions for each connection2. Better Observability
Section titled β2. Better Observabilityβ- Asset Lineage: Visual graph of data dependencies
- Run History: Complete audit trail of all syncs
- Error Tracking: Detailed error messages and stack traces
- Performance Metrics: Execution time, data volume, success rates
3. Improved Scheduling
Section titled β3. Improved SchedulingβBefore: Manual triggers or basic cron
// Old: TypeScript wrapperawait tasks.trigger('sync-erp-entity', { connectionId: 'uuid', entityType: 'products',});After: Declarative schedules
# New: Built-in cron schedules@schedule(cron_schedule="0 */2 * * *", job=bronze_sync)def bronze_sync_schedule(context): return RunRequest()4. Incremental Sync Support
Section titled β4. Incremental Sync SupportβThe new pipeline includes built-in incremental sync capabilities:
- Automatically detects first sync vs. incremental sync
- Tracks last sync date from Iceberg tables
- Only fetches changed records for efficiency
Migration Steps
Section titled βMigration StepsβStep 1: Review Old Code (Optional)
Section titled βStep 1: Review Old Code (Optional)βThe old Trigger.dev pipeline code has been moved to apps/dagster/erp_pipeline/ with improvements:
- Sources:
apps/trigger/src/python/etl/sources/βapps/dagster/erp_pipeline/sources/ - Transformers:
apps/trigger/src/python/etl/transformers/βapps/dagster/erp_pipeline/transformers/ - Pipelines:
apps/trigger/src/python/etl/*_pipeline.pyβapps/dagster/erp_pipeline/implementations/
Step 2: Update Environment Variables
Section titled βStep 2: Update Environment VariablesβAdd Dagster-specific environment variables to your .env:
# Dagster ConfigurationDAGSTER_HOME=/path/to/dagster-home # Optional: for local development
# R2 Storage (required for Iceberg tables)R2_ENDPOINT_URL=https://<account-id>.r2.cloudflarestorage.comR2_ACCESS_KEY_ID=...R2_SECRET_ACCESS_KEY=...R2_DATA_CATALOG_BUCKET=erp-data-catalogR2_PRODUCT_IMAGES_BUCKET=erp-product-images
# Typesense (for search sync - use admin key for ETL operations)TYPESENSE_HOST=localhostTYPESENSE_PORT=8108TYPESENSE_ADMIN_API_KEY=...TYPESENSE_PROTOCOL=http
# Database (for ERP connection discovery)DATABASE_URL=postgresql://...ENCRYPTION_KEY=... # For decrypting ERP passwordsStep 3: Start Dagster Service
Section titled βStep 3: Start Dagster ServiceβOption A: Docker (Recommended)
# From project rootdocker compose up -d dagsterdocker compose logs -f dagsterAccess Dagster UI at: http://localhost:3000
Option B: Native Python
cd apps/dagsteruv syncuv run dagster devStep 4: Verify Pipeline
Section titled βStep 4: Verify Pipelineβ- Check Asset Graph: Navigate to Assets β View the asset dependency graph
- Test Discovery: Verify sensors are discovering ERP connections
- Run Test Sync: Materialize a single asset for a test connection
Step 5: Disable Old Trigger.dev Tasks (Optional)
Section titled βStep 5: Disable Old Trigger.dev Tasks (Optional)βIf you want to completely disable the old Trigger.dev pipeline:
# In apps/trigger/.envENABLE_ICEBERG_SYNC=falseENABLE_DLT_NATIVE_SYNC=falseNote: The old code has been removed from apps/trigger/src/python/etl/, so these flags are no longer functional. Theyβre kept for backward compatibility only.
Architecture Comparison
Section titled βArchitecture ComparisonβOld Architecture (Trigger.dev)
Section titled βOld Architecture (Trigger.dev)βββββββββββββββββββββ Trigger.dev ββ TypeScript ββ Task Wrapper βββββββββββ¬βββββββββ β βΌββββββββββββββββββββ Python DLT ββ Pipeline ββ (Sequential) βββββββββββ¬βββββββββ β βΌββββββββββββββββββββ Iceberg (R2) ββββββββββββββββββββNew Architecture (Dagster)
Section titled βNew Architecture (Dagster)βββββββββββββββββββββββββββββββββββββββββ Dagster Webserver ββ βββββββββββββββββββββββββββββββββ ββ β Discovery Sensor β ββ β (Auto-discovers connections) β ββ ββββββββββββββββ¬βββββββββββββββββ ββ β ββ ββββββββββββββββΌβββββββββββββββββ ββ β Dynamic Partitions β ββ β (One per connection_id) β ββ ββββββββββββββββ¬βββββββββββββββββ ββ β ββ ββββββββββββββββΌβββββββββββββββββ ββ β Asset Graph β ββ β Bronze β dbt β Typesense β ββ ββββββββββββββββ¬βββββββββββββββββ ββββββββββββββββββββΌβββββββββββββββββββ β βΌ βββββββββββββββββββ β Iceberg (R2) β β Typesense β βββββββββββββββββββCode Mapping
Section titled βCode MappingβPipeline Files
Section titled βPipeline Filesβ| Old Location | New Location | Notes |
|---|---|---|
apps/trigger/src/python/etl/base_pipeline.py | apps/dagster/erp_pipeline/implementations/p21_sync_orchestrator.py | Refactored as orchestrator |
apps/trigger/src/python/etl/products_pipeline.py | apps/dagster/erp_pipeline/assets/bronze.py | Converted to Dagster asset |
apps/trigger/src/python/etl/customers_pipeline.py | apps/dagster/erp_pipeline/assets/bronze.py | Converted to Dagster asset |
apps/trigger/src/python/etl/sources/p21_odata.py | apps/dagster/erp_pipeline/sources/p21_odata.py | Moved, improved |
apps/trigger/src/python/etl/transformers/ | apps/dagster/erp_pipeline/transformers/ | Moved, added registry |
Key Changes
Section titled βKey Changesβ- Factory Pattern: Added
ERPSourceFactoryfor ERP-agnostic design - Transformer Registry: Centralized transformer mapping
- Incremental Sync: Built-in sync state management
- Multi-ERP Support: Protocol-based design for adding new ERPs
Running Pipelines
Section titled βRunning PipelinesβOld Way (Trigger.dev)
Section titled βOld Way (Trigger.dev)β// From TypeScript codeawait tasks.trigger('sync-erp-entity', { connectionId: 'uuid', entityType: 'products', options: { fullSync: true },});New Way (Dagster)
Section titled βNew Way (Dagster)βVia UI:
- Navigate to Assets β Select asset (e.g.,
bronze/products) - Click Materialize β Select partition(s)
- Monitor run in Runs tab
Via CLI:
# Sync specific connectionuv run dagster job execute -j bronze_sync --partition "connection-uuid"
# Sync all connectionsuv run dagster job execute -j bronze_syncVia GraphQL API:
const response = await fetch(DAGSTER_CLOUD_URL + '/graphql', { method: 'POST', headers: { 'Dagster-Cloud-Api-Token': DAGSTER_CLOUD_API_TOKEN, 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `mutation LaunchRun($jobName: String!, $partitionKey: String) { launchRun(executionParams: { selector: { jobName: $jobName } runConfigData: {} tags: [{ key: "dagster/partition", value: $partitionKey }] }) { ... on LaunchRunSuccess { run { runId } } } }`, variables: { jobName: 'full_sync_pipeline', partitionKey: 'connection-uuid', }, }),});Monitoring & Debugging
Section titled βMonitoring & DebuggingβOld Way (Trigger.dev)
Section titled βOld Way (Trigger.dev)β- Trigger.dev dashboard
- Limited run history
- Basic error messages
New Way (Dagster)
Section titled βNew Way (Dagster)βDagster UI Features:
- Asset Lineage: Visual dependency graph
- Run History: Complete audit trail
- Logs: Structured logging with context
- Metrics: Performance and data volume metrics
- Retries: Automatic retry on failure
Debug Tools:
apps/dagster/debug_auth.py- Test ERP authenticationapps/dagster/test_image_sync.py- Test image downloadsapps/dagster/list_runs.py- List and filter runsapps/dagster/cancel_hanging_runs.py- Cancel stuck runs
Scheduling
Section titled βSchedulingβOld Way (Trigger.dev)
Section titled βOld Way (Trigger.dev)βManual triggers or basic cron via TypeScript tasks.
New Way (Dagster)
Section titled βNew Way (Dagster)βBuilt-in schedules defined in apps/dagster/erp_pipeline/schedules.py:
| Schedule | Cron | Description |
|---|---|---|
bronze_sync_schedule | 0 */2 * * * | Incremental extract every 2 hours |
dbt_transform_schedule | 30 * * * * | Transform hourly |
typesense_sync_schedule | 45 * * * * | Search sync hourly |
Schedules automatically run for all active connections (discovered via sensors).
Multi-Tenant Support
Section titled βMulti-Tenant SupportβOld Way
Section titled βOld WayβManual connection ID passing:
# Old: Manual connection managementdef sync_products(connection_id: str): # ... sync logicNew Way
Section titled βNew WayβAutomatic discovery and isolation:
- Discovery Sensor: Runs every 5 minutes, discovers active connections
- Dynamic Partitions: Each connection becomes a partition
- Data Isolation: All queries filtered by
connection_id - Parallel Execution: Multiple tenants sync simultaneously
Rollback Plan
Section titled βRollback PlanβIf you need to rollback to Trigger.dev:
- Restore Old Code: Checkout commit before migration
- Re-enable Tasks: Set
ENABLE_ICEBERG_SYNC=truein Trigger.dev - Stop Dagster:
docker compose stop dagster
Note: The old Trigger.dev pipeline code has been removed. Rollback requires restoring from git history.
Troubleshooting
Section titled βTroubleshootingβIssue: Dagster not discovering connections
Section titled βIssue: Dagster not discovering connectionsβSolution: Check database connection and ensure erp_connections table has active records.
# Test database connectioncd apps/dagsteruv run python -c "from erp_pipeline.partitions import get_active_erp_connections; print(get_active_erp_connections())"Issue: Assets not materializing
Section titled βIssue: Assets not materializingβSolution: Check Dagster logs and ensure R2 credentials are correct.
# Check Dagster logsdocker compose logs dagster
# Verify R2 accessuv run python -c "from erp_pipeline.utils.sync_state import get_iceberg_table; print(get_iceberg_table('products'))"Issue: Incremental sync not working
Section titled βIssue: Incremental sync not workingβSolution: Verify sync state utilities can read from Iceberg tables.
# Test sync stateuv run python -c "from erp_pipeline.utils.sync_state import get_last_sync_date; print(get_last_sync_date('products', 'connection-uuid'))"Next Steps
Section titled βNext Stepsβ- Review Dagster README:
apps/dagster/README.mdfor detailed usage - Monitor First Runs: Watch initial syncs in Dagster UI
- Adjust Schedules: Modify cron schedules in
schedules.pyif needed - Add Tests: Consider adding integration tests for critical paths
Support
Section titled βSupportβ- Dagster Documentation: https://docs.dagster.io/
- Project README:
apps/dagster/README.md - Architecture Docs:
docs/architecture/erp-sync-architecture.md
Summary
Section titled βSummaryββ
Migrated: ERP sync pipelines from Trigger.dev to Dagster
β
Improved: Multi-tenant support, observability, scheduling
β
Maintained: Same data flow (DLT β Iceberg β dbt β Typesense)
β
Enhanced: Incremental sync, automatic discovery, better error handling
The migration maintains backward compatibility with existing data structures while providing a more robust, observable, and maintainable pipeline infrastructure.