# Idempotent Seed Script

> Write data-seed scripts (add-product, add-user, migrate-data, bootstrap-tenant) to be idempotent — detect existing records and skip rather than duplicate or fail — and run them with a targeted grep (added/skip/error) so the outcome is visible in one line. Use when seeding data into a database or service (e-commerce products, tenants, users, reference data), especially scripts that may be re-run.

- Skill: `jcdavis131/idempotent-seed-script` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jcdavis131/idempotent-seed-script`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jcdavis131/idempotent-seed-script/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: jcdavis131 (https://skillmd.com/u/jcdavis131)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/jcdavis131/idempotent-seed-script

---


# Idempotent Seed Script

A data-seed that isn't idempotent fails or duplicates on a second run. An idempotent seed (detect existing → skip) is re-runnable safely, and a targeted grep shows what happened in one line.

## The shape

```ts
/**
 * Add the cert-service RAG Certification product (Spec 0015, TASK-009).
 * Price is a placeholder pending Operator pricing decision — adjust in admin.
 * npx medusa exec ./src/scripts/add-certification.ts
 */
import { ExecArgs, ContainerRegistrationKeys, Modules, ProductStatus } from "@medusajs/framework";

export default async function addCertification({ container }: ExecArgs) {
  const productModule = container.resolve(Modules.PRODUCT);
  const existing = await productModule.listProducts({ handle: "rag-certification" });
  if (existing.length) {
    console.log("skip: certification product already exists");
    return;
  }
  await productModule.createProducts({ /* ... */ });
  console.log("added: certification product");
}
```

## The rules

1. **Detect existing before creating.** Look up by a stable unique key (handle, slug, sku, external id). If it exists, skip — don't duplicate, don't error.
2. **Log `added` / `skip` explicitly.** A silent seed is un-auditable; an explicit `console.log("added: …")` or `console.log("skip: …")` makes the outcome grep-able.
3. **Cite the spec + task ID in the header.** `(Spec 0015, TASK-009)` — traceability from the seed back to the tracker (see `follow-procedure`).
4. **Document placeholders pending a decision.** `Price is a placeholder pending Operator pricing decision — adjust in admin` (see `document-non-action`). A placeholder is a deferred decision, not a forgotten value.
5. **Run with a targeted grep**, not a raw output dump:
   ```bash
   timeout 240 npx medusa exec ./src/scripts/add-certification.ts 2>&1 | tr '\r' '\n' | grep -iE "certification|error|added|skip" | tail -3
   ```
   - `timeout` — bound it (a seed that hangs on a missing connection shouldn't hang forever).
   - `tr '\r' '\n'` — convert progress-bar CRs to newlines (see `diagnose-before-retry`).
   - `grep -iE "added|skip|error|<topic>"` — surface only the outcome lines.
   - `tail -3` — bound the output.

## When NOT to make it idempotent

- The seed is genuinely one-shot and will never re-run (rare — assume it will).
- The seed is destructive-by-design (a re-seed should overwrite). Then make it explicitly `upsert`, not silent `create`.

## The migration variant: check head, skip if already current

The same idempotent principle applies to schema migrations. Before running migrations, check the DB's current migration head:

- If the DB is already at head → skip the migrate. A no-op migrate on an already-current DB is wasted work and can still error on a partially-applied state.
- If the DB is behind head → run the migrate (it'll apply the pending set).

This is the affected-set principle applied to migrations: don't redo what's already applied. Pair with a head-check command (`alembic current`, `prisma migrate status`, `medusa db:migrate --skip` if supported) before the migrate.

## Anti-patterns

- **Bare `createProducts` with no existence check.** Re-run → duplicate product, broken catalog.
- **Silent success.** The seed ran, but no log line — you can't tell from the output whether it added, skipped, or did nothing.
- **No spec/task citation.** The seed exists but isn't traceable to why it was written.
- **Raw output dump.** A medusa exec log is hundreds of lines; the one line you care about (`added` / `skip`) is buried.
- **Hardcoded final value for a pending decision.** Price set to a real number with no "placeholder pending decision" note → the Operator doesn't know to adjust it.

## Pair with

- `document-non-action` — the placeholder-pending-decision is a documented deferred value.
- `diagnose-before-retry` — the `timeout | tr | grep | tail` run idiom is shared.
- `policy-as-config` — a seed that writes policy-governed data (products, consent) should encode the policy fields, not just the rows.

