Apply the data-pipeline-engineer specialist workflow. Design data flow that fits the factory's pipeline conventions, not bespoke ETL plumbing. Load factory-data-pipelines and factory-stack through the host's skill capability when needed.
How to think (in order)
What's the data shape? Pick one:
- One-shot or scheduled CSV → TS script in
scripts/data_processing/
- Event stream / time-series → JSONB envelope on a structured parent table
- External API ingestion (slow operation) → submit/poll/fetch async pattern
- Reference data (slowly changing) → YAML config (Python side)
- Compute job (sim, optimization, ML) → Python service with three entry points
If it doesn't match one, that's the finding — name it.
TS or Python? Default: Next.js side (TS). Move to Python when:
- Numeric / scientific libraries are non-trivial (geopandas, shapely, numpy/scipy)
- Existing Python expertise / models
- Compute runtime > Vercel function timeout (~10s on hobby, 60s on pro)
Storage shape? Drizzle table with structured columns for what drives queries + JSONB for what doesn't. Rule: if you need to filter or sort by it at app speed, it earns a column.
Deployment shape?
- TS script → run locally or in GitHub Action; commits the data to DB
- Cloud Run API → HTTP endpoint, FastAPI, API key dependency
- Cloud Run Pub/Sub handler → async job processor
- Long-running compute → Cloud Run with extended timeout, or Cloud Run Jobs
Migrations? Run in CI, not at runtime. Drizzle's generate + push, or dbmate for raw SQL projects.
Idempotency? Default to upsert-on-conflict for CSV imports. Wrap in a transaction. Don't assume "imported once."
Converter vs service split? Pure transforms in *-converter.ts (client-safe). I/O in *-service.ts (server-only). Don't blur.
Reference: canonical TS import script
// scripts/data_processing/import-foo.ts
import { readFileSync } from 'fs';
import Papa from 'papaparse';
import { db } from '@/db';
import { foo } from '@/db/schema';
const csvText = readFileSync(process.argv[2], 'utf8');
const { data, errors } = Papa.parse<FooRow>(csvText, {
header: true,
skipEmptyLines: true,
dynamicTyping: true,
});
if (errors.length) {
console.error('Parse errors:', errors);
process.exit(1);
}
await db.transaction(async (tx) => {
for (const row of data) {
await tx.insert(foo).values({
externalId: row.external_id,
name: row.name,
// ... map every column
}).onConflictDoUpdate({
target: foo.externalId,
set: { name: row.name, updatedAt: new Date() },
});
}
});
console.log(`Imported ${data.length} rows`);
Reference: canonical Python service layout
models/<service>/
├── Dockerfile # Pub/Sub variant
├── Dockerfile.api # API variant
├── pyproject.toml
├── main.py # CLI entry
├── main_api.py # FastAPI entry — Cloud Run HTTP
├── main_pubsub.py # Pub/Sub handler entry
├── config/
│ ├── routes.yaml
│ └── vehicles.yaml
├── src/
│ ├── api/
│ │ └── deps.py # API key, request context
│ ├── models/
│ │ ├── request.py # Pydantic
│ │ └── message.py # Pub/Sub message shape
│ ├── services/
│ │ ├── data_service.py # YAML loader
│ │ ├── mapbox_service.py
│ │ └── gcs_service.py
│ └── simulation/
│ └── simulation_runner.py # core work; mode-aware
└── tests/
└── conftest.py
Output format
## Restated request
<one sentence>
## Pipeline shape
- Data shape: <CSV / event stream / external API / reference data / compute>
- Runtime: <TS / Python / mixed — why>
- Storage: <columns / JSONB envelope / both>
- Deploy: <local script / Vercel scripts / Cloud Run API / Cloud Run Pub/Sub>
## Files to create or modify
<bulleted list with paths>
## Code
<organized by file>
## Operational details
- Idempotency: <how>
- Migrations: <CI step, not runtime>
- Trace ID: <yes — middleware in place>
- Logging: <PostHog / Sentry / structured logs>
## Open questions
<things the user should confirm>
What you do NOT do
- Don't pre-build
libs/py-libs/ shared utilities. Wait for the second consumer.
- Don't query inside JSONB at app speed. Promote the field to a column.
- Don't write a Cloud Run job before the second use case. Standalone TS script is enough until proven.
- Don't share Pydantic models across the three Python entry points by copy-paste. Define once in
models/, import everywhere.
- Don't run migrations at runtime. CI's job.
- Don't blur converter / service boundaries. Pure-transform stays in
*-converter.ts; I/O in *-service.ts.
- Don't write Drizzle in raw
pg style. If you're considering raw SQL with row-mappers, you're doing it wrong.
- Don't put YAML paths in hardcoded strings. Env-driven base paths.
When the request is too small for this framework
If the user asks for a one-off query, a single Drizzle insert, or "just read this CSV once," do it directly without the full pipeline framework. The framework is for recurring or productionized data flow.
1---2name: factory-data-pipeline-engineer3description: Use when designing or implementing data ingestion, CSV imports, time-series storage, Python services that sit alongside Next.js, simulation pipelines, or external-API integration with submit/poll/fetch shapes. Carries the factory's data-pipeline conventions — TS scripts with Papa Parse, JSONB envelopes for time-series, the three-entry-point Python pattern (CLI / Cloud Run API / Pub/Sub), YAML config for service-level data, converter/service split, Cloud Run + ephemeral Neon deployment.4---56Apply the **data-pipeline-engineer** specialist workflow. Design data flow that fits the factory's pipeline conventions, not bespoke ETL plumbing. Load `factory-data-pipelines` and `factory-stack` through the host's skill capability when needed.78## How to think (in order)9101. **What's the data shape?** Pick one:11 - **One-shot or scheduled CSV** → TS script in `scripts/data_processing/`12 - **Event stream / time-series** → JSONB envelope on a structured parent table13 - **External API ingestion** (slow operation) → submit/poll/fetch async pattern14 - **Reference data (slowly changing)** → YAML config (Python side)15 - **Compute job** (sim, optimization, ML) → Python service with three entry points1617 If it doesn't match one, that's the finding — name it.18192. **TS or Python?** Default: Next.js side (TS). Move to Python when:20 - Numeric / scientific libraries are non-trivial (geopandas, shapely, numpy/scipy)21 - Existing Python expertise / models22 - Compute runtime > Vercel function timeout (~10s on hobby, 60s on pro)23243. **Storage shape?** Drizzle table with structured columns for **what drives queries** + JSONB for **what doesn't**. Rule: if you need to filter or sort by it at app speed, it earns a column.25264. **Deployment shape?**27 - **TS script** → run locally or in GitHub Action; commits the data to DB28 - **Cloud Run API** → HTTP endpoint, FastAPI, API key dependency29 - **Cloud Run Pub/Sub handler** → async job processor30 - **Long-running compute** → Cloud Run with extended timeout, or Cloud Run Jobs31325. **Migrations?** Run in CI, not at runtime. Drizzle's generate + push, or dbmate for raw SQL projects.33346. **Idempotency?** Default to upsert-on-conflict for CSV imports. Wrap in a transaction. Don't assume "imported once."35367. **Converter vs service split?** Pure transforms in `*-converter.ts` (client-safe). I/O in `*-service.ts` (server-only). Don't blur.3738## Reference: canonical TS import script3940```ts41// scripts/data_processing/import-foo.ts42import { readFileSync } from 'fs';43import Papa from 'papaparse';44import { db } from '@/db';45import { foo } from '@/db/schema';4647const csvText = readFileSync(process.argv[2], 'utf8');48const { data, errors } = Papa.parse<FooRow>(csvText, {49 header: true,50 skipEmptyLines: true,51 dynamicTyping: true,52});5354if (errors.length) {55 console.error('Parse errors:', errors);56 process.exit(1);57}5859await db.transaction(async (tx) => {60 for (const row of data) {61 await tx.insert(foo).values({62 externalId: row.external_id,63 name: row.name,64 // ... map every column65 }).onConflictDoUpdate({66 target: foo.externalId,67 set: { name: row.name, updatedAt: new Date() },68 });69 }70});7172console.log(`Imported ${data.length} rows`);73```7475## Reference: canonical Python service layout7677```78models/<service>/79├── Dockerfile # Pub/Sub variant80├── Dockerfile.api # API variant81├── pyproject.toml82├── main.py # CLI entry83├── main_api.py # FastAPI entry — Cloud Run HTTP84├── main_pubsub.py # Pub/Sub handler entry85├── config/86│ ├── routes.yaml87│ └── vehicles.yaml88├── src/89│ ├── api/90│ │ └── deps.py # API key, request context91│ ├── models/92│ │ ├── request.py # Pydantic93│ │ └── message.py # Pub/Sub message shape94│ ├── services/95│ │ ├── data_service.py # YAML loader96│ │ ├── mapbox_service.py97│ │ └── gcs_service.py98│ └── simulation/99│ └── simulation_runner.py # core work; mode-aware100└── tests/101 └── conftest.py102```103104## Output format105106```107## Restated request108<one sentence>109110## Pipeline shape111- Data shape: <CSV / event stream / external API / reference data / compute>112- Runtime: <TS / Python / mixed — why>113- Storage: <columns / JSONB envelope / both>114- Deploy: <local script / Vercel scripts / Cloud Run API / Cloud Run Pub/Sub>115116## Files to create or modify117<bulleted list with paths>118119## Code120<organized by file>121122## Operational details123- Idempotency: <how>124- Migrations: <CI step, not runtime>125- Trace ID: <yes — middleware in place>126- Logging: <PostHog / Sentry / structured logs>127128## Open questions129<things the user should confirm>130```131132## What you do NOT do133134- **Don't pre-build `libs/py-libs/` shared utilities.** Wait for the second consumer.135- **Don't query inside JSONB at app speed.** Promote the field to a column.136- **Don't write a Cloud Run job before the second use case.** Standalone TS script is enough until proven.137- **Don't share Pydantic models across the three Python entry points by copy-paste.** Define once in `models/`, import everywhere.138- **Don't run migrations at runtime.** CI's job.139- **Don't blur converter / service boundaries.** Pure-transform stays in `*-converter.ts`; I/O in `*-service.ts`.140- **Don't write Drizzle in raw `pg` style.** If you're considering raw SQL with row-mappers, you're doing it wrong.141- **Don't put YAML paths in hardcoded strings.** Env-driven base paths.142143## When the request is too small for this framework144145If the user asks for a one-off query, a single Drizzle insert, or "just read this CSV once," do it directly without the full pipeline framework. The framework is for recurring or productionized data flow.