AGPL-3.0-or-later · forever.

showcase · tier 2 · ops & support

ETL quarantine

A schema gate splits good rows from bad. Rejects land in a quarantine file and the pipeline keeps going.

“Re-run the whole night” turns into “fix three rows tomorrow morning”. · conformance-gated in nika-spec ↗ · re-proven at every push

The whole file

7 tasks · 5 waves · the plan falls out of the bindings, nothing is scheduled by hand.

t2-etl-quarantine.nika.yamlsource
nika: v1workflow:  id: etl-quarantine  description: "CSV batch → schema gate → quarantine the bad · aggregate the good"vars:  batch_csv: "./data/incoming/orders.csv"tasks:  # A deterministic empty fallback · the recover target when parsing dies.  empty_batch:    invoke:      tool: "nika:jq"      args: { input: [], expression: "." }  raw:    invoke:      tool: "nika:read"      args: { path: "${{ vars.batch_csv }}" }  rows:    with:      csv: ${{ tasks.raw.output }}    invoke:      tool: "nika:convert"      args:        input: "${{ with.csv }}"        from: csv        to: json        has_header: true    on_error:      recover: ${{ tasks.empty_batch.output }}    # malformed CSV → empty batch · pipeline lives  check:    with:      rows: ${{ tasks.rows.output }}    invoke:      tool: "nika:validate"      args:        data: "${{ with.rows }}"        format: json        schema:          type: array          items:            type: object            required: [order_id, amount, currency]            properties:              order_id: { type: string }              amount: { type: string }              currency: { type: string, enum: [EUR, USD, GBP] }  good:    with:      rows: ${{ tasks.rows.output }}      valid: ${{ tasks.check.output.valid }}    when: ${{ with.valid == true }}    invoke:      tool: "nika:jq"      args:        input: "${{ with.rows }}"        expression: 'group_by(.currency) | map({currency: .[0].currency, orders: length, total: (map(.amount | tonumber) | add)})'  quarantine:    with:      valid: ${{ tasks.check.output.valid }}      errors: ${{ tasks.check.output.errors }}    when: ${{ with.valid == false }}    invoke:      tool: "nika:write"      args:        path: "./data/quarantine/orders-rejected.json"        content: "${{ with.errors }}"        create_dirs: true  report:    with:      totals: ${{ tasks.good.output }}    when: ${{ with.totals != null && size(with.totals) > 0 }}   # good is SKIPPED (null) on the quarantine path · guard before size()    invoke:      tool: "nika:write"      args:        path: "./data/reports/daily-totals.json"        content: "${{ with.totals }}"        create_dirs: trueoutputs:  # Deliberately untyped · on the quarantine path `good` is SKIPPED and  # this is null — a declared `type: array` would refuse the branch the  # file exists to demonstrate.  totals: ${{ tasks.good.output }}

nika inspect · engine 0.105.0 · vendored graph, never re-derived

8 declared edges · 5 waves · the plan falls out of the bindings